From 4c7c9a5c667ab0c1fcca5b87c8b2832f8fd6c19a Mon Sep 17 00:00:00 2001 From: Jan Schulte Date: Tue, 4 Feb 2025 16:08:52 +0100 Subject: [PATCH] add extent facet --- pnpm-lock.yaml | 3 + src/apps/empty/build.config.mjs | 6 + .../empty/components/facets/FacetList.tsx | 15 +- .../components/facets/GeometryExtentFacet.tsx | 277 ++++++++++++++++++ src/apps/empty/package.json | 1 + src/apps/empty/services.ts | 4 + .../services/facet-extent-map-provider.ts | 60 ++++ src/packages/catalog/CatalogService.ts | 60 ++++ .../GeonodeCatalogServiceImpl.ts | 17 +- 9 files changed, 433 insertions(+), 10 deletions(-) create mode 100644 src/apps/empty/components/facets/GeometryExtentFacet.tsx create mode 100644 src/apps/empty/services/facet-extent-map-provider.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ed7afb..4983816 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -382,6 +382,9 @@ importers: '@open-pioneer/map': specifier: 'catalog:' version: 0.8.0(@emotion/is-prop-valid@1.3.0)(@types/react@18.3.11)(typescript@5.6.3) + '@open-pioneer/map-navigation': + specifier: 'catalog:' + version: 0.8.0(@emotion/is-prop-valid@1.3.0)(@types/react@18.3.11)(typescript@5.6.3) '@open-pioneer/notifier': specifier: 'catalog:' version: 2.4.0(@chakra-ui/react@2.10.4(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@11.3.31(@emotion/is-prop-valid@1.3.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/is-prop-valid@1.3.0)(@types/react@18.3.11)(typescript@5.6.3) diff --git a/src/apps/empty/build.config.mjs b/src/apps/empty/build.config.mjs index 3753f98..7fe7de6 100644 --- a/src/apps/empty/build.config.mjs +++ b/src/apps/empty/build.config.mjs @@ -22,6 +22,12 @@ export default defineBuildConfig({ }, ResultPreviewMapProvider: { provides: ["map.MapConfigProvider"] + }, + FacetExtentMapProvider: { + provides: ["map.MapConfigProvider"] + }, + FacetExtentModalMapProvider: { + provides: ["map.MapConfigProvider"] } } }); diff --git a/src/apps/empty/components/facets/FacetList.tsx b/src/apps/empty/components/facets/FacetList.tsx index 9fb65c7..9cd7919 100644 --- a/src/apps/empty/components/facets/FacetList.tsx +++ b/src/apps/empty/components/facets/FacetList.tsx @@ -11,7 +11,8 @@ import { } from "@open-pioneer/chakra-integration"; import { DateFacetComp } from "./DateFacet"; import { MultiSelectFacet } from "./MultiSelectFacet"; -import { DateFacet, Facet, MultiSelectionFacet } from "catalog"; +import { DateFacet, Facet, GeometryExtentFacet, MultiSelectionFacet } from "catalog"; +import { GeometryExtentFacetComp } from "./GeometryExtentFacet"; export function FacetList(props: { facets: Facet[] }) { const { facets } = props; @@ -46,15 +47,13 @@ function FacetComp(props: { facet: Facet }) { function getFacetContent(isExpanded: boolean) { if (facet instanceof DateFacet) { - return ; + return ; } if (facet instanceof MultiSelectionFacet) { - return ( - - ); + return ; + } + if (facet instanceof GeometryExtentFacet) { + return ; } console.error("Could not find facet type"); return <>; diff --git a/src/apps/empty/components/facets/GeometryExtentFacet.tsx b/src/apps/empty/components/facets/GeometryExtentFacet.tsx new file mode 100644 index 0000000..7c33855 --- /dev/null +++ b/src/apps/empty/components/facets/GeometryExtentFacet.tsx @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 + +import { GeometryExtentFacet } from "catalog"; +import { + Box, + Button, + IconButton, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + useDisclosure, + VStack +} from "@open-pioneer/chakra-integration"; +import { MdClear, MdEdit } from "react-icons/md"; +import { MapAnchor, MapContainer, MapModel, useMapModel } from "@open-pioneer/map"; +import Draw, { createBox } from "ol/interaction/Draw"; +import OlMap from "ol/Map"; +import { + FACET_EXTENT_MAP_ID, + FACET_EXTENT_MODAL_MAP_ID, + MAP_PROJECTION +} from "../../services/facet-extent-map-provider"; +import { ZoomIn, ZoomOut } from "@open-pioneer/map-navigation"; +import VectorSource from "ol/source/Vector"; +import VectorLayer from "ol/layer/Vector"; +import { useService } from "open-pioneer:react-hooks"; +import { useEffect, useState } from "react"; +import Feature from "ol/Feature"; +import { SearchService } from "../../services/search-service"; +import { transformExtent } from "ol/proj"; +import { fromExtent } from "ol/geom/Polygon"; + +export function GeometryExtentFacetComp(props: { + facet: GeometryExtentFacet; + isExpanded: boolean; +}) { + const { facet, isExpanded } = props; + const { isOpen, onOpen, onClose } = useDisclosure(); + const searchSrvc = useService("SearchService"); + const facetMapModel = useMapModel(FACET_EXTENT_MAP_ID); + const modalMapModal = useMapModel(FACET_EXTENT_MODAL_MAP_ID); + const controller = useExtentController(facetMapModel.map, modalMapModal.map, facet, searchSrvc); + + useEffect(() => { + if (isExpanded) { + controller?.fitFeature(); + } + }, [isExpanded, controller]); + + return ( + <> + + + + + + + + + + + } + onClick={() => { + onOpen(); + controller?.activateExtentDraw(); + }} + /> + {controller?.hasExtent() && ( + } + onClick={() => controller?.clearExtent()} + /> + )} + + + + + + + + + Modal Title + + + + + + + + + + + + + } + onClick={() => controller?.activateExtentDraw()} + /> + + + + + + + + + + + + + ); +} + +function useExtentController( + facetMapModel: MapModel | undefined, + modalMapModel: MapModel | undefined, + facet: GeometryExtentFacet, + searchSrvc: SearchService +) { + const [controller, setController] = useState(undefined); + + useEffect(() => { + if (!modalMapModel || !facetMapModel) { + return; + } + const controller = new ExtentController( + facetMapModel.olMap, + modalMapModel.olMap, + facet, + searchSrvc + ); + setController(controller); + + return () => { + controller.destroy(); + setController(undefined); + }; + }, [facetMapModel, modalMapModel, facet, searchSrvc]); + + return controller; +} + +class ExtentController { + private modalSource = new VectorSource({ wrapX: false }); + private modalVectorLayer = new VectorLayer({ source: this.modalSource }); + + private facetSource = new VectorSource({ wrapX: false }); + private facetVectorLayer = new VectorLayer({ source: this.facetSource }); + + private extentFeature: Feature | undefined; + + constructor( + private facetMap: OlMap, + private modalMap: OlMap, + private facet: GeometryExtentFacet, + private searchSrvc: SearchService + ) { + const extent = this.facet.getExtent(); + if (extent) { + const geometry = fromExtent([ + extent.left, + extent.bottom, + extent.right, + extent.top + ]).transform("EPSG:4326", MAP_PROJECTION); + this.extentFeature = new Feature({ geometry }); + this.setExtentOnFacetMap(); + this.modalSource.addFeature(this.extentFeature); + } + this.initLayer(); + } + + private setExtentOnFacetMap() { + if (this.extentFeature) { + this.facetSource.clear(); + this.facetSource.addFeature(this.extentFeature); + } + } + + hasExtent() { + return this.extentFeature !== undefined; + } + + fitFeature() { + if (this.extentFeature) { + const extent = this.extentFeature.getGeometry()?.getExtent(); + if (extent) { + setTimeout(() => { + this.facetMap + .getView() + .fit(extent, { padding: [30, 30, 30, 30], duration: 300 }); + }, 10); + } + } + } + + activateExtentDraw() { + const draw = new Draw({ + type: "Circle", + geometryFunction: createBox() + }); + this.modalMap.addInteraction(draw); + draw.on("drawend", (evt) => { + this.extentFeature = evt.feature; + this.modalSource.clear(); + this.modalSource.addFeature(this.extentFeature); + this.modalMap.removeInteraction(draw); + }); + } + + clearExtent() { + this.extentFeature = undefined; + this.modalSource.clear(); + this.facetSource.clear(); + this.facet.clearFilter(); + this.searchSrvc.startSearch(); + } + + applyExtent() { + const extent = this.extentFeature?.getGeometry()?.getExtent(); + if (extent) { + this.setExtentOnFacetMap(); + this.fitFeature(); + const transformedExtent = transformExtent(extent, MAP_PROJECTION, "EPSG:4326"); + const [left, bottom, right, top] = transformedExtent; + if ( + left !== undefined && + bottom !== undefined && + top !== undefined && + right !== undefined + ) { + this.facet.setExtent({ left, bottom, right, top }); + this.searchSrvc.startSearch(); + } + } + } + + private initLayer() { + this.modalMap.addLayer(this.modalVectorLayer); + this.facetMap.addLayer(this.facetVectorLayer); + } + + destroy() { + this.modalSource.clear(); + this.facetSource.clear(); + this.modalMap.removeLayer(this.modalVectorLayer); + this.facetMap.removeLayer(this.facetVectorLayer); + } +} diff --git a/src/apps/empty/package.json b/src/apps/empty/package.json index a3c92c9..bc56cd0 100644 --- a/src/apps/empty/package.json +++ b/src/apps/empty/package.json @@ -4,6 +4,7 @@ "dependencies": { "@open-pioneer/chakra-integration": "catalog:", "@open-pioneer/map": "catalog:", + "@open-pioneer/map-navigation": "catalog:", "@open-pioneer/notifier": "catalog:", "@open-pioneer/reactivity": "catalog:", "@open-pioneer/runtime": "catalog:", diff --git a/src/apps/empty/services.ts b/src/apps/empty/services.ts index 37eae11..27236d1 100644 --- a/src/apps/empty/services.ts +++ b/src/apps/empty/services.ts @@ -3,3 +3,7 @@ export { SearchServiceImpl } from "./services/search-service"; export { ResultPreviewMapProvider } from "./services/result-preview-map-provider"; +export { + FacetExtentMapProvider, + FacetExtentModalMapProvider +} from "./services/facet-extent-map-provider"; diff --git a/src/apps/empty/services/facet-extent-map-provider.ts b/src/apps/empty/services/facet-extent-map-provider.ts new file mode 100644 index 0000000..341cc63 --- /dev/null +++ b/src/apps/empty/services/facet-extent-map-provider.ts @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) +// SPDX-License-Identifier: Apache-2.0 + +import { MapConfig, MapConfigProvider, SimpleLayer } from "@open-pioneer/map"; +import TileLayer from "ol/layer/Tile"; +import { OSM } from "ol/source"; + +export const MAP_PROJECTION = "EPSG:3857"; + +export const FACET_EXTENT_MAP_ID = "facet_extent"; +export class FacetExtentMapProvider implements MapConfigProvider { + mapId = FACET_EXTENT_MAP_ID; + + async getMapConfig(): Promise { + return { + initialView: { + kind: "position", + center: { x: 0, y: 0 }, + zoom: 1 + }, + projection: MAP_PROJECTION, + layers: [ + new SimpleLayer({ + title: "OpenStreetMap", + olLayer: new TileLayer({ + source: new OSM(), + properties: { title: "OSM" } + }), + isBaseLayer: true + }) + ] + }; + } +} + +export const FACET_EXTENT_MODAL_MAP_ID = "facet_extent_modal"; +export class FacetExtentModalMapProvider implements MapConfigProvider { + mapId = FACET_EXTENT_MODAL_MAP_ID; + + async getMapConfig(): Promise { + return { + initialView: { + kind: "position", + center: { x: 0, y: 0 }, + zoom: 1 + }, + projection: MAP_PROJECTION, + layers: [ + new SimpleLayer({ + title: "OpenStreetMap", + olLayer: new TileLayer({ + source: new OSM(), + properties: { title: "OSM" } + }), + isBaseLayer: true + }) + ] + }; + } +} diff --git a/src/packages/catalog/CatalogService.ts b/src/packages/catalog/CatalogService.ts index 431ece2..63996a8 100644 --- a/src/packages/catalog/CatalogService.ts +++ b/src/packages/catalog/CatalogService.ts @@ -154,6 +154,66 @@ export abstract class MultiSelectionFacet extends Facet { } } +export interface GeometryExtent { + left: number; + right: number; + top: number; + bottom: number; +} + +export abstract class GeometryExtentFacet extends Facet { + protected extent?: GeometryExtent; + + hasActiveFilter(): boolean { + return this.extent !== undefined; + } + + getExtent(): GeometryExtent | undefined { + return this.extent; + } + + setExtent(extent: GeometryExtent): void { + this.extent = extent; + } + + clearFilter(): void { + this.extent = undefined; + } + + appendSearchParams(params: URLSearchParams): void { + if (this.extent) { + params.append( + this.paramKey, + `${this.extent.left},${this.extent.top},${this.extent.right},${this.extent.bottom}` + ); + } + } + + applyOfSearchParams(params: URLSearchParams): void { + const matchedParam = params.get(this.paramKey); + if (matchedParam) { + const [left, top, right, bottom] = matchedParam.split(","); + if ( + left !== undefined && + top !== undefined && + bottom !== undefined && + right !== undefined + ) { + try { + this.extent = { + top: parseFloat(top), + bottom: parseFloat(bottom), + left: parseFloat(left), + right: parseFloat(right) + }; + } catch (error) { + console.error(error); + } + } + } + } +} + export interface SearchFilter { searchTerm?: string; pageSize?: number; diff --git a/src/packages/geonode-catalog/GeonodeCatalogServiceImpl.ts b/src/packages/geonode-catalog/GeonodeCatalogServiceImpl.ts index 652e8f3..f2f67ec 100644 --- a/src/packages/geonode-catalog/GeonodeCatalogServiceImpl.ts +++ b/src/packages/geonode-catalog/GeonodeCatalogServiceImpl.ts @@ -9,7 +9,8 @@ import { OrderOption, SearchFilter, SearchResponse, - SearchResultEntry + SearchResultEntry, + GeometryExtentFacet } from "catalog"; import type { ServiceOptions } from "@open-pioneer/runtime"; import { HttpService } from "@open-pioneer/http"; @@ -104,6 +105,17 @@ class GeonodeCatalogMultiSelectionFacet extends MultiSelectionFacet implements G } } +class GeonodeCatalogGeometryExtentFacet extends GeometryExtentFacet implements GeonodeCatalogFacet { + addFilterParameter(params: URLSearchParams): void { + if (this.extent) { + params.append( + this.key, + `${this.extent.left},${this.extent.top},${this.extent.right},${this.extent.bottom}` + ); + } + } +} + export class GeonodeCatalogServiceImpl implements CatalogService { private httpService: HttpService; @@ -209,7 +221,8 @@ export class GeonodeCatalogServiceImpl implements CatalogService { (key, filter) => this.loadFacetOptions(key, url, filter) ), new GeonodeCatalogDateFacet("date_from", "Date from", "filter{date.gte}"), - new GeonodeCatalogDateFacet("date_to", "Date to", "filter{date.lte}") + new GeonodeCatalogDateFacet("date_to", "Date to", "filter{date.lte}"), + new GeonodeCatalogGeometryExtentFacet("extent", "Extent") ]) ); }