diff --git a/.changeset/gold-donkeys-itch.md b/.changeset/gold-donkeys-itch.md new file mode 100644 index 00000000..7b4c514a --- /dev/null +++ b/.changeset/gold-donkeys-itch.md @@ -0,0 +1,5 @@ +--- +"@open-pioneer/map": minor +--- + +Additional helpers for highlight and zoom diff --git a/.changeset/loud-numbers-worry.md b/.changeset/loud-numbers-worry.md new file mode 100644 index 00000000..5cac8049 --- /dev/null +++ b/.changeset/loud-numbers-worry.md @@ -0,0 +1,5 @@ +--- +"@open-pioneer/map-test-utils": minor +--- + +mock vector layer rendering during tests diff --git a/src/packages/map-test-utils/index.ts b/src/packages/map-test-utils/index.ts index 535f02d0..6d9ee10f 100644 --- a/src/packages/map-test-utils/index.ts +++ b/src/packages/map-test-utils/index.ts @@ -183,3 +183,14 @@ class MapConfigProviderImpl implements MapConfigProvider { return Promise.resolve(this.mapConfig); } } + +function mockVectorLayer() { + // Overwrite render so it doesn't actually do anything during tests. + // Would otherwise error because is not fully implemented in happy dom. + const div = document.createElement("div"); + VectorLayer.prototype.render = () => { + return div; + }; +} + +mockVectorLayer(); diff --git a/src/packages/map/api/MapModel.ts b/src/packages/map/api/MapModel.ts index 286e90bd..63312a6c 100644 --- a/src/packages/map/api/MapModel.ts +++ b/src/packages/map/api/MapModel.ts @@ -1,13 +1,14 @@ // SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer) // SPDX-License-Identifier: Apache-2.0 -import type { EventSource } from "@open-pioneer/core"; +import type { EventSource, Resource } from "@open-pioneer/core"; import type OlMap from "ol/Map"; import type OlBaseLayer from "ol/layer/Base"; import type { ExtentConfig } from "./MapConfig"; import type { Layer, LayerBase } from "./layers"; import type { LayerRetrievalOptions } from "./shared"; import type { Geometry } from "ol/geom"; -import type { StyleLike } from "ol/style/Style"; +import { BaseFeature } from "./BaseFeature"; +import { StyleLike } from "ol/style/Style"; /** Events emitted by the {@link MapModel}. */ export interface MapModelEvents { @@ -17,13 +18,16 @@ export interface MapModelEvents { "destroy": void; } -/** Options supported by the map model's {@link MapModel.highlightAndZoom | highlightAndZoom} method. */ +/** Options supported when creating a new {@link Highlight}. */ export interface HighlightOptions { /** * Optional styles to override the default styles. */ highlightStyle?: HighlightStyle; +} +/** Options supported by the map model's {@link MapModel.highlightAndZoom | highlightAndZoom} method. */ +export interface HighlightZoomOptions extends HighlightOptions { /** * The zoom-level used if there is no valid extend (such as for single points). */ @@ -40,12 +44,17 @@ export interface HighlightOptions { viewPadding?: MapPadding; } -export interface HighlightStyle { +/** + * Custom styles when creating a new {@link Highlight}. + */ +export type HighlightStyle = { Point?: StyleLike; LineString?: StyleLike; Polygon?: StyleLike; MultiPolygon?: StyleLike; -} + MultiPoint?: StyleLike; + MultiLineString?: StyleLike; +}; /** * Map padding, all values are pixels. @@ -59,6 +68,20 @@ export interface MapPadding { bottom?: number; } +/** + * Represents the additional graphical representations of objects. + * + * See also {@link MapModel.highlight}. + */ +export interface Highlight extends Resource { + readonly isActive: boolean; +} + +/** + * Represents a Object + */ +export type DisplayTarget = BaseFeature | Geometry; + /** * Represents a map. */ @@ -107,17 +130,30 @@ export interface MapModel extends EventSource { whenDisplayed(): Promise; /** - * Highlights the given geometries on the map. - * Centers and zooms the view on the geometries. + * Creates a highlight at the given targets. + * + * A highlight is a temporary graphic on the map that calls attention to a point or an area. + * + * Call `destroy()` on the returned highlight object to remove the highlight again. + */ + highlight(geometries: DisplayTarget[], options?: HighlightOptions): Highlight; + + /** + * Zooms to to the given targets. + */ + zoom(geometries: DisplayTarget[], options?: HighlightZoomOptions): void; + + /** + * Creates a highlight and zooms to the given targets. * - * Removes any previous highlights. + * See also {@link highlight} and {@link zoom}. */ - highlightAndZoom(geometries: Geometry[], options?: HighlightOptions): void; + highlightAndZoom(geometries: DisplayTarget[], options?: HighlightZoomOptions): Highlight; /** * Removes any existing highlights from the map. */ - removeHighlight(): void; + removeHighlights(): void; } /** Events emitted by the {@link LayerCollection}. */ diff --git a/src/packages/map/model/Highlights.test.ts b/src/packages/map/model/Highlights.test.ts index d87bfc9e..f71c1c37 100644 --- a/src/packages/map/model/Highlights.test.ts +++ b/src/packages/map/model/Highlights.test.ts @@ -7,6 +7,7 @@ import { Highlights } from "./Highlights"; import { LineString, Point, Polygon } from "ol/geom"; import View from "ol/View"; import { approximatelyEquals } from "ol/extent"; +import { BaseFeature } from "../api/BaseFeature"; let _highlights: Highlights | undefined; afterEach(() => { @@ -14,69 +15,190 @@ afterEach(() => { _highlights = undefined; }); -it("should successfully zoom and add marker for point geometries", async () => { +it("should successfully add marker for point geometries", async () => { + const { highlights } = setup(); + + const point = new Point([852011.307424, 6788511.322702]); + const highlight = highlights.addHighlight([point], {}); + expect(highlight).toBeDefined(); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); +}); + +it("should successfully add line geometries", async () => { + const { highlights } = setup(); + + const line = new LineString([ + [851890.680238, 6788133.616293], + [859419.420804, 6790407.617885] + ]); + const highlight = highlights.addHighlight([line], {}); + expect(highlight).toBeDefined(); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); +}); + +it("should successfully add polygon geometries", async () => { + const { highlights } = setup(); + + const polygon = new Polygon([ + [ + [845183.331006, 6794496.998898], + [850132.628588, 6794764.528497], + [850629.469272, 6791707.047365], + [844399.851466, 6791229.315939], + [845183.331006, 6794496.998898] + ] + ]); + const highlight = highlights.addHighlight([polygon], {}); + expect(highlight).toBeDefined(); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); +}); + +it("should successfully zoom for geometries", async () => { + const { map, highlights } = setup(); + + const point = new Point([852011.307424, 6788511.322702]); + const line = new LineString([ + [851890.680238, 6788133.616293], + [859419.420804, 6790407.617885] + ]); + const highlight = highlights.addHighlight([point], {}); + expect(highlight).toBeDefined(); + + highlights.zoomToHighlight([point], {}); + const zoomLevel = map.getView().getZoom(); + expect(zoomLevel).toBeTruthy(); + + const highlight2 = highlights.addHighlight([line], {}); + expect(highlight2).toBeDefined(); + + highlights.zoomToHighlight([line], {}); + const zoomLevel2 = map.getView().getZoom(); + expect(zoomLevel2).toBeTruthy(); + + expect(zoomLevel).not.toEqual(zoomLevel2); + + highlights.zoomToHighlight([point], {}); + const newZoomLevel = map.getView().getZoom(); + expect(newZoomLevel).toBeTruthy(); + expect(newZoomLevel).toEqual(zoomLevel); +}); + +it("should successfully zoom and add geometries", async () => { const { map, highlights } = setup(); const point = new Point([852011.307424, 6788511.322702]); const zoomLevel = map.getView().getZoom(); expect(zoomLevel).toBeTruthy(); - highlights.addHighlightOrMarkerAndZoom([point], {}); + const highlight = highlights.addHighlightAndZoom([point], {}); + expect(highlight).toBeDefined(); const newZoomLevel = map.getView().getZoom(); expect(zoomLevel).toBeTruthy(); expect(zoomLevel).not.toEqual(newZoomLevel); - const layers = map.getLayers().getArray(); - const pointLayer = layers.find((l) => l.getClassName().includes("highlight-layer")); - expect(pointLayer).toBeDefined(); + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); }); -it("should successfully zoom and highlight for line or polygon geometries", async () => { +it("should successfully zoom and add BaseFeatures", async () => { const { map, highlights } = setup(); - const line = new LineString([ - [851890.680238, 6788133.616293], - [853419.420804, 6790407.617885] - ]); + const point = new Point([852011.307424, 6788511.322702]); + const feature = { id: "test", geometry: point } as BaseFeature; const zoomLevel = map.getView().getZoom(); + expect(zoomLevel).toBeTruthy(); - highlights.addHighlightOrMarkerAndZoom([line], {}); + const highlight = highlights.addHighlightAndZoom([feature], {}); + expect(highlight).toBeDefined(); const newZoomLevel = map.getView().getZoom(); - expect(newZoomLevel).toBeTruthy(); + expect(zoomLevel).toBeTruthy(); expect(zoomLevel).not.toEqual(newZoomLevel); - const layers = map.getLayers().getArray(); - const lineLayer = layers.find((l) => l.getClassName().includes("highlight-layer")); - expect(lineLayer).toBeDefined(); + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); }); -it("should successfully remove previously added markers or highlights", async () => { +it("should successfully zoom and add only BaseFeatures with geometry", async () => { const { map, highlights } = setup(); const point = new Point([852011.307424, 6788511.322702]); - const polygon = new Polygon([ - [ - [851728.251553, 6788384.425292], - [851518.049725, 6788651.954891], - [852182.096409, 6788881.265976], - [851728.251553, 6788384.425292] - ] - ]); + const feature = { id: "test", geometry: point } as BaseFeature; + const feature2 = { id: "test2" } as BaseFeature; + const feature3 = { id: "test3", geometry: point } as BaseFeature; + + const zoomLevel = map.getView().getZoom(); + expect(zoomLevel).toBeTruthy(); + + const highlight = highlights.addHighlightAndZoom([feature, feature2, feature3], {}); + expect(highlight).toBeDefined(); + + const newZoomLevel = map.getView().getZoom(); + expect(zoomLevel).toBeTruthy(); + expect(zoomLevel).not.toEqual(newZoomLevel); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(2); +}); + +it("should successfully remove previously added highlights", async () => { + const { highlights } = setup(); + + const point = new Point([852011.307424, 6788511.322702]); + const highlight = highlights.addHighlight([point], {}); + expect(highlight).toBeDefined(); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); + + expect(source?.getFeatures()?.length).toBe(1); + + highlight?.destroy(); + expect(highlight?.isActive).toBeFalsy(); - highlights.addHighlightOrMarkerAndZoom([point], {}); - highlights.addHighlightOrMarkerAndZoom([polygon], {}); + expect(source?.getFeatures()?.length).toBe(0); +}); + +it("highlights should not be removed multiple times", async () => { + const { highlights } = setup(); + + const point = new Point([852011.307424, 6788511.322702]); + const highlight = highlights.addHighlight([point], {}); + expect(highlight).toBeDefined(); + + const source = getLayerSource(highlights); + expect(source).toBeDefined(); - const layers = map.getLayers().getArray(); - const searchResultLayers = layers.filter((l) => l.getClassName().includes("highlight-layer")); + expect(source?.getFeatures()?.length).toBe(1); - expect(searchResultLayers).toBeDefined(); - expect(searchResultLayers.length).toBe(1); + highlight?.destroy(); + expect(highlight?.isActive).toBeFalsy(); + expect(source?.getFeatures()?.length).toBe(0); + + expect(highlight?.destroy()).toBeUndefined(); }); it("should successfully remove all markers or highlights", async () => { - const { map, highlights } = setup(); + const { highlights } = setup(); const points = [ new Point([852011.307424, 6788511.322702]), @@ -84,22 +206,16 @@ it("should successfully remove all markers or highlights", async () => { new Point([851518.049725, 6788651.954891]) ]; - highlights.addHighlightOrMarkerAndZoom(points, {}); + const highlight = highlights.addHighlightAndZoom(points, {}); + expect(highlight).toBeDefined(); - const addedLayer = map - .getLayers() - .getArray() - .find((l) => l.getClassName().includes("highlight-layer")); + const source = getLayerSource(highlights); + expect(source).toBeDefined(); - expect(addedLayer).toBeDefined(); + expect(source?.getFeatures()?.length).toBe(3); highlights.clearHighlight(); - - const layerAfterRemove = map - .getLayers() - .getArray() - .find((l) => l.getClassName().includes("highlight-layer")); - expect(layerAfterRemove).toBeUndefined(); + expect(source?.getFeatures()?.length).toBe(0); }); it("should zoom the map to the extent of the geometries but not further than the defined maxZoom", async () => { @@ -110,13 +226,13 @@ it("should zoom the map to the extent of the geometries but not further than the [849081.619449, 6793197.569417] ]); - highlights.addHighlightOrMarkerAndZoom([line], {}); + highlights.addHighlightAndZoom([line], {}); const mapZoom = map.getView().getZoom(); //default maxZoom is 20 expect(mapZoom).toBeLessThanOrEqual(20); - highlights.addHighlightOrMarkerAndZoom([line], { maxZoom: 13 }); + highlights.addHighlightAndZoom([line], { maxZoom: 13 }); const mapZoom2 = map.getView().getZoom(); expect(mapZoom2).toBeLessThanOrEqual(13); @@ -127,11 +243,11 @@ it("should zoom the map to the default or configured zoom level if there is no e const point = new Point([852011.307424, 6788511.322702]); - highlights.addHighlightOrMarkerAndZoom([point], {}); + highlights.addHighlightAndZoom([point], {}); const defaultZoom = map.getView().getZoom(); expect(defaultZoom).toStrictEqual(17); - highlights.addHighlightOrMarkerAndZoom([point], { pointZoom: 12 }); + highlights.addHighlightAndZoom([point], { pointZoom: 12 }); const configuredZoom = map.getView().getZoom(); expect(configuredZoom).toStrictEqual(12); @@ -147,7 +263,7 @@ it("should zoom the map to the right extent", async () => { const expectedExtent = [ 845321.8731197501, 6789925.10914325, 851866.7936672498, 6796470.02969075 ]; - highlights.addHighlightOrMarkerAndZoom([line], {}); + highlights.addHighlightAndZoom([line], {}); const currentExtent = map.getView().calculateExtent(); expect(approximatelyEquals(expectedExtent, currentExtent, 1)).toBe(true); }); @@ -165,3 +281,9 @@ function setup() { const highlights = (_highlights = new Highlights(map)); return { map, highlights }; } + +function getLayerSource(highlights: Highlights) { + const highlightLayer = highlights.getLayer(); + if (!highlightLayer) return; + return highlightLayer.getSource(); +} diff --git a/src/packages/map/model/Highlights.ts b/src/packages/map/model/Highlights.ts index 9534d6c9..9e9623e5 100644 --- a/src/packages/map/model/Highlights.ts +++ b/src/packages/map/model/Highlights.ts @@ -5,16 +5,24 @@ import { Feature } from "ol"; import OlMap from "ol/Map"; import { Coordinate } from "ol/coordinate"; import { Extent, createEmpty, extend, getArea, getCenter } from "ol/extent"; -import { Geometry, LineString, Point, Polygon } from "ol/geom"; +import { Geometry } from "ol/geom"; import VectorLayer from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; import { Fill, Icon, Stroke, Style } from "ol/style"; import { toFunction as toStyleFunction } from "ol/style/Style"; -import { HighlightOptions, HighlightStyle } from "../api/MapModel"; +import { + DisplayTarget, + Highlight, + HighlightOptions, + HighlightStyle, + HighlightZoomOptions +} from "../api/MapModel"; import mapMarkerUrl from "../assets/images/mapMarker.png?url"; import { FeatureLike } from "ol/Feature"; -import { TOPMOST_LAYER_Z } from "./LayerCollectionImpl"; -import { Layer as OlLayer } from "ol/layer"; +import { TOPMOST_LAYER_Z } from "../api"; +import { Type } from "ol/geom/Geometry"; + +type HighlightStyleType = keyof HighlightStyle; const DEFAULT_OL_POINT_ZOOM_LEVEL = 17; const DEFAULT_OL_MAX_ZOOM_LEVEL = 20; @@ -22,43 +30,116 @@ const DEFAULT_VIEW_PADDING = { top: 50, right: 20, bottom: 10, left: 20 }; export class Highlights { private olMap: OlMap; - private currentHighlight: OlLayer | undefined; + + private olLayer: VectorLayer; + private olSource: VectorSource>; + private activeHighlights: Set; constructor(olMap: OlMap) { this.olMap = olMap; + this.olSource = new VectorSource({ + features: undefined + }); + this.olLayer = new VectorLayer({ + className: "highlight-layer", + source: this.olSource, + style: function (feature, resolution) { + return resolveStyle(feature, resolution); + } + }); + this.activeHighlights = new Set(); + this.olLayer.setZIndex(TOPMOST_LAYER_Z); + this.olMap.addLayer(this.olLayer); + } + + /** + * Getter for Hightlightlayer + * @returns Highlights.olLayer + */ + getLayer() { + return this.olLayer; } + /** + * This method removes all highlights before destroying the class + */ destroy() { this.clearHighlight(); } /** - * This method shows the position of a text search result zoomed to and marked or highlighted in the map. + * Method of filtering out objects that are not geometry or have no property geometry. */ - addHighlightOrMarkerAndZoom( - geometries: Point[] | LineString[] | Polygon[], - options: HighlightOptions - ) { - // Cleanup existing highlight - this.clearHighlight(); + #filterGeoobjects(geoObjects: DisplayTarget[]): Geometry[] { + const geometries: Geometry[] = []; + geoObjects.forEach((item) => { + if ("getType" in item) geometries.push(item); + if ("geometry" in item && item.geometry) geometries.push(item.geometry); + }); + return geometries; + } - if (!geometries || !geometries.length) { - return; + /** + * This method displays geometries or BaseFeatures with optional styling in the map + */ + addHighlight(displayTarget: DisplayTarget[], highlightOptions: HighlightOptions | undefined) { + const geometries = this.#filterGeoobjects(displayTarget); + + if (geometries.length === 0) { + return { + get isActive() { + return false; + }, + destroy() {} + }; } - this.zoomAndAddMarkers(geometries, options); + + const features = geometries.map((geometry) => { + const type = geometry.getType(); + const feature = new Feature({ + type: type, + geometry: geometry + }); + feature.setStyle(getOwnStyle(type, highlightOptions?.highlightStyle)); + return feature; + }); + + const source = this.olSource; + const highlights = this.activeHighlights; + const highlight: Highlight = { + get isActive() { + return highlights.has(highlight); + }, + destroy() { + if (!this.isActive) { + return; + } + + for (const feature of features) { + source.removeFeature(feature); + } + highlights.delete(highlight); + } + }; + + source.addFeatures(features); + this.activeHighlights.add(highlight); + return highlight; } - clearHighlight() { - if (this.currentHighlight) { - this.olMap.removeLayer(this.currentHighlight); - this.currentHighlight = undefined; + /** + * This method zoom to geometries or BaseFeatures + */ + zoomToHighlight(displayTarget: DisplayTarget[], options: HighlightZoomOptions | undefined) { + const geometries = this.#filterGeoobjects(displayTarget); + + if (geometries.length === 0) { + return; } - } - private zoomAndAddMarkers(geometries: Geometry[], options: HighlightOptions | undefined) { let extent = createEmpty(); - for (const geom of geometries) { - extent = extend(extent, geom.getExtent()); + for (const geometry of geometries) { + extent = extend(extent, geometry!.getExtent()); } const center = getCenter(extent); @@ -76,30 +157,24 @@ export class Highlights { } = options?.viewPadding ?? DEFAULT_VIEW_PADDING; const padding = [top, right, bottom, left]; zoomTo(this.olMap, extent, zoomScale, padding); + } - this.createAndAddLayer(geometries, options?.highlightStyle); + /** + * This method displays geometries or BaseFeatures with optional styling in the map and executed a zoom + */ + addHighlightAndZoom( + displayTarget: DisplayTarget[], + highlightZoomStyle: HighlightZoomOptions | undefined + ) { + const result = this.addHighlight(displayTarget, highlightZoomStyle); + this.zoomToHighlight(displayTarget, highlightZoomStyle); + return result; } - private createAndAddLayer(geometries: Geometry[], highlightStyle: HighlightStyle | undefined) { - const features = geometries.map((geometry) => { - return new Feature({ - type: geometry.getType(), - geometry: geometry - }); - }); - const layer = new VectorLayer({ - className: "highlight-layer", - source: new VectorSource({ - features: features - }), - style: function (feature, resolution) { - return resolveStyle(feature, resolution, highlightStyle); - } - }); - // Ensure layer is rendered on top of operational layers - layer.setZIndex(TOPMOST_LAYER_Z); - this.olMap.addLayer(layer); - this.currentHighlight = layer; + clearHighlight() { + for (const highlight of this.activeHighlights) { + highlight.destroy(); + } } } @@ -120,17 +195,43 @@ function zoomTo( } } -/** Returns the appropriate style from the user's highlightStyle or falls back to the default style. */ -function resolveStyle( - feature: FeatureLike, - resolution: number, - highlightStyle: HighlightStyle | undefined -) { +/** + * Returns the appropriate style from the user's highlightStyle or falls back to the default style + */ +function resolveStyle(feature: FeatureLike, resolution: number) { const type: keyof typeof defaultHighlightStyle = feature.get("type"); - const style = toStyleFunction(highlightStyle?.[type] ?? defaultHighlightStyle[type]); + const style = toStyleFunction(getDefaultStyle(type)); return style(feature, resolution); } +/** + * This method creates styling for a highlight based on the optional style information or the default style + */ +function getOwnStyle(type: Type, highlightStyle: HighlightStyle | undefined) { + if (highlightStyle && type in highlightStyle) { + const supportedType = type as HighlightStyleType; + const ownStyle = highlightStyle[supportedType]; + return ownStyle ? ownStyle : getDefaultStyle(type); + } else { + return getDefaultStyle(type); + } +} + +/** + * This returns default styling for a highlight + */ +function getDefaultStyle(type: Type) { + if (type in defaultHighlightStyle) { + const supportedType = type as HighlightStyleType; + return defaultHighlightStyle[supportedType]; + } else { + return defaultHighlightStyle.Polygon; + } +} + +/** + * Default styling for highlights + */ const defaultHighlightStyle = { "Point": new Style({ image: new Icon({ @@ -138,6 +239,12 @@ const defaultHighlightStyle = { src: mapMarkerUrl }) }), + "MultiPoint": new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: mapMarkerUrl + }) + }), "LineString": [ new Style({ stroke: new Stroke({ @@ -152,6 +259,20 @@ const defaultHighlightStyle = { }) }) ], + "MultiLineString": [ + new Style({ + stroke: new Stroke({ + color: "#fff", + width: 5 + }) + }), + new Style({ + stroke: new Stroke({ + color: "#00ffff", + width: 3 + }) + }) + ], "Polygon": [ new Style({ stroke: new Stroke({ diff --git a/src/packages/map/model/LayerCollectionImpl.test.ts b/src/packages/map/model/LayerCollectionImpl.test.ts index 0e32b7b9..f2b31c3e 100644 --- a/src/packages/map/model/LayerCollectionImpl.test.ts +++ b/src/packages/map/model/LayerCollectionImpl.test.ts @@ -80,7 +80,8 @@ it("makes the map layers accessible", async () => { const allLayers = model.layers.getAllLayers(); expect(allLayers).toEqual(layers); - expect(model.olMap.getAllLayers().length).toBe(2); + // OSM + TopPlus Open + "highlight-layer" = 3 + expect(model.olMap.getAllLayers().length).toBe(3); }); it("supports ordered retrieval of layers", async () => { diff --git a/src/packages/map/model/MapModelImpl.ts b/src/packages/map/model/MapModelImpl.ts index 811d0ca2..0dc05cbd 100644 --- a/src/packages/map/model/MapModelImpl.ts +++ b/src/packages/map/model/MapModelImpl.ts @@ -12,9 +12,16 @@ import OlMap from "ol/Map"; import { unByKey } from "ol/Observable"; import { EventsKey } from "ol/events"; import { getCenter } from "ol/extent"; -import { ExtentConfig, HighlightOptions, MapModel, MapModelEvents } from "../api"; +import { + ExtentConfig, + Highlight, + HighlightOptions, + HighlightZoomOptions, + MapModel, + MapModelEvents +} from "../api"; import { LayerCollectionImpl } from "./LayerCollectionImpl"; -import { LineString, Point, Polygon } from "ol/geom"; +import { Geometry } from "ol/geom"; import { Highlights } from "./Highlights"; import { HttpService } from "@open-pioneer/http"; @@ -127,11 +134,18 @@ export class MapModelImpl extends EventEmitter implements MapMod return this.#sharedDeps; } - highlightAndZoom(geometries: Point[] | LineString[] | Polygon[], options?: HighlightOptions) { - this.#highlights.addHighlightOrMarkerAndZoom(geometries, options ?? {}); + highlight(geometries: Geometry[], options?: HighlightOptions | undefined): Highlight { + return this.#highlights.addHighlight(geometries, options); + } + zoom(geometries: Geometry[], options?: HighlightZoomOptions | undefined): void { + this.#highlights.zoomToHighlight(geometries, options); + } + + highlightAndZoom(geometries: Geometry[], options?: HighlightZoomOptions) { + return this.#highlights.addHighlightAndZoom(geometries, options ?? {}); } - removeHighlight() { + removeHighlights() { this.#highlights.clearHighlight(); } diff --git a/src/samples/map-sample/ol-app/AppModel.ts b/src/samples/map-sample/ol-app/AppModel.ts index be4587c6..007dc807 100644 --- a/src/samples/map-sample/ol-app/AppModel.ts +++ b/src/samples/map-sample/ol-app/AppModel.ts @@ -216,7 +216,7 @@ export class AppModel implements Service { (layerSelectionSource.status === "unavailable" || layerSelectionSource.status?.kind === "unavailable") ) { - map.removeHighlight(); + map.removeHighlights(); } }); this._resources.push(eventHandler, layerSelectionSource); diff --git a/src/samples/map-sample/ol-app/ui/AppUI.tsx b/src/samples/map-sample/ol-app/ui/AppUI.tsx index f433133e..bfce1c80 100644 --- a/src/samples/map-sample/ol-app/ui/AppUI.tsx +++ b/src/samples/map-sample/ol-app/ui/AppUI.tsx @@ -83,11 +83,11 @@ export function AppUI() { if (interactionType !== currentInteractionType && newValue) { // A new interaction type was toggled on setCurrentInteractionType(interactionType); - map?.removeHighlight(); + map?.removeHighlights(); } else if (interactionType === currentInteractionType && !newValue) { // The current interaction type was toggled off setCurrentInteractionType(undefined); - map?.removeHighlight(); + map?.removeHighlights(); } } else { setCurrentToolState({ diff --git a/src/samples/map-sample/ol-app/ui/Search.tsx b/src/samples/map-sample/ol-app/ui/Search.tsx index e8433778..7ebaca05 100644 --- a/src/samples/map-sample/ol-app/ui/Search.tsx +++ b/src/samples/map-sample/ol-app/ui/Search.tsx @@ -30,7 +30,7 @@ export function SearchComponent() { function onSearchCleared() { console.debug("The user cleared the search"); - map?.removeHighlight(); + map?.removeHighlights(); } return ( diff --git a/src/samples/map-sample/ol-app/ui/Selection.tsx b/src/samples/map-sample/ol-app/ui/Selection.tsx index 64f492fb..385e4218 100644 --- a/src/samples/map-sample/ol-app/ui/Selection.tsx +++ b/src/samples/map-sample/ol-app/ui/Selection.tsx @@ -44,7 +44,7 @@ export function SelectionComponent() { return; } - map?.removeHighlight(); + map?.removeHighlights(); const geometries = results.map((result) => result.geometry); if (geometries.length > 0) { highlightAndZoom(map, geometries); @@ -75,7 +75,7 @@ export function SelectionComponent() { } function onSelectionSourceChanged(_: SelectionSourceChangedEvent) { - map?.removeHighlight(); + map?.removeHighlights(); } return ( diff --git a/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/AppUI.tsx b/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/AppUI.tsx index a40f0620..799d506d 100644 --- a/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/AppUI.tsx +++ b/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/AppUI.tsx @@ -3,9 +3,12 @@ import { Box, Button, + Checkbox, Flex, + HStack, ListItem, Stack, + StackDivider, Text, UnorderedList, VStack @@ -14,10 +17,14 @@ import { MapAnchor, MapContainer, MapModel, useMapModel } from "@open-pioneer/ma import { SectionHeading, TitledSection } from "@open-pioneer/react-utils"; import { Geometry, LineString, Point, Polygon } from "ol/geom"; import { MAP_ID } from "./MapConfigProviderImpl"; +import { Fill, Icon, Stroke, Style } from "ol/style"; +import mapMarkerUrl2 from "./mapMarker2.png?url"; +import { useRef, useState } from "react"; export function AppUI() { const { map } = useMapModel(MAP_ID); - + const highlightMap = useRef(new Map()); + const [ownStyle, setOwnStyle] = useState(false); const pointGeometries = [ new Point([852011.307424, 6788511.322702]), new Point([829800.379064, 6809086.916672]) @@ -64,6 +71,35 @@ export function AppUI() { ] ]) ]; + + function handleClick(map: MapModel | undefined, resultGeometries: Geometry[], id: string) { + if (map && !highlightMap.current.has(id)) { + if (ownStyle) { + const highlight = map.highlightAndZoom(resultGeometries, { + "highlightStyle": ownHighlightStyle + }); + if (highlight) highlightMap.current.set(id, highlight); + } else { + const highlight = map.highlightAndZoom(resultGeometries, {}); + if (highlight) highlightMap.current.set(id, highlight); + } + } + } + + function removeHighlight(id: string) { + if (highlightMap.current.has(id)) { + highlightMap.current.get(id)?.destroy(); + highlightMap.current.delete(id); + } + } + + function reset(map: MapModel | undefined) { + if (map) { + map.removeHighlights(); + highlightMap.current = new Map(); + } + } + return ( + Test Controls: + } + pt={5} + > + { + setOwnStyle(value.target.checked); + }} + > + Own Style + + - Test Controls: - - - - - + + + + + + + + + + + + + + + + + @@ -117,7 +202,7 @@ export function AppUI() { This application can be used to test adding highlight or marker, zoom to their extent, and removing highlight and marker. The highlight and zoom for point, line string and polygon geometries - can be tested. + in two different styles can be tested. @@ -136,8 +221,16 @@ export function AppUI() { different types. - Clicking on {"'Reset'"} removes the highlights or markers - from the map. + Clicking on {"'Remove'"} will remove the marker or highlight + added by the button on the left. + + + Clicking on {"'Reset All'"} removes all highlights and + markers from the map. + + + Clicking on {"'Own Style'"} activates highlighting with + customstyle. @@ -149,13 +242,79 @@ export function AppUI() { ); } -function handleClick(map: MapModel | undefined, resultGeometries: Geometry[]) { - if (map) { - map.highlightAndZoom(resultGeometries, {}); - } -} -function reset(map: MapModel | undefined) { - if (map) { - map.removeHighlight(); - } -} +const ownHighlightStyle = { + "Point": new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: mapMarkerUrl2 + }) + }), + "MultiPoint": new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: mapMarkerUrl2 + }) + }), + "LineString": [ + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 5 + }) + }), + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 3 + }) + }) + ], + "MultiLineString": [ + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 5 + }) + }), + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 3 + }) + }) + ], + "Polygon": [ + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 5 + }) + }), + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 3 + }), + fill: new Fill({ + color: "rgba(51, 171, 71,0.35)" + }) + }) + ], + "MultiPolygon": [ + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 5 + }) + }), + new Style({ + stroke: new Stroke({ + color: "#ff0000", + width: 3 + }), + fill: new Fill({ + color: "rgba(51, 171, 71,0.35)" + }) + }) + ] +}; diff --git a/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/mapMarker2.png b/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/mapMarker2.png new file mode 100644 index 00000000..d2c339cd Binary files /dev/null and b/src/samples/test-highlight-and-zoom/highlight-and-zoom-app/mapMarker2.png differ diff --git a/src/samples/test-highlight-and-zoom/index.html b/src/samples/test-highlight-and-zoom/index.html index efab8a4a..0b635a8a 100644 --- a/src/samples/test-highlight-and-zoom/index.html +++ b/src/samples/test-highlight-and-zoom/index.html @@ -3,7 +3,7 @@ - Result handler Test App + Highlight and Zoom Test App