diff --git a/packages/geoview-core/public/configs/navigator/06-basic-footer.json b/packages/geoview-core/public/configs/navigator/06-basic-footer.json index a8661a225e7..d2901384c24 100644 --- a/packages/geoview-core/public/configs/navigator/06-basic-footer.json +++ b/packages/geoview-core/public/configs/navigator/06-basic-footer.json @@ -9,6 +9,28 @@ "shaded": true, "labeled": true }, + "overlayObjects": { + "pointMarkers": { + "group1": [ + { + "id": "1", + "coordinate": [-100, 60], + "color": "blue", + "opacity": 0.5 + }, + { + "id": "2", + "coordinate": [-80, 65], + "color": "rgb(0, 226, 0)" + }, + { + "id": "3", + "coordinate": [-115, 66], + "color": "#C52022" + } + ] + } + }, "listOfGeoviewLayerConfig": [ { "geoviewLayerId": "airborne_radioactivity", diff --git a/packages/geoview-core/public/img/marker-icon36.png b/packages/geoview-core/public/img/marker-icon36.png new file mode 100644 index 00000000000..028b60c0326 Binary files /dev/null and b/packages/geoview-core/public/img/marker-icon36.png differ diff --git a/packages/geoview-core/public/templates/demos/demo-function-event.html b/packages/geoview-core/public/templates/demos/demo-function-event.html index 1ade15b3aee..85d22d6f45b 100644 --- a/packages/geoview-core/public/templates/demos/demo-function-event.html +++ b/packages/geoview-core/public/templates/demos/demo-function-event.html @@ -123,11 +123,11 @@

API Functions:

cgpv.api.maps.Map1.appBarApi.selectAppBarTab('AppbarPanelButtonGeolocator', 'geolocator')

  • - cgpv.api.maps.Map1.layer.hoverFeatureInfoLayerSet.disableHoverListener('esriFeatureLYR5/0') + cgpv.api.maps.Map1.layer.hoverFeatureInfoLayerSet.disableHoverListener('esriFeatureLYR5/0')
    cgpv.api.maps.Map1.layer.hoverFeatureInfoLayerSet.enableHoverListener('esriFeatureLYR5/0')

  • - cgpv.api.maps.Map1.layer.featureInfoLayerSet.disableClickListener('esriFeatureLYR5/0') + cgpv.api.maps.Map1.layer.featureInfoLayerSet.disableClickListener('esriFeatureLYR5/0')
    cgpv.api.maps.Map1.layer.featureInfoLayerSet.enableClickListener('esriFeatureLYR5/0')

  • @@ -140,17 +140,18 @@

    API Functions:

    cgpv.api.maps.Map1.getMapLayerOrderInfo()

  • - cgpv.api.maps.Map1.getMapLayerOrderInfo() + cgpv.api.maps.Map1.setLanguage('en')
    + cgpv.api.maps.Map1.setLanguage('fr')

  • - const basemap = await cgpv.api.maps.Map1.basemap.createCoreBasemap(basemapOptions = {basemapId: 'simple'}) + const basemap = await cgpv.api.maps.Map1.basemap.createCoreBasemap(basemapOptions = {basemapId: 'simple'})
    cgpv.api.maps.Map1.basemap.setBasemap(basemap)

  • cgpv.api.maps.Map1.stateApi.setSelectedLayersTabLayer('nonmetalmines/5')

  • - cgpv.api.maps.Map1.plugins['swiper'].activateForLayer('nonmetalmines/5') + cgpv.api.maps.Map1.plugins['swiper'].activateForLayer('nonmetalmines/5')
    cgpv.api.maps.Map1.plugins['swiper'].deActivateForLayer('nonmetalmines/5')
  • @@ -167,6 +168,16 @@

    API Functions:


  • cgpv.api.maps.Map1.reloadWithCurrentState()
  • +

  • + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.addPointMarkers('group1', markers)
    + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.removePointMarkersOrGroup('group1'); +
  • +

  • + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.zoomToPointMarkers('group1', ['1', '3']); +
  • +

  • + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.zoomToPointMarkerGroup('group1'); +
  • Events that will generate notifications:


    @@ -283,8 +295,11 @@

    Events that will generate notifications:

    cgpv.api.maps.Map1.notifications.addNotificationSuccess(`${payload.layerPath} opacity changed to ${payload.opacity}`); }); - }); - + // listen to map added to div event + cgpv.api.onMapAddedToDiv((sender, payload) => { + cgpv.api.maps[payload.mapId].notifications.addNotificationSuccess(`Map ${payload.mapId} added`); + }); + }) // Add WMS Button====================================================================================================== // find the button element by ID var addLayerButton = document.getElementById('Add-layer'); @@ -518,7 +533,7 @@

    Events that will generate notifications:

    // add an event listener when a button is clicked changeLanguageEnglishButton.addEventListener('click', async () => { - console.log(cgpv.api.maps.Map1.setLanguage('en')); + cgpv.api.maps.Map1.setLanguage('en'); }); // Enable language fr Button================================================================================================ @@ -527,7 +542,7 @@

    Events that will generate notifications:

    // add an event listener when a button is clicked changeLanguageFrenchButton.addEventListener('click', async () => { - console.log(cgpv.api.maps.Map1.setLanguage('fr')); + cgpv.api.maps.Map1.setLanguage('fr'); }); // Change basemap Button================================================================================================ @@ -607,6 +622,61 @@

    Events that will generate notifications:

    reloadMapButton.addEventListener('click', () => { cgpv.api.maps.Map1.reloadWithCurrentState(); }); + + // Add-marker Button================================================================================================ + // find the button element by ID + var addMarkerButton = document.getElementById('Add-marker'); + + const markers = [ + { + "id": "1", + "coordinate": [-100, 60], + "color": "blue", + "opacity": 0.5 + }, + { + "id": "2", + "coordinate": [-80, 65], + "color": "rgb(0, 226, 0)" + }, + { + "id": "3", + "coordinate": [-115, 66], + "color": "#C52022" + } + ] + + // add an event listener when a button is clicked + addMarkerButton.addEventListener('click', () => { + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.addPointMarkers('group1', markers); + }); + + // Remove-marker Button================================================================================================ + // find the button element by ID + var removeMarkerButton = document.getElementById('Remove-marker'); + + // add an event listener when a button is clicked + removeMarkerButton.addEventListener('click', () => { + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.removePointMarkersOrGroup('group1'); + }); + + // Zoom-to-markers Button================================================================================================ + // find the button element by ID + var zoomToMarkersButton = document.getElementById('Zoom-to-markers'); + + // add an event listener when a button is clicked + zoomToMarkersButton.addEventListener('click', () => { + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.zoomToPointMarkers('group1', ['1', '3']); + }); + + // Zoom-to-marker-group Button================================================================================================ + // find the button element by ID + var zoomToMarkerGroupButton = document.getElementById('Zoom-to-marker-group'); + + // add an event listener when a button is clicked + zoomToMarkerGroupButton.addEventListener('click', () => { + cgpv.api.maps.Map1.layer.featureHighlight.pointMarkers.zoomToPointMarkerGroup('group1'); + }); // create snippets window.addEventListener('load', () => { diff --git a/packages/geoview-core/schema.json b/packages/geoview-core/schema.json index ff23c65fc4b..d30c03bb863 100644 --- a/packages/geoview-core/schema.json +++ b/packages/geoview-core/schema.json @@ -565,7 +565,7 @@ }, "dataProjection": { "type": "string", - "description": "The projection code of the source. Used only for GeoJSON format. Default value is EPSG:4326. " + "description": "The projection code of the source. Used only for GeoJSON format. Default value is EPSG:4326." }, "featureInfo": { "$ref": "#/definitions/TypeFeatureInfoLayerConfig" @@ -1346,6 +1346,9 @@ "highlightColor": { "$ref": "#/definitions/TypeHighlightColors" }, + "overlayObjects": { + "$ref": "#/definitions/TypeOverlayObjects" + }, "extraOptions": { "type": "object", "description": "Additional options used for OpenLayers map options" @@ -1388,6 +1391,60 @@ "default": "black", "description": "Color to use for feature highlights." }, + "TypeOverlayObjects": { + "type": "object", + "properties": { + "pointMarkers": { + "$ref": "#/definitions/TypePointMarkers" + } + } + }, + "TypePointMarkers": { + "type": "object", + "patternProperties": { + "[^]*": { + "type": "array", + "items": { + "$ref": "#/definitions/TypePointMarker" + } + } + } + }, + "TypePointMarker": { + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID for point marker. Must be unique in group." + }, + "coordinate": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "number" + }, + "description": "The coordinates of the marker." + }, + "color": { + "type": "string", + "default": "green", + "description": "Marker color." + }, + "opacity": { + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "projection": { + "type": "number", + "description": "The projection code of the coordinates. Default value is 4326." + } + }, + "required": ["id", "coordinate"] + }, "TypeListOfGeoviewLayerConfig": { "description": "List of GeoView Layers in the order which they should be added to the map.", "type": "array", diff --git a/packages/geoview-core/src/api/api.ts b/packages/geoview-core/src/api/api.ts index b7e850272da..304a65cab38 100644 --- a/packages/geoview-core/src/api/api.ts +++ b/packages/geoview-core/src/api/api.ts @@ -12,6 +12,7 @@ import { MapViewer } from '@/geo/map/map-viewer'; import * as GeoUtilities from '@/geo/utils/utilities'; import { initMapDivFromFunctionCall } from '@/app'; +import EventHelper, { EventDelegateBase } from './events/event-helper'; /** * Class used to handle api calls (events, functions etc...) @@ -32,6 +33,9 @@ export class API { // utilities object utilities; + // Keep all callback delegates references + #onMapAddedToDivHandlers: MapAddedToDivDelegate[] = []; + /** * Initiate the event and projection objects */ @@ -110,9 +114,51 @@ export class API { if (mapDiv) { // Init by function call await initMapDivFromFunctionCall(mapDiv, mapConfig); + this.#emitMapAddedToDiv({ mapId: divId }); return Promise.resolve(); } return Promise.reject(new Error(`Div with id ${divId} does not exist`)); } + + /** + * Emits an event to all handlers. + * @param {MapAddedToDivEvent} event - The event to emit + * @private + */ + #emitMapAddedToDiv(event: MapAddedToDivEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onMapAddedToDivHandlers, event); + } + + /** + * Registers a map added to div event handler. + * @param {MapAddedToDivDelegate} callback - The callback to be executed whenever the event is emitted + */ + onMapAddedToDiv(callback: MapAddedToDivDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onMapAddedToDivHandlers, callback); + } + + /** + * Unregisters a map added to div event handler. + * @param {MapAddedToDivdDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offMapAddedToDiv(callback: MapAddedToDivDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onMapAddedToDivHandlers, callback); + } } + +/** + * Define a delegate for the event handler function signature + */ +type MapAddedToDivDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type MapAddedToDivEvent = { + // The added layer + mapId: string; +}; diff --git a/packages/geoview-core/src/api/config/types/config-constants.ts b/packages/geoview-core/src/api/config/types/config-constants.ts index b95c3943c9b..923f23cc158 100644 --- a/packages/geoview-core/src/api/config/types/config-constants.ts +++ b/packages/geoview-core/src/api/config/types/config-constants.ts @@ -159,6 +159,9 @@ export const CV_DEFAULT_MAP_FEATURE_CONFIG = Cast({ interaction: 'dynamic', listOfGeoviewLayerConfig: [], highlightColor: 'black', + overlayObjects: { + pointMarkers: {}, + }, viewSettings: { initialView: { zoomAndCenter: [3.5, CV_MAP_CENTER[3978]], diff --git a/packages/geoview-core/src/api/config/types/config-validation-schema.json b/packages/geoview-core/src/api/config/types/config-validation-schema.json index 8ed9e999e14..727358c76cc 100644 --- a/packages/geoview-core/src/api/config/types/config-validation-schema.json +++ b/packages/geoview-core/src/api/config/types/config-validation-schema.json @@ -73,6 +73,9 @@ "highlightColor": { "$ref": "#/definitions/TypeHighlightColors" }, + "overlayObjects": { + "$ref": "#/definitions/TypeOverlayObjects" + }, "extraOptions": { "description": "Additional options used for OpenLayers map options", "type": "object" @@ -506,6 +509,60 @@ "enum": ["black", "white", "red", "green"], "default": "black" }, + "TypeOverlayObjects": { + "type": "object", + "properties": { + "pointMarkers": { + "$ref": "#/definitions/TypePointMarkers" + } + } + }, + "TypePointMarkers": { + "type": "object", + "patternProperties": { + "[^]*": { + "type": "array", + "items": { + "$ref": "#/definitions/TypePointMarker" + } + } + } + }, + "TypePointMarker": { + "additionalProperties": false, + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID for point marker. Must be unique in group." + }, + "coordinate": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "type": "number" + }, + "description": "The coordinates of the marker." + }, + "color": { + "type": "string", + "default": "green", + "description": "Marker color." + }, + "opacity": { + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 1 + }, + "projection": { + "type": "number", + "description": "The projection code of the coordinates. Default value is 4326." + } + }, + "required": ["id", "coordinate"] + }, "TypeDisplayLanguage": { "description": "Display languages supported.", "enum": ["en", "fr"] diff --git a/packages/geoview-core/src/api/config/types/map-schema-types.ts b/packages/geoview-core/src/api/config/types/map-schema-types.ts index 2b9f42fdaff..667b1e27b91 100644 --- a/packages/geoview-core/src/api/config/types/map-schema-types.ts +++ b/packages/geoview-core/src/api/config/types/map-schema-types.ts @@ -1,4 +1,5 @@ import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config'; +import { Coordinate } from 'ol/coordinate'; import { TimeDimension } from '@/core/utils/date-mgt'; @@ -154,6 +155,8 @@ export type TypeMapConfig = { viewSettings: TypeViewSettings; /** Highlight color. */ highlightColor?: TypeHighlightColors; + /** Point markers to add to map. */ + overlayObjects?: TypeOverlayObjects; /** Additional options used for OpenLayers map options. */ extraOptions?: Record; }; @@ -230,6 +233,30 @@ export type TypeValidMapProjectionCodes = 3978 | 3857; /** Type used to define valid highlight colors. */ export type TypeHighlightColors = 'black' | 'white' | 'red' | 'green'; +/** Type used to define overlay objects. */ +// TODO: Add more overlay objects - polygons, bounding box? +export type TypeOverlayObjects = { + /** Non interactive markers */ + pointMarkers?: TypePointMarkers; +}; + +/** Type used to define point markers object. */ +type TypePointMarkers = Record; + +/** Type used to define point marker. */ +export type TypePointMarker = { + /** ID for marker, must be unique within group */ + id: string; + /** Marker coordinates, unique in group, projection code must be added if not in lon/lat */ + coordinate: Coordinate; + /** Marker color */ + color?: string; + /** Marker opacity */ + opacity?: number; + /** Projection code if coordinates are not in lon/lat */ + projectionCode?: number; +}; + // #region GEOVIEW LAYERS /** Parent class of the GeoView layers. */ diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/data-table-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/data-table-event-processor.ts index 5ab3272707a..67731d347cd 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/data-table-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/data-table-event-processor.ts @@ -70,8 +70,8 @@ export class DataTableEventProcessor extends AbstractEventProcessor { * @param {string} filter - The filter */ static addOrUpdateTableFilter(mapId: string, layerPath: string, filter: string): void { - const curSliderFilters = this.getDataTableState(mapId)?.tableFilters; - this.getDataTableState(mapId)?.setterActions.setTableFilters({ ...curSliderFilters, [layerPath]: filter }); + const curTableFilters = this.getDataTableState(mapId)?.tableFilters; + this.getDataTableState(mapId)?.setterActions.setTableFilters({ ...curTableFilters, [layerPath]: filter }); } /** @@ -88,8 +88,7 @@ export class DataTableEventProcessor extends AbstractEventProcessor { * Propagates feature info layer sets to the store. * The propagation actually happens only if it wasn't already there. Otherwise, no update is propagated. * @param {string} mapId - The map identifier of the modified result set. - * @param {string} layerPath - The layer path that has changed. - * @param {TypeFeatureInfoResultSet} resultSet - The result set associated to the map. + * @param {TypeAllFeatureInfoResultSetEntry} resultSetEntry - The result set associated to the map. */ static propagateFeatureInfoToStore(mapId: string, resultSetEntry: TypeAllFeatureInfoResultSetEntry): void { /** diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts index 6dd6a824e69..afc0a000498 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/map-event-processor.ts @@ -15,6 +15,7 @@ import { TypeValidFooterBarTabsCoreProps, TypeValidMapProjectionCodes, TypeViewSettings, + TypePointMarker, } from '@config/types/map-schema-types'; import { api } from '@/app'; import { LayerApi } from '@/geo/layer/layer'; @@ -312,6 +313,10 @@ export class MapEventProcessor extends AbstractEventProcessor { return this.getMapStateProtected(mapId).initialFilters[layerPath]; } + static getPointMarkers(mapId: string): Record { + return this.getMapStateProtected(mapId).pointMarkers; + } + static clickMarkerIconShow(mapId: string, marker: TypeClickMarker): void { // Project coords const projectedCoords = Projection.transformPoints( @@ -560,6 +565,64 @@ export class MapEventProcessor extends AbstractEventProcessor { } } + /** + * Add a point marker + * @param {string} mapId - The ID of the map. + * @param {string} group - The group to add the markers to. + * @param {TypePointMarker} pointMarkers - The point markers to add. + */ + static addPointMarkers(mapId: string, group: string, pointMarkers: TypePointMarker[]): void { + const curMarkers = this.getMapStateProtected(mapId).pointMarkers; + + // Check for existing group, and existing markers that match input IDs or coordinates + let groupMarkers = curMarkers[group]; + if (groupMarkers) { + pointMarkers.forEach((pointMarker) => { + // Replace any existing ids or markers at the same coordinates with new marker + groupMarkers = groupMarkers.filter((marker) => marker.coordinate.join() !== pointMarker.coordinate.join()); + groupMarkers = groupMarkers.filter((marker) => marker.id !== pointMarker.id); + groupMarkers.push(pointMarker); + }); + } else { + groupMarkers = pointMarkers; + } + + // Set the group markers, and update on the map + curMarkers[group] = groupMarkers; + this.getMapStateProtected(mapId).setterActions.setPointMarkers(curMarkers); + MapEventProcessor.getMapViewerLayerAPI(mapId).featureHighlight.pointMarkers.updatePointMarkers(curMarkers); + } + + /** + * Remove a point marker + * @param {string} mapId - The ID of the map. + * @param {string} group - The group to remove the markers from. + * @param {string | Coordinate} idsOrCoordinates - The IDs or coordinates of the markers to remove. + */ + static removePointMarkersOrGroup(mapId: string, group: string, idsOrCoordinates?: string[] | Coordinate[]): void { + const curMarkers = this.getMapStateProtected(mapId).pointMarkers; + + // If no IDs or coordinates are provided, remove group + if (!idsOrCoordinates) { + delete curMarkers[group]; + } else { + // Set property to check + const property = typeof idsOrCoordinates[0] === 'string' ? 'id' : 'coordinate'; + + // Filter out markers that match given ones + let groupMarkers = curMarkers[group]; + idsOrCoordinates.forEach((idOrCoordinate) => { + groupMarkers = groupMarkers.filter((marker) => marker[property] !== idOrCoordinate); + }); + + curMarkers[group] = groupMarkers; + } + + // Set the pointMarkers and update on map + this.getMapStateProtected(mapId).setterActions.setPointMarkers(curMarkers); + MapEventProcessor.getMapViewerLayerAPI(mapId).featureHighlight.pointMarkers.updatePointMarkers(curMarkers); + } + /** * Update or remove the layer highlight. * @param {string} mapId - The ID of the map. @@ -1084,6 +1147,7 @@ export class MapEventProcessor extends AbstractEventProcessor { interaction: this.getMapInteraction(mapId), listOfGeoviewLayerConfig, highlightColor: config.map.highlightColor, + overlayObjects: { pointMarkers: this.getPointMarkers(mapId) }, viewSettings, }; diff --git a/packages/geoview-core/src/api/plugin/footer-plugin.ts b/packages/geoview-core/src/api/plugin/footer-plugin.ts index 9ccc7f5db90..982270919a3 100644 --- a/packages/geoview-core/src/api/plugin/footer-plugin.ts +++ b/packages/geoview-core/src/api/plugin/footer-plugin.ts @@ -53,7 +53,7 @@ export abstract class FooterPlugin extends AbstractPlugin { // No need to log, parent class does it well already via removed() function. // Remove the footer tab - if (this.value) this.mapViewer().footerBarApi.removeTab(this.footerProps!.id); + if (this.value && this.mapViewer()?.footerBarApi) this.mapViewer().footerBarApi.removeTab(this.footerProps!.id); } /** diff --git a/packages/geoview-core/src/core/components/point-markers/point-markers.ts b/packages/geoview-core/src/core/components/point-markers/point-markers.ts new file mode 100644 index 00000000000..6e4120b662a --- /dev/null +++ b/packages/geoview-core/src/core/components/point-markers/point-markers.ts @@ -0,0 +1,188 @@ +import { Coordinate } from 'ol/coordinate'; +import Feature from 'ol/Feature'; +import Point from 'ol/geom/Point'; +import { Icon, Style } from 'ol/style'; +import { Extent } from 'ol/extent'; +import { Projection } from '@/geo/utils/projection'; +import { getExtentUnion } from '@/geo/utils/utilities'; +import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; +import { FeatureHighlight, getScriptAndAssetURL, MapViewer } from '@/app'; +import { logger } from '@/core/utils/logger'; +import { TypePointMarker } from '@/api/config/types/map-schema-types'; + +/** + * A class to handle point markers + * + * @exports + * @class PointMarkers + */ +export class PointMarkers { + /** The feature highlight class, used to access overlay layer source */ + #featureHighlight: FeatureHighlight; + + /** The map projection */ + mapProjection: string; + + /** The map ID */ + mapId: string; + + /** Array to track marker feature IDs */ + #featureIds: string[] = []; + + /** + * Initializes point marker classes + * @param {MapViewer} mapViewer - The map viewer + * @param {FeatureHighlight} featureHighlight - The feature highlight class + */ + constructor(mapViewer: MapViewer, featureHighlight: FeatureHighlight) { + this.mapProjection = mapViewer.map.getView().getProjection().getCode(); + this.mapId = mapViewer.mapId; + this.#featureHighlight = featureHighlight; + if (Object.keys(MapEventProcessor.getPointMarkers(this.mapId)).length) + this.updatePointMarkers(MapEventProcessor.getPointMarkers(this.mapId)); + } + + /** + * Update the point markers on the map. + * @param {Record} mapPointMarkers - The markers + */ + updatePointMarkers(mapPointMarkers: Record): void { + // Remove existing markers + this.#removePointMarkersFromMap(); + + // Add point markers to map + Object.keys(mapPointMarkers).forEach((markerGroup) => { + mapPointMarkers[markerGroup].forEach((point) => { + const pointStyle = new Style({ + image: new Icon({ + anchor: [0.5, 1], + src: `${getScriptAndAssetURL()}/img/marker-icon36.png`, + color: point.color || 'green', + opacity: point.opacity || 1, + scale: 0.25, + }), + }); + + const pointFeature = new Feature({ + geometry: new Point( + Projection.transformPoints([point.coordinate], `EPSG:${point.projectionCode || 4326}`, this.mapProjection)[0] + ), + }); + + // Set ID and style for feature + const featureId = `${markerGroup}-${point.id}`; + pointFeature.setId(featureId); + pointFeature.setStyle(pointStyle); + + // Add feature to source + this.#featureHighlight.highlighSource.addFeature(pointFeature); + // Add ID to array + this.#featureIds.push(featureId); + }); + }); + } + + /** + * Remove the point markers from the map. + * @private + */ + #removePointMarkersFromMap(): void { + this.#featureIds.forEach((id) => { + const feature = this.#featureHighlight.highlighSource.getFeatureById(id); + if (feature) this.#featureHighlight.highlighSource.removeFeature(feature); + }); + this.#featureIds = []; + } + + /** + * Add point markers. + * @param {string} group - The group to add the markers to. + * @param {Record[]} pointMarkers - The masrker to add. + */ + addPointMarkers(group: string, pointMarkers: TypePointMarker[]): void { + // Redirect to event processor + MapEventProcessor.addPointMarkers(this.mapId, group, pointMarkers); + } + + /** + * Remove an array of point markers or a point marker group. + * @param {string} group - The group to remove the markers from. + * @param {string[] | Coordinate[]} idsOrCoordinates - The id or coordinate of the marker to remove. + */ + removePointMarkersOrGroup(group: string, idsOrCoordinates?: string[] | Coordinate[]): void { + // Redirect to event processor + MapEventProcessor.removePointMarkersOrGroup(this.mapId, group, idsOrCoordinates); + } + + /** + * Zoom to point marker group. + * @param {string} group - The group to zoom to. + */ + zoomToPointMarkerGroup(group: string): void { + const groupMarkers = MapEventProcessor.getPointMarkers(this.mapId)[group]; + + if (groupMarkers) { + // Create list of feature IDs + const idList: string[] = groupMarkers.map((marker) => marker.id); + + // If there are IDs, zoom to them + if (idList.length) this.zoomToPointMarkers(group, idList); + else logger.logError(`Point marker group ${group} has no markers.`); + } else logger.logError(`Point marker group ${group} does not exist.`); + } + + /** + * Zoom to point markers. + * @param {string} group - The group containing the markers to zoom to. + * @param {string | Coordinate} ids - The ids of the markers to zoom to. + */ + zoomToPointMarkers(group: string, ids: string[]): void { + // Create list of feature IDs + const idList = ids.map((id) => `${group}-${id}`); + + // Get extent of point markers and zoom to it + const extent = this.getExtentFromMarkerIds(idList); + if (extent) + MapEventProcessor.zoomToExtent(this.mapId, extent).catch((error: unknown) => { + // Log + logger.logPromiseFailed('zoomToExtent in zoomToPointMarkersOrGroup in MapEventProcessor', error); + }); + else logger.logError(`Point marker group ${group} has no markers or does not exist, or point marker ids ${ids} are not correct.`); + } + + /** + * Get the extent of point markers. + * @param {string[]} ids - The ids of markers to get the extents of. + * @returns {Extent | undefined} The calculated extent or undefined. + */ + getExtentFromMarkerIds(ids: string[]): Extent | undefined { + if (ids.length) { + // Get the point coordinates and extrapolate to extent + const coordinates = ids + .map((id) => { + const feature = this.#featureHighlight.highlighSource.getFeatureById(id); + if (feature) { + const pointCoordinates = (feature?.getGeometry() as Point).getCoordinates(); + return [pointCoordinates[0], pointCoordinates[1], pointCoordinates[0], pointCoordinates[1]] as Extent; + } + return undefined; + }) + .filter((extents) => extents); + + // If only one extent, return + if (coordinates.length === 1) return coordinates[0]; + + // Find max extent of points + if (coordinates.length) { + let extent = coordinates[0] as number[]; + for (let i = 1; i < coordinates.length; i++) { + extent = getExtentUnion(extent, coordinates[i]); + } + + return extent; + } + } + + return undefined; + } +} diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts index ccbf6e9cd48..f5a3c8e847e 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/map-state.ts @@ -14,6 +14,7 @@ import { TypeMapMouseInfo } from '@/geo/map/map-viewer'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; import { TypeClickMarker } from '@/core/components/click-marker/click-marker'; import { TypeFeatureInfoEntry } from '@/geo/map/map-schema-types'; +import { TypePointMarker } from '@/api/config/types/map-schema-types'; import { TypeFeatureInfoResultSet, TypeHoverFeatureInfo } from './feature-info-state'; import { CV_MAP_CENTER } from '@/api/config/types/config-constants'; @@ -46,6 +47,7 @@ export interface IMapState { overviewMap: boolean; overviewMapHideZoom: number; pointerPosition?: TypeMapMouseInfo; + pointMarkers: Record; rotation: number; scale: TypeScaleInfo; size: [number, number]; @@ -65,6 +67,8 @@ export interface IMapState { highlightBBox: (extent: Extent, isLayerHighlight?: boolean) => void; addHighlightedFeature: (feature: TypeFeatureInfoEntry) => void; removeHighlightedFeature: (feature: TypeFeatureInfoEntry | 'all') => void; + addPointMarkers: (group: string, pointMarkers: TypePointMarker[]) => void; + removePointMarkersOrGroup: (group: string, idsOrCoordinates?: string[] | Coordinate[]) => void; reorderLayer: (layerPath: string, move: number) => void; resetBasemap: () => Promise; setLegendCollapsed: (layerPath: string, newValue?: boolean) => void; @@ -104,6 +108,7 @@ export interface IMapState { scale: TypeScaleInfo ) => void; setPointerPosition: (pointerPosition: TypeMapMouseInfo) => void; + setPointMarkers: (pointMarkers: Record) => void; setClickCoordinates: (clickCoordinates: TypeMapMouseInfo) => void; setCurrentBasemapOptions: (basemapOptions: TypeBasemapOptions) => void; setFixNorth: (ifFix: boolean) => void; @@ -147,6 +152,7 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt overviewMap: false, overviewMapHideZoom: 0, pointerPosition: undefined, + pointMarkers: {}, rotation: 0, scale: { lineWidth: '', labelGraphic: '', labelNumeric: '' } as TypeScaleInfo, size: [0, 0] as [number, number], @@ -171,6 +177,7 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt northArrow: geoviewConfig.components!.indexOf('north-arrow') > -1 || false, overviewMap: geoviewConfig.components!.indexOf('overview-map') > -1 || false, overviewMapHideZoom: geoviewConfig.overviewMap !== undefined ? geoviewConfig.overviewMap.hideOnZoom : 0, + pointMarkers: geoviewConfig.map.overlayObjects?.pointMarkers || {}, rotation: geoviewConfig.map.viewSettings.rotation || 0, zoom: geoviewConfig.map.viewSettings.initialView?.zoomAndCenter ? geoviewConfig.map.viewSettings.initialView.zoomAndCenter[0] @@ -277,6 +284,26 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt MapEventProcessor.removeHighlightedFeature(get().mapId, feature); }, + /** + * Add point markers. + * @param {string} group - The group to add the point to + * @param {TypePointMarker[]} pointMarkers - The points to add + */ + addPointMarkers: (group: string, pointMarkers: TypePointMarker[]): void => { + // Redirect to processor + return MapEventProcessor.addPointMarkers(get().mapId, group, pointMarkers); + }, + + /** + * Remove a point marker. + * @param {string} group - The group to remove the point from + * @param {string[] | Coordinate[]} idsOrCoordinates - The point to remove + */ + removePointMarkersOrGroup: (group: string, idsOrCoordinates?: string[] | Coordinate[]): void => { + // Redirect to processor + return MapEventProcessor.removePointMarkersOrGroup(get().mapId, group, idsOrCoordinates); + }, + /** * Reorders the layer. * @param {string} layerPath - The path of the layer. @@ -603,6 +630,19 @@ export function initializeMapState(set: TypeSetStore, get: TypeGetStore): IMapSt }); }, + /** + * Sets the point markers. + * @param {Record} pointMarkers - The new point markers. + */ + setPointMarkers: (pointMarkers: Record): void => { + set({ + mapState: { + ...get().mapState, + pointMarkers, + }, + }); + }, + /** * Sets map move end properties. * @param {Coordinate} centerCoordinates - The center coordinates of the map. @@ -827,6 +867,7 @@ export const useMapClickCoordinates = (): TypeMapMouseInfo | undefined => useStore(useGeoViewStore(), (state) => state.mapState.clickCoordinates); export const useMapExtent = (): Extent | undefined => useStore(useGeoViewStore(), (state) => state.mapState.mapExtent); export const useMapFixNorth = (): boolean => useStore(useGeoViewStore(), (state) => state.mapState.fixNorth); +export const useMapInitialFilters = (): Record => useStore(useGeoViewStore(), (state) => state.mapState.initialFilters); export const useMapInteraction = (): TypeInteraction => useStore(useGeoViewStore(), (state) => state.mapState.interaction); export const useMapHoverFeatureInfo = (): TypeHoverFeatureInfo => useStore(useGeoViewStore(), (state) => state.mapState.hoverFeatureInfo); export const useMapLoaded = (): boolean => useStore(useGeoViewStore(), (state) => state.mapState.mapLoaded); @@ -837,6 +878,8 @@ export const useMapOverviewMap = (): boolean => useStore(useGeoViewStore(), (sta export const useMapOverviewMapHideZoom = (): number => useStore(useGeoViewStore(), (state) => state.mapState.overviewMapHideZoom); export const useMapPointerPosition = (): TypeMapMouseInfo | undefined => useStore(useGeoViewStore(), (state) => state.mapState.pointerPosition); +export const useMapPointMarkers = (): Record => + useStore(useGeoViewStore(), (state) => state.mapState.pointMarkers); export const useMapProjection = (): TypeValidMapProjectionCodes => useStore(useGeoViewStore(), (state) => state.mapState.currentProjection); export const useMapRotation = (): number => useStore(useGeoViewStore(), (state) => state.mapState.rotation); export const useMapScale = (): TypeScaleInfo => useStore(useGeoViewStore(), (state) => state.mapState.scale); diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state.ts index a8f2ab1d278..37fdfa227a9 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state.ts @@ -254,6 +254,6 @@ export interface TypeTimeSliderValues { export const useTimeSliderLayers = (): TimeSliderLayerSet => useStore(useGeoViewStore(), (state) => state.timeSliderState.timeSliderLayers); export const useTimeSliderSelectedLayerPath = (): string => useStore(useGeoViewStore(), (state) => state.timeSliderState.selectedLayerPath); export const useTimeSliderFilters = (): Record => - useStore(useGeoViewStore(), (state) => state.timeSliderState.sliderFilters); + useStore(useGeoViewStore(), (state) => state.timeSliderState?.sliderFilters); export const useTimeSliderStoreActions = (): TimeSliderActions => useStore(useGeoViewStore(), (state) => state.timeSliderState.actions); diff --git a/packages/geoview-core/src/geo/map/feature-highlight.ts b/packages/geoview-core/src/geo/map/feature-highlight.ts index a848a26e503..9a581753dbf 100644 --- a/packages/geoview-core/src/geo/map/feature-highlight.ts +++ b/packages/geoview-core/src/geo/map/feature-highlight.ts @@ -13,6 +13,7 @@ import { TypeHighlightColors } from '@config/types/map-schema-types'; import { logger } from '@/core/utils/logger'; import { MapViewer } from '@/geo/map/map-viewer'; import { TypeFeatureInfoEntry } from './map-schema-types'; +import { PointMarkers } from '@/core/components/point-markers/point-markers'; /** ***************************************************************************************************************************** * A class to handle highlighting of features @@ -22,12 +23,15 @@ import { TypeFeatureInfoEntry } from './map-schema-types'; */ export class FeatureHighlight { /** The vector source to use for the animation features */ - #highlighSource: VectorSource = new VectorSource(); + highlighSource: VectorSource = new VectorSource(); /** The hidden layer to display animations. */ // GV It's public, to save an eslint warning, because even if it's not read in this class, it's actually important to instanciate per OpenLayer design. overlayLayer: VectorLayer; + // Used to access point markers + pointMarkers: PointMarkers; + /** The fill for the highlight */ #highlightColor = 'black'; @@ -51,7 +55,8 @@ export class FeatureHighlight { * @param {MapViewer} mapViewer a reference to the map viewer */ constructor(mapViewer: MapViewer) { - this.overlayLayer = new VectorLayer({ source: this.#highlighSource, map: mapViewer.map }); + this.overlayLayer = new VectorLayer({ source: this.highlighSource, map: mapViewer.map }); + this.pointMarkers = new PointMarkers(mapViewer, this); // if (this.#highlightColor !== undefined) // this.changeHighlightColor(MapEventProcessor.getMapHighlightColor(this.#mapId) as TypeHighlightColors); } @@ -107,7 +112,7 @@ export class FeatureHighlight { feature.setStyle(this.#highlightStyle); feature.setId(id); this.#highlightedFeatureIds.push(id); - this.#highlighSource.addFeature(feature); + this.highlighSource.addFeature(feature); } /** @@ -117,14 +122,14 @@ export class FeatureHighlight { removeHighlight(id: string): void { if (id === 'all' && this.#highlightedFeatureIds.length) { for (let i = 0; i < this.#highlightedFeatureIds.length; i++) { - this.#highlighSource.removeFeature(this.#highlighSource.getFeatureById(this.#highlightedFeatureIds[i]) as Feature); + this.highlighSource.removeFeature(this.highlighSource.getFeatureById(this.#highlightedFeatureIds[i]) as Feature); } this.#highlightedFeatureIds = []; } else if (this.#highlightedFeatureIds.length) { for (let i = this.#highlightedFeatureIds.length - 1; i >= 0; i--) { if (this.#highlightedFeatureIds[i] === id || this.#highlightedFeatureIds[i].startsWith(`${id}-`)) { - if (this.#highlighSource.getFeatureById(this.#highlightedFeatureIds[i])) - this.#highlighSource.removeFeature(this.#highlighSource.getFeatureById(this.#highlightedFeatureIds[i]) as Feature); + if (this.highlighSource.getFeatureById(this.#highlightedFeatureIds[i])) + this.highlighSource.removeFeature(this.highlighSource.getFeatureById(this.#highlightedFeatureIds[i]) as Feature); this.#highlightedFeatureIds.splice(i, 1); } } @@ -202,8 +207,8 @@ export class FeatureHighlight { * @param {boolean} isLayerHighlight - Optional if it is a layer highlight */ highlightGeolocatorBBox(extent: Extent, isLayerHighlight = false): void { - if (this.#highlighSource.getFeatureById('geoLocatorFeature')) { - this.#highlighSource.removeFeature(this.#highlighSource.getFeatureById('geoLocatorFeature') as Feature); + if (this.highlighSource.getFeatureById('geoLocatorFeature')) { + this.highlighSource.removeFeature(this.highlighSource.getFeatureById('geoLocatorFeature') as Feature); clearTimeout(this.#bboxTimeout as NodeJS.Timeout); } const bboxPoly = fromExtent(extent); @@ -211,10 +216,10 @@ export class FeatureHighlight { const style = this.#darkOutlineStyle; bboxFeature.setStyle(style); bboxFeature.setId('geoLocatorFeature'); - this.#highlighSource.addFeature(bboxFeature); + this.highlighSource.addFeature(bboxFeature); if (!isLayerHighlight) this.#bboxTimeout = setTimeout( - () => this.#highlighSource.removeFeature(this.#highlighSource.getFeatureById('geoLocatorFeature') as Feature), + () => this.highlighSource.removeFeature(this.highlighSource.getFeatureById('geoLocatorFeature') as Feature), 5000 ); } @@ -223,6 +228,6 @@ export class FeatureHighlight { * Removes bounding box highlight */ removeBBoxHighlight(): void { - this.#highlighSource.removeFeature(this.#highlighSource.getFeatureById('geoLocatorFeature') as Feature); + this.highlighSource.removeFeature(this.highlighSource.getFeatureById('geoLocatorFeature') as Feature); } } diff --git a/packages/geoview-core/src/geo/map/map-schema-types.ts b/packages/geoview-core/src/geo/map/map-schema-types.ts index 5cdc8a691e9..952abd663dd 100644 --- a/packages/geoview-core/src/geo/map/map-schema-types.ts +++ b/packages/geoview-core/src/geo/map/map-schema-types.ts @@ -10,6 +10,7 @@ import { TypeViewSettings, TypeInteraction, TypeHighlightColors, + TypeOverlayObjects, TypeValidMapProjectionCodes, TypeDisplayTheme, TypeLocalizedString, @@ -539,6 +540,8 @@ export type TypeMapConfig = { viewSettings: TypeViewSettings; //! config /** Highlight color. */ highlightColor?: TypeHighlightColors; //! config + /** Highlight color. */ + overlayObjects?: TypeOverlayObjects; //! config /** Additional options used for OpenLayers map options. */ extraOptions?: Record; }; diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index a3b1862cdd5..eb4cb0ae9a4 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -605,12 +605,18 @@ export class MapViewer { // Zoom to extent provided in config, it present if (this.mapFeaturesConfig.map.viewSettings.initialView?.extent) - await this.zoomToExtent( - Projection.transformExtent( - this.mapFeaturesConfig.map.viewSettings.initialView?.extent, - Projection.PROJECTION_NAMES.LNGLAT, - `EPSG:${this.mapFeaturesConfig.map.viewSettings.projection}` - ) + setTimeout( + // eslint-disable-next-line @typescript-eslint/no-misused-promises + () => + this.zoomToExtent( + Projection.transformExtent( + this.mapFeaturesConfig.map.viewSettings.initialView?.extent as Extent, + Projection.PROJECTION_NAMES.LNGLAT, + `EPSG:${this.mapFeaturesConfig.map.viewSettings.projection}` + ), + { padding: [0, 0, 0, 0] } + ).catch((error) => logger.logPromiseFailed('promiseMapLayers in #checkMapLayersProcessed in map-viewer', error)), + 200 ); // Zoom to extents of layers selected in config, if provided.