From f58584453fc4aca31040c464e81730908f222363 Mon Sep 17 00:00:00 2001 From: Damon Ulmi Date: Wed, 22 May 2024 12:33:30 -0700 Subject: [PATCH] feat(utility): Functions added for OSDP Closes #2114 --- packages/geoview-core/public/index.html | 2 + .../templates/demos/demo-function-event.html | 315 ++++++++++++++ .../public/templates/layers/geojson.html | 2 +- .../public/templates/pygeoapi-processes.html | 4 +- .../legend-event-processor.ts | 177 +++++++- .../map-event-processor.ts | 45 +- .../time-slider-event-processor.ts | 4 - .../layers/right-panel/layer-details.tsx | 2 +- .../mouse-position/mouse-position.tsx | 6 +- .../layer-state.ts | 404 ++++++++---------- .../time-slider-state.ts | 1 - .../validation-classes/config-base-class.ts | 47 +- .../geoview-layers/raster/esri-dynamic.ts | 6 + .../layer/geoview-layers/raster/esri-image.ts | 6 + .../geo/layer/geoview-layers/raster/wms.ts | 6 + .../vector/abstract-geoview-vector.ts | 6 + .../layer/layer-sets/abstract-layer-set.ts | 22 + .../layer-sets/all-feature-info-layer-set.ts | 18 + .../layer-sets/feature-info-layer-set.ts | 18 + .../geo/layer/layer-sets/legends-layer-set.ts | 49 ++- packages/geoview-core/src/geo/layer/layer.ts | 337 +++++++++++++-- .../geoview-core/src/geo/map/map-viewer.ts | 15 + .../geoview-core/src/geo/utils/utilities.ts | 43 +- packages/geoview-time-slider/src/index.tsx | 4 - .../src/time-slider-panel.tsx | 17 +- .../geoview-time-slider/src/time-slider.tsx | 7 +- 26 files changed, 1263 insertions(+), 300 deletions(-) create mode 100644 packages/geoview-core/public/templates/demos/demo-function-event.html diff --git a/packages/geoview-core/public/index.html b/packages/geoview-core/public/index.html index 6ca172cdcb6..c15822753b3 100644 --- a/packages/geoview-core/public/index.html +++ b/packages/geoview-core/public/index.html @@ -58,10 +58,12 @@

Interactions

Demos

Specific Demos Pages + API Function and Event Demo

Other

Performance Test Outlier Layers +
diff --git a/packages/geoview-core/public/templates/demos/demo-function-event.html b/packages/geoview-core/public/templates/demos/demo-function-event.html new file mode 100644 index 00000000000..392d78854b8 --- /dev/null +++ b/packages/geoview-core/public/templates/demos/demo-function-event.html @@ -0,0 +1,315 @@ + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + + + + + + + +
+ + + + + + + +
+

API Functions and Events

+
+ + + + + + + + + + + + + +
+ Main
+

This page is used to test functions and events

+ 1. Basic Map
+
+
+ +
+

1. Basic Map

+ Top +
+ +

+    
+
+
+
+

API Functions

+ +

Events that will generate notifications:

+

onLayerAdded, onLayerRemoved, onLayerVisibilityToggled, onMapZoomEnd, onMapMoveEnd, onLayerStatusChanged for uniqueValueId/1, onLayerFilterApplied for uniqueValueId/1

+
+ + + + + + diff --git a/packages/geoview-core/public/templates/layers/geojson.html b/packages/geoview-core/public/templates/layers/geojson.html index 7a4ab0681d0..8b67e241735 100644 --- a/packages/geoview-core/public/templates/layers/geojson.html +++ b/packages/geoview-core/public/templates/layers/geojson.html @@ -214,7 +214,7 @@

1. Many GeoJSON Layers

removeGeoJSONButton.addEventListener('click', function (e) { // removing a geojson layer using the ID const layerPath = document.getElementById('new-layer-id-label').innerText; - if (layerPath) cgpv.api.maps['LYR1'].layer.removeLayersUsingPath(layerPath); + if (layerPath) cgpv.api.maps['LYR1'].layer.removeLayerUsingPath(layerPath); document.getElementById('new-layer-id-label').innerText = ''; createTableOfFilter('LYR1'); }); diff --git a/packages/geoview-core/public/templates/pygeoapi-processes.html b/packages/geoview-core/public/templates/pygeoapi-processes.html index 0d2044fd0f0..bfb774f14be 100644 --- a/packages/geoview-core/public/templates/pygeoapi-processes.html +++ b/packages/geoview-core/public/templates/pygeoapi-processes.html @@ -161,7 +161,7 @@

GeoJSON Layer

// add an event listener when a button is clicked addGeoJSONButton.addEventListener('click', async () => { const layerPath = document.getElementById('GeoMet-New-Layer-Id-Label').innerText; - if (layerPath) cgpv.api.maps['LYR5'].layer.removeLayersUsingPath(layerPath); + if (layerPath) cgpv.api.maps['LYR5'].layer.removeLayerUsingPath(layerPath); document.getElementById('GeoMet-New-Layer-Id-Label').innerText = ''; createTableOfFilter('LYR5'); @@ -210,7 +210,7 @@

GeoJSON Layer

// add an event listener when a button is clicked addGeoJSONButton.addEventListener('click', async () => { const layerPath = document.getElementById('Hydro-New-Layer-Id-Label').innerText; - if (layerPath) cgpv.api.maps['LYR5'].layer.removeLayersUsingPath(layerPath); + if (layerPath) cgpv.api.maps['LYR5'].layer.removeLayerUsingPath(layerPath); document.getElementById('Hydro-New-Layer-Id-Label').innerText = ''; createTableOfFilter('LYR5'); diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts index 2a162405308..86ad5bd008d 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts @@ -1,5 +1,5 @@ import { TypeLayerControls } from '@config/types/map-schema-types'; -// import { layerEntryIsGroupLayer } from '@config/types/type-guards'; +import _ from 'lodash'; import { TypeLegendLayer, TypeLegendLayerIcons, TypeLegendLayerItem, TypeLegendItem } from '@/core/components/layers/types'; import { CONST_LAYER_TYPES, @@ -231,7 +231,7 @@ export class LegendEventProcessor extends AbstractEventProcessor { createNewLegendEntries(layerPathNodes[0], 1, layers); // Update the legend layers with the updated array, triggering the subscribe - this.getLayerState(mapId).actions.setLegendLayers(layers); + this.getLayerState(mapId).setterActions.setLegendLayers(layers); } // #endregion @@ -240,4 +240,177 @@ export class LegendEventProcessor extends AbstractEventProcessor { // ********************************************************** // GV NEVER add a store action who does set state AND map action at a same time. // GV Review the action in store state to make sure + + /** + * Sets the highlighted layer state. + * @param {string} mapId - The ID of the map + * @param {string} layerPath - The layer path to set as the highlighted layer + */ + static setHighlightLayer(mapId: string, layerPath: string): void { + // Get highlighted layer to set active button state because there can only be one highlighted layer at a time. + const currentHighlight = this.getLayerState(mapId).highlightedLayer; + // Highlight layer and get new highlighted layer path from map event processor. + const highlightedLayerpath = MapEventProcessor.changeOrRemoveLayerHighlight(mapId, layerPath, currentHighlight); + this.getLayerState(mapId).setterActions.setHighlightLayer(highlightedLayerpath); + } + + /** + * Finds a legend layer by a layerPath. + * @param {TypeLegendLayer[]} layers - The legend layers to search. + * @param {string} layerPath - The path of the layer. + * @returns {TypeLegendLayer | undefined} + */ + static findLayerByPath(layers: TypeLegendLayer[], layerPath: string): TypeLegendLayer | undefined { + let foundLayer: TypeLegendLayer | undefined; + + layers.forEach((layer) => { + if (layerPath === layer.layerPath) { + foundLayer = layer; + } + + if (layerPath?.startsWith(layer.layerPath) && layer.children?.length > 0) { + const result: TypeLegendLayer | undefined = LegendEventProcessor.findLayerByPath(layer.children, layerPath); + if (result) { + foundLayer = result; + } + } + }); + + return foundLayer; + } + + /** + * Delete layer from legend layers. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path of the layer to change. + */ + static deleteLayerFromLegendLayers(mapId: string, layerPath: string): void { + // Get legend layers to pass to recursive function + const curLayers = this.getLayerState(mapId).legendLayers; + // Remove layer and children + LegendEventProcessor.#deleteLayersFromLegendLayersAndChildren(mapId, curLayers, layerPath); + } + + /** + * Delete layer from legend layers. + * @param {string} mapId - The ID of the map. + * @param {TypeLegendLayer[]} legendLayers - The legend layers list to remove layer from. + * @param {string} layerPath - The layer path of the layer to change. + * @private + */ + static #deleteLayersFromLegendLayersAndChildren(mapId: string, legendLayers: TypeLegendLayer[], layerPath: string): void { + // Find index of layer and remove it + const layersIndexToDelete = legendLayers.findIndex((l) => l.layerPath === layerPath); + if (layersIndexToDelete >= 0) { + legendLayers.splice(layersIndexToDelete, 1); + } else { + // Check for layer to remove in children + legendLayers.forEach((layer) => { + if (layer.children && layer.children.length > 0) { + LegendEventProcessor.#deleteLayersFromLegendLayersAndChildren(mapId, layer.children, layerPath); + } + }); + } + } + + /** + * Delete layer. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path of the layer to change. + */ + static deleteLayer(mapId: string, layerPath: string): void { + // Delete layer through layer API + MapEventProcessor.getMapViewerLayerAPI(mapId).removeLayerUsingPath(layerPath); + } + + /** + * Set visibility of an item in legend layers. + * @param {string} mapId - The ID of the map. + * @param {TypeLegendItem} item - The item to change. + * @param {boolean} visibility - The new visibility. + */ + static setItemVisibility(mapId: string, item: TypeLegendItem, visibility: boolean = true): void { + // Get current layer legends and set item visibility + const curLayers = this.getLayerState(mapId).legendLayers; + // eslint-disable-next-line no-param-reassign + item.isVisible = visibility; + + // Set updated legend layers + this.getLayerState(mapId).setterActions.setLegendLayers(curLayers); + } + + /** + * Toggle visibility of an item. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path of the layer to change. + * @param {TypeLegendItem} item - The item to change. + */ + static toggleItemVisibility(mapId: string, layerPath: string, item: TypeLegendItem): void { + MapEventProcessor.getMapViewerLayerAPI(mapId).setItemVisibility(layerPath, item, !item.isVisible); + } + + /** + * Sets the visibility of all items in the layer. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path of the layer to change. + * @param {boolean} visibility - The visibility. + */ + static setAllItemsVisibility(mapId: string, layerPath: string, visibility: boolean): void { + // Set layer to visible + MapEventProcessor.setOrToggleMapLayerVisibility(mapId, layerPath, true); + // Get legend layers and legend layer to update + const curLayers = this.getLayerState(mapId).legendLayers; + const layer = this.findLayerByPath(curLayers, layerPath); + + // Set item visibility on map and in legend layer item for each item in layer + if (layer) { + layer.items.forEach((item) => { + MapEventProcessor.getMapViewerLayerAPI(mapId).setItemVisibility(layerPath, item, visibility, false); + // eslint-disable-next-line no-param-reassign + item.isVisible = visibility; + }); + } + + // Set updated legend layers + this.getLayerState(mapId).setterActions.setLegendLayers(curLayers); + } + + /** + * Sets the opacity of the layer. + * @param {string} mapId - The ID of the map. + * @param {string} layer - The layer to set the opacity. + * @param {number} opacity - The opacity to set. + * @param {boolean} isChild - Is the layer a child layer. + * @private + */ + static #setOpacityInLayerAndChildren(mapId: string, layer: TypeLegendLayer, opacity: number, isChild = false): void { + _.set(layer, 'opacity', opacity); + MapEventProcessor.getMapViewerLayerAPI(mapId).getGeoviewLayer(layer.layerPath)?.setOpacity(opacity, layer.layerPath); + if (isChild) { + _.set(layer, 'opacityFromParent', opacity); + } + if (layer.children && layer.children.length > 0) { + layer.children.forEach((child) => { + this.#setOpacityInLayerAndChildren(mapId, child, opacity, true); + }); + } + } + + /** + * Sets the opacity of the layer. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path of the layer to change. + * @param {number} opacity - The opacity to set. + */ + static setLayerOpacity(mapId: string, layerPath: string, opacity: number): void { + const curLayers = this.getLayerState(mapId).legendLayers; + const layer = LegendEventProcessor.findLayerByPath(curLayers, layerPath); + if (layer) { + layer.opacity = opacity; + this.#setOpacityInLayerAndChildren(mapId, layer, opacity); + } + + // Set updated legend layers + this.getLayerState(mapId).setterActions.setLegendLayers(curLayers); + } } 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 207376a1c20..9b6965629c7 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 @@ -481,6 +481,29 @@ export class MapEventProcessor extends AbstractEventProcessor { } } + /** + * Update or remove the layer highlight. + * @param {string} mapId - The ID of the map. + * @param {string} layerPath - The layer path to set as the highlighted layer. + * @param {string} hilightedLayerPath - The layer path of the currently highlighted layer. + * @returns {string} The layer path of the highlighted layer. + */ + static changeOrRemoveLayerHighlight(mapId: string, layerPath: string, hilightedLayerPath: string): string { + // If layer is currently highlighted layer, remove highlight + if (hilightedLayerPath === layerPath) { + MapEventProcessor.getMapViewerLayerAPI(mapId).removeHighlightLayer(); + return ''; + } + + // Redirect to layer to highlight + MapEventProcessor.getMapViewerLayerAPI(mapId).highlightLayer(layerPath); + // Get bounds and highlight a bounding box for the layer + const bounds = MapEventProcessor.getMapViewerLayerAPI(mapId).getGeoviewLayer(layerPath)?.calculateBounds(layerPath); + if (bounds && bounds[0] !== Infinity) this.getMapStateProtected(mapId).actions.highlightBBox(bounds, true); + + return layerPath; + } + static setMapLayerHoverable(mapId: string, layerPath: string, hoverable: boolean): void { this.getMapStateProtected(mapId).setterActions.setHoverable(layerPath, hoverable); } @@ -502,6 +525,8 @@ export class MapEventProcessor extends AbstractEventProcessor { // Apply some visibility logic const curOrderedLayerInfo = this.getMapStateProtected(mapId).orderedLayerInfo; const layerVisibility = this.getMapVisibilityFromOrderedLayerInfo(mapId, layerPath); + // Determine the outcome of the new visibility based on parameters + const newVisibility = newValue !== undefined ? newValue : !layerVisibility; const layerInfos = curOrderedLayerInfo.filter((info) => info.layerPath.startsWith(layerPath)); const parentLayerPathArray = layerPath.split('/'); parentLayerPathArray.pop(); @@ -510,9 +535,6 @@ export class MapEventProcessor extends AbstractEventProcessor { layerInfos.forEach((layerInfo) => { if (layerInfo) { - // Determine the outcome of the new visibility based on parameters - const newVisibility = newValue !== undefined ? newValue : !layerVisibility; - // If the new visibility is different than before if (newVisibility !== layerVisibility) { // Go for it @@ -537,6 +559,8 @@ export class MapEventProcessor extends AbstractEventProcessor { if (!children.some((child) => child.visible === true)) this.setOrToggleMapLayerVisibility(mapId, parentLayerPath, false); } + // Emit event + this.getMapViewerLayerAPI(mapId).emitLayerVisibilityToggled({ layerPath, visibility: newVisibility }); // Redirect this.getMapStateProtected(mapId).setterActions.setOrderedLayerInfo([...curOrderedLayerInfo]); } @@ -816,5 +840,20 @@ export class MapEventProcessor extends AbstractEventProcessor { this.getMapViewer(mapId).map.getOverlayById(`${mapId}-clickmarker`)!.setPosition(position); }; + /** + * Get layer bounds for given layer path. + * @param {string} mapId - ID of map. + * @param {string} layerPath - The layer path to get bounds for. + * @return {Extent | undefined} + */ + static getLayerBounds(mapId: string, layerPath: string): Extent | undefined { + const layer = MapEventProcessor.getMapViewerLayerAPI(mapId).getGeoviewLayer(layerPath); + if (layer) { + const bounds = layer.calculateBounds(layerPath); + if (bounds) return bounds; + } + return undefined; + } + // #endregion } diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/time-slider-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/time-slider-event-processor.ts index 04839004a26..92fcf785eb7 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/time-slider-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/time-slider-event-processor.ts @@ -96,9 +96,6 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor { const temporalDimensionInfo = geoviewLayer.getTemporalDimension(layerConfig.layerPath); if (!temporalDimensionInfo || !temporalDimensionInfo.range) return undefined; - // Get layer name and temporal dimension - const name = getLocalizedValue(layerConfig.layerName, AppEventProcessor.getDisplayLanguage(mapId)) || layerConfig.layerId; - // Set defaults values from temporal dimension const { range } = temporalDimensionInfo.range; const defaultValueIsArray = Array.isArray(temporalDimensionInfo.default); @@ -128,7 +125,6 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor { : [...minAndMax]; return { - name, range, defaultValue, discreteValues: nearestValues === 'discrete', diff --git a/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx b/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx index 02c0b89b8b5..aad18450e81 100644 --- a/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx +++ b/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx @@ -121,7 +121,7 @@ export function LayerDetails(props: LayerDetailsProps): JSX.Element { } return ( - toggleItemVisibility(layerDetails.layerPath, item.geometryType, item.name)}> + toggleItemVisibility(layerDetails.layerPath, item)}> {item.isVisible === true ? : } ); diff --git a/packages/geoview-core/src/core/components/mouse-position/mouse-position.tsx b/packages/geoview-core/src/core/components/mouse-position/mouse-position.tsx index 22aaff61faa..a23d168f80c 100644 --- a/packages/geoview-core/src/core/components/mouse-position/mouse-position.tsx +++ b/packages/geoview-core/src/core/components/mouse-position/mouse-position.tsx @@ -5,7 +5,7 @@ import { useTheme } from '@mui/material/styles'; import { Box, Button, CheckIcon } from '@/ui'; import { useUIMapInfoExpanded } from '@/core/stores/store-interface-and-intial-values/ui-state'; import { useMapPointerPosition } from '@/core/stores/store-interface-and-intial-values/map-state'; -import { coordFormnatDMS } from '@/geo/utils/utilities'; +import { coordFormatDMS } from '@/geo/utils/utilities'; import { getSxClasses } from './mouse-position-style'; @@ -51,8 +51,8 @@ export function MousePosition(): JSX.Element { const labelX = lnglat[0] < 0 ? t('mapctrl.mouseposition.west') : t('mapctrl.mouseposition.east'); const labelY = lnglat[1] < 0 ? t('mapctrl.mouseposition.south') : t('mapctrl.mouseposition.north'); - const lng = `${DMS ? coordFormnatDMS(lnglat[0]) : Math.abs(lnglat[0]).toFixed(4)} ${labelX}`; - const lat = `${DMS ? coordFormnatDMS(lnglat[1]) : Math.abs(lnglat[1]).toFixed(4)} ${labelY}`; + const lng = `${DMS ? coordFormatDMS(lnglat[0]) : Math.abs(lnglat[0]).toFixed(4)} ${labelX}`; + const lat = `${DMS ? coordFormatDMS(lnglat[1]) : Math.abs(lnglat[1]).toFixed(4)} ${labelY}`; return { lng, lat }; } diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts index f7b33f2c22e..69e3225d0a4 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts @@ -1,21 +1,23 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ // this esLint is used in many places for findLayerByPath function. It is why we keep it global... import { useStore } from 'zustand'; -import _ from 'lodash'; import { FitOptions } from 'ol/View'; +import { Extent } from 'ol/extent'; import { useGeoViewStore } from '@/core/stores/stores-managers'; -import { TypeLayersViewDisplayState, TypeLegendLayer } from '@/core/components/layers/types'; +import { TypeLayersViewDisplayState, TypeLegendItem, TypeLegendLayer } from '@/core/components/layers/types'; import { TypeGetStore, TypeSetStore } from '@/core/stores/geoview-store'; -import { TypeClassBreakStyleConfig, TypeStyleGeometry, TypeUniqueValueStyleConfig } from '@/geo/map/map-schema-types'; -import { AbstractGeoViewVector } from '@/geo/layer/geoview-layers/vector/abstract-geoview-vector'; import { OL_ZOOM_DURATION, OL_ZOOM_PADDING } from '@/core/utils/constant'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; -import { VectorLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-layer-entry-config'; +import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; + +// GV Important: See notes in header of MapEventProcessor file for information on the paradigm to apply when working with AppEventProcessor vs AppState // #region INTERFACES & TYPES +type LayerActions = ILayerState['actions']; + export interface ILayerState { highlightedLayer: string; selectedLayer: TypeLegendLayer; @@ -25,292 +27,230 @@ export interface ILayerState { layerDeleteInProgress: boolean; actions: { - setLegendLayers: (legendLayers: TypeLegendLayer[]) => void; + deleteLayer: (layerPath: string) => void; getLayer: (layerPath: string) => TypeLegendLayer | undefined; getLayerBounds: (layerPath: string) => number[] | undefined; + getLayerDeleteInProgress: () => boolean; + setAllItemsVisibility: (layerPath: string, visibility: boolean) => void; setDisplayState: (newDisplayState: TypeLayersViewDisplayState) => void; setHighlightLayer: (layerPath: string) => void; + setLayerDeleteInProgress: (newVal: boolean) => void; setLayerOpacity: (layerPath: string, opacity: number) => void; setSelectedLayerPath: (layerPath: string) => void; - toggleItemVisibility: (layerPath: string, geometryType: TypeStyleGeometry, itemName: string) => void; - setAllItemsVisibility: (layerPath: string, visibility: boolean) => void; - setLayerDeleteInProgress: (newVal: boolean) => void; - getLayerDeleteInProgress: () => boolean; - deleteLayer: (layerPath: string) => void; + toggleItemVisibility: (layerPath: string, item: TypeLegendItem) => void; zoomToLayerExtent: (layerPath: string) => Promise; }; -} -// #endregion INTERFACES & TYPES + setterActions: { + setDisplayState: (newDisplayState: TypeLayersViewDisplayState) => void; + setHighlightLayer: (layerPath: string) => void; + setLayerDeleteInProgress: (newVal: boolean) => void; + setLegendLayers: (legendLayers: TypeLegendLayer[]) => void; + setSelectedLayerPath: (layerPath: string) => void; + }; +} +/** + * Initializes a Layer State and provide functions which use the get/set Zustand mechanisms. + * @param {TypeSetStore} set - The setter callback to be used by this state + * @param {TypeGetStore} get - The getter callback to be used by this state + * @returns The initialized Layer State + */ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILayerState { - const init = { + return { highlightedLayer: '', legendLayers: [] as TypeLegendLayer[], selectedLayerPath: null, displayState: 'view', layerDeleteInProgress: false, + // #region ACTIONS actions: { - setLegendLayers: (legendLayers: TypeLegendLayer[]): void => { - set({ - layerState: { - ...get().layerState, - legendLayers: [...legendLayers], - }, - }); + /** + * Deletes a layer. + * @param {string} layerPath - The path of the layer to delete. + */ + deleteLayer: (layerPath: string): void => { + LegendEventProcessor.deleteLayer(get().mapId, layerPath); + get().layerState.setterActions.setLayerDeleteInProgress(false); }, - getLayer: (layerPath: string) => { + + /** + * Get legend layer for given layer path. + * @param {string} layerPath - The layer path to get info for. + * @return {TypeLegendLayer | undefined} + */ + getLayer: (layerPath: string): TypeLegendLayer | undefined => { const curLayers = get().layerState.legendLayers; - const layer = findLayerByPath(curLayers, layerPath); - return layer; + return LegendEventProcessor.findLayerByPath(curLayers, layerPath); }, - getLayerBounds: (layerPath: string) => { - const layer = MapEventProcessor.getMapViewerLayerAPI(get().mapId).getGeoviewLayer(layerPath); - if (layer) { - const bounds = layer.calculateBounds(layerPath); - if (bounds) return bounds; - } - return undefined; + + /** + * Get layer bounds for given layer path. + * @param {string} layerPath - The layer path to get bounds for. + * @return {Extent | undefined} + */ + getLayerBounds: (layerPath: string): Extent | undefined => { + // Redirect to map event processor. + return MapEventProcessor.getLayerBounds(get().mapId, layerPath); }, - setDisplayState: (newDisplayState: TypeLayersViewDisplayState) => { - const curState = get().layerState.displayState; - set({ - layerState: { - ...get().layerState, - displayState: curState === newDisplayState ? 'view' : newDisplayState, - }, - }); + + /** + * Get the LayerDeleteInProgress state. + */ + getLayerDeleteInProgress: () => get().layerState.layerDeleteInProgress, + + /** + * Sets the visibility of all items in the layer. + * @param {string} layerPath - The layer path of the layer to change. + * @param {boolean} visibility - The visibility. + */ + setAllItemsVisibility: (layerPath: string, visibility: boolean): void => { + // Redirect to processor. + LegendEventProcessor.setAllItemsVisibility(get().mapId, layerPath, visibility); }, - setHighlightLayer: (layerPath: string) => { - // keep track of highlighted layer to set active button state because there can only be one highlighted layer at a time - const currentHighlight = get().layerState.highlightedLayer; - let tempLayerPath = layerPath; - // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion - if (currentHighlight === tempLayerPath) { - MapEventProcessor.getMapViewerLayerAPI(get().mapId).removeHighlightLayer(); - tempLayerPath = ''; - } else { - MapEventProcessor.getMapViewerLayerAPI(get().mapId).highlightLayer(tempLayerPath); - const layer = findLayerByPath(get().layerState.legendLayers, layerPath); - const { bounds } = layer as TypeLegendLayer; - if (bounds && bounds[0] !== Infinity) get().mapState.actions.highlightBBox(bounds, true); - } - set({ - layerState: { - ...get().layerState, - highlightedLayer: tempLayerPath, - }, - }); + /** + * Sets the display state. + * @param {TypeLayersViewDisplayState} newDisplayState - The display state to set. + */ + setDisplayState: (newDisplayState: TypeLayersViewDisplayState): void => { + // Redirect to setter + get().layerState.setterActions.setDisplayState(newDisplayState); }, - setSelectedLayerPath: (layerPath: string) => { - const curLayers = get().layerState.legendLayers; - const layer = findLayerByPath(curLayers, layerPath); - set({ - layerState: { - ...get().layerState, - selectedLayerPath: layerPath, - selectedLayer: layer as TypeLegendLayer, - }, - }); + + /** + * Sets the highlighted layer state. + * @param {string} layerPath - The layer path to set as the highlighted layer. + */ + setHighlightLayer: (layerPath: string): void => { + // Redirect to event processor + LegendEventProcessor.setHighlightLayer(get().mapId, layerPath); }, - setLayerOpacity: (layerPath: string, opacity: number) => { - const curLayers = get().layerState.legendLayers; - const layer = findLayerByPath(curLayers, layerPath); - if (layer) { - layer.opacity = opacity; - const { mapId } = get(); - setOpacityInLayerAndChildren(layer, opacity, mapId); - } + /** + * Sets the layer delete in progress state. + * @param {boolean} newVal - The new value. + */ + setLayerDeleteInProgress: (newVal: boolean): void => { + // Redirect to setter + get().layerState.setterActions.setLayerDeleteInProgress(newVal); + }, - // now update store + /** + * Sets the opacity of the layer. + * @param {string} layerPath - The layer path of the layer to change. + * @param {number} opacity - The opacity to set. + */ + setLayerOpacity: (layerPath: string, opacity: number): void => { + // Redirect to event processor + LegendEventProcessor.setLayerOpacity(get().mapId, layerPath, opacity); + }, + + /** + * Sets the selected layer path. + * @param {string} layerPath - The layer path to set as selected. + */ + setSelectedLayerPath: (layerPath: string): void => { + // Redirect to setter + get().layerState.setterActions.setSelectedLayerPath(layerPath); + }, + + /** + * Toggle visibility of an item. + * @param {string} layerPath - The layer path of the layer to change. + * @param {TypeLegendItem} item - The name of the item to change. + */ + toggleItemVisibility: (layerPath: string, item: TypeLegendItem): void => { + // Redirect to processor. + LegendEventProcessor.toggleItemVisibility(get().mapId, layerPath, item); + }, + + /** + * Zoom to extents of a layer. + * @param {string} layerPath - The path of the layer to zoom to. + */ + zoomToLayerExtent: (layerPath: string): Promise => { + const options: FitOptions = { padding: OL_ZOOM_PADDING, duration: OL_ZOOM_DURATION }; + const layer = LegendEventProcessor.findLayerByPath(get().layerState.legendLayers, layerPath); + const { bounds } = layer as TypeLegendLayer; + if (bounds) return MapEventProcessor.zoomToExtent(get().mapId, bounds, options); + return Promise.resolve(); + }, + }, + + setterActions: { + /** + * Sets the display state. + * @param {TypeLayersViewDisplayState} newDisplayState - The display state to set. + */ + setDisplayState: (newDisplayState: TypeLayersViewDisplayState): void => { + const curState = get().layerState.displayState; set({ layerState: { ...get().layerState, - legendLayers: [...curLayers], + displayState: curState === newDisplayState ? 'view' : newDisplayState, }, }); }, - toggleItemVisibility: (layerPath: string, geometryType: TypeStyleGeometry, itemName: string) => { - const curLayers = get().layerState.legendLayers; - - const registeredLayer = MapEventProcessor.getMapViewerLayerAPI(get().mapId).registeredLayers[layerPath] as VectorLayerEntryConfig; - const layer = findLayerByPath(curLayers, layerPath); - if (layer) { - _.each(layer.items, (item) => { - if (item.geometryType === geometryType && item.name === itemName) { - item.isVisible = !item.isVisible; // eslint-disable-line no-param-reassign - if (item.isVisible && MapEventProcessor.getMapVisibilityFromOrderedLayerInfo(get().mapId, layerPath)) { - MapEventProcessor.setOrToggleMapLayerVisibility(get().mapId, layerPath, true); - } - - // assign value to registered layer. This is use by applyFilter function to set visibility - // TODO: check if we need to refactor to centralize attribute setting.... - // TODO: know issue when we toggle a default visibility item https://github.com/Canadian-Geospatial-Platform/geoview/issues/1564 - if (registeredLayer.style![geometryType]?.styleType === 'classBreaks') { - const geometryStyleConfig = registeredLayer.style![geometryType]! as TypeClassBreakStyleConfig; - const classBreakStyleInfo = geometryStyleConfig.classBreakStyleInfo.find((styleInfo) => styleInfo.label === itemName); - if (classBreakStyleInfo) classBreakStyleInfo.visible = item.isVisible; - else geometryStyleConfig.defaultVisible = item.isVisible; - } else if (registeredLayer.style![geometryType]?.styleType === 'uniqueValue') { - const geometryStyleConfig = registeredLayer.style![geometryType]! as TypeUniqueValueStyleConfig; - const uniqueStyleInfo = geometryStyleConfig.uniqueValueStyleInfo.find((styleInfo) => styleInfo.label === itemName); - if (uniqueStyleInfo) uniqueStyleInfo.visible = item.isVisible; - else geometryStyleConfig.defaultVisible = item.isVisible; - } - } - }); - - // apply filter to layer - (MapEventProcessor.getMapViewerLayerAPI(get().mapId).getGeoviewLayer(layerPath) as AbstractGeoViewVector).applyViewFilter( - layerPath, - '' - ); - } + /** + * Sets the highlighted layer state. + * @param {string} layerPath - The layer path to set as the highlighted layer. + */ + setHighlightLayer: (layerPath: string): void => { set({ layerState: { ...get().layerState, - legendLayers: [...curLayers], + highlightedLayer: layerPath, }, }); }, - setAllItemsVisibility: (layerPath: string, visibility: boolean) => { - MapEventProcessor.setOrToggleMapLayerVisibility(get().mapId, layerPath, true); - const curLayers = get().layerState.legendLayers; - - const registeredLayer = MapEventProcessor.getMapViewerLayerAPI(get().mapId).registeredLayers[layerPath] as VectorLayerEntryConfig; - const layer = findLayerByPath(curLayers, layerPath); - if (layer) { - _.each(layer.items, (item) => { - // eslint-disable-next-line no-param-reassign - item.isVisible = visibility; - }); - // assign value to registered layer. This is use by applyFilter function to set visibility - // TODO: check if we need to refactor to centralize attribute setting.... - if (registeredLayer.style) { - ['Point', 'LineString', 'Polygon'].forEach((geometry) => { - if (registeredLayer.style![geometry as TypeStyleGeometry]) { - if (registeredLayer.style![geometry as TypeStyleGeometry]?.styleType === 'classBreaks') { - const geometryStyleConfig = registeredLayer.style![geometry as TypeStyleGeometry]! as TypeClassBreakStyleConfig; - if (geometryStyleConfig.defaultVisible !== undefined) geometryStyleConfig.defaultVisible = visibility; - geometryStyleConfig.classBreakStyleInfo.forEach((styleInfo) => { - // eslint-disable-next-line no-param-reassign - styleInfo.visible = visibility; - }); - } else if (registeredLayer.style![geometry as TypeStyleGeometry]?.styleType === 'uniqueValue') { - const geometryStyleConfig = registeredLayer.style![geometry as TypeStyleGeometry]! as TypeUniqueValueStyleConfig; - if (geometryStyleConfig.defaultVisible !== undefined) geometryStyleConfig.defaultVisible = visibility; - geometryStyleConfig.uniqueValueStyleInfo.forEach((styleInfo) => { - // eslint-disable-next-line no-param-reassign - styleInfo.visible = visibility; - }); - } - } - }); - } - } + /** + * Sets the layer delete in progress state. + * @param {boolean} newVal - The new value. + */ + setLayerDeleteInProgress: (newVal: boolean): void => { set({ layerState: { ...get().layerState, - legendLayers: [...curLayers], + layerDeleteInProgress: newVal, }, }); - - // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion - // GV try to make reusable store actions.... - // GV we can have always item.... we cannot set visibility so if present we will need to trap. Need more use case - // GV create a function setItemVisibility called with layer path and this function set the registered layer (from store values) then apply the filter. - (MapEventProcessor.getMapViewerLayerAPI(get().mapId).getGeoviewLayer(layerPath) as AbstractGeoViewVector).applyViewFilter( - layerPath, - '' - ); }, - getLayerDeleteInProgress: () => get().layerState.layerDeleteInProgress, - setLayerDeleteInProgress: (newVal: boolean) => { + + /** + * Sets the legend layers state. + * @param {TypeLegendLayer} legendLayers - The legend layers to set. + */ + setLegendLayers: (legendLayers: TypeLegendLayer[]): void => { set({ layerState: { ...get().layerState, - layerDeleteInProgress: newVal, + legendLayers: [...legendLayers], }, }); }, - deleteLayer: (layerPath: string) => { + + /** + * Sets the selected layer path. + * @param {string} layerPath - The layer path to set as selected. + */ + setSelectedLayerPath: (layerPath: string): void => { const curLayers = get().layerState.legendLayers; - deleteSingleLayer(curLayers, layerPath); + const layer = LegendEventProcessor.findLayerByPath(curLayers, layerPath); set({ layerState: { ...get().layerState, - layerDeleteInProgress: false, - legendLayers: [...curLayers], + selectedLayerPath: layerPath, + selectedLayer: layer as TypeLegendLayer, }, }); - - // TODO: keep reference to geoview map instance in the store or keep accessing with api - discussion - MapEventProcessor.getMapViewerLayerAPI(get().mapId).removeLayersUsingPath(layerPath); - }, - zoomToLayerExtent: (layerPath: string): Promise => { - const options: FitOptions = { padding: OL_ZOOM_PADDING, duration: OL_ZOOM_DURATION }; - const layer = findLayerByPath(get().layerState.legendLayers, layerPath); - const { bounds } = layer as TypeLegendLayer; - if (bounds) return MapEventProcessor.zoomToExtent(get().mapId, bounds, options); - return Promise.resolve(); }, }, + // #endregion ACTIONS } as ILayerState; - - return init; -} - -// private functions - -function setOpacityInLayerAndChildren(layer: TypeLegendLayer, opacity: number, mapId: string, isChild = false): void { - _.set(layer, 'opacity', opacity); - MapEventProcessor.getMapViewerLayerAPI(mapId).getGeoviewLayer(layer.layerPath)?.setOpacity(opacity, layer.layerPath); - if (isChild) { - _.set(layer, 'opacityFromParent', opacity); - } - if (layer.children && layer.children.length > 0) { - _.each(layer.children, (child) => { - setOpacityInLayerAndChildren(child, opacity, mapId, true); - }); - } -} - -function findLayerByPath(layers: TypeLegendLayer[], layerPath: string): TypeLegendLayer | undefined { - // TODO: refactor - iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations - // eslint-disable-next-line no-restricted-syntax - for (const l of layers) { - if (layerPath === l.layerPath) { - return l; - } - if (layerPath?.startsWith(l.layerPath) && l.children?.length > 0) { - const result: TypeLegendLayer | undefined = findLayerByPath(l.children, layerPath); - if (result) { - return result; - } - } - } - - return undefined; -} - -function deleteSingleLayer(layers: TypeLegendLayer[], layerPath: string): void { - const indexToDelete = layers.findIndex((l) => l.layerPath === layerPath); - if (indexToDelete >= 0) { - layers.splice(indexToDelete, 1); - } else { - // TODO: refactor - iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations - // eslint-disable-next-line no-restricted-syntax - for (const l of layers) { - if (l.children && l.children.length > 0) { - deleteSingleLayer(l.children, layerPath); - } - } - } } // ********************************************************** @@ -323,23 +263,21 @@ export const useLayerSelectedLayerPath = (): string | null | undefined => useStore(useGeoViewStore(), (state) => state.layerState.selectedLayerPath); export const useLayerDisplayState = (): TypeLayersViewDisplayState => useStore(useGeoViewStore(), (state) => state.layerState.displayState); -// TODO: Refactor - We should explicit a type for the layerState.actions -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const useLayerStoreActions = (): any => useStore(useGeoViewStore(), (state) => state.layerState.actions); +export const useLayerStoreActions = (): LayerActions => useStore(useGeoViewStore(), (state) => state.layerState.actions); // computed gets export const useSelectedLayer = (): TypeLegendLayer | undefined => { const layers = useStore(useGeoViewStore(), (state) => state.layerState.legendLayers); const selectedLayerPath = useStore(useGeoViewStore(), (state) => state.layerState.selectedLayerPath); if (selectedLayerPath) { - return findLayerByPath(layers, selectedLayerPath); + return LegendEventProcessor.findLayerByPath(layers, selectedLayerPath); } return undefined; }; export const useIconLayerSet = (layerPath: string): string[] => { const layers = useStore(useGeoViewStore(), (state) => state.layerState.legendLayers); - const layer = findLayerByPath(layers, layerPath); + const layer = LegendEventProcessor.findLayerByPath(layers, layerPath); if (layer) { return layer.items.map((item) => item.icon).filter((d) => d !== null) as string[]; } 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 b7f6244879e..0e997056ded 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 @@ -23,7 +23,6 @@ export interface TypeTimeSliderValues { filtering: boolean; locked?: boolean; minAndMax: number[]; - name: string; range: string[]; reversed?: boolean; singleHandle: boolean; diff --git a/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts b/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts index 79639c307be..9e220226b93 100644 --- a/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts +++ b/packages/geoview-core/src/core/utils/config/validation-classes/config-base-class.ts @@ -75,6 +75,8 @@ export class ConfigBaseClass { // TD.CONT: the event is not define so this.onLayerStatus.... failed #onLayerStatusChangedHandlers: LayerStatusChangedDelegate[] = []; + #onLayerFilterAppliedHandlers: LayerFilterAppliedDelegate[] = []; + // TODO: Review - The status. I think we should have: newInstance, processsing, loading, - loaded : error static #layerStatusWeight = { newInstance: 10, @@ -221,7 +223,7 @@ export class ConfigBaseClass { * @param {LayerStatusChangedEvent} event The event to emit * @private */ - // TODO: refactor - if this emit is privare with #, abstract-base-layer-entry-config.ts:28 Uncaught (in promise) TypeError: Private element is not present on this object + // TODO: refactor - if this emit is private with #, abstract-base-layer-entry-config.ts:28 Uncaught (in promise) TypeError: Private element is not present on this object // TO.DOCONT: this by pass the error, I need to set this public. The problem come from the groupLayer object trying to emit this event but // TO.DOCONT: the event is not define so this.onLayerStatus.... failed #emitLayerStatusChanged(event: LayerStatusChangedEvent): void { @@ -247,6 +249,34 @@ export class ConfigBaseClass { EventHelper.offEvent(this.#onLayerStatusChangedHandlers, callback); } + /** + * Emits filter applied event. + * @param {FilterAppliedEvent} event - The event to emit + * @private + */ + emitLayerFilterApplied(event: LayerFilterAppliedEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onLayerFilterAppliedHandlers, event); + } + + /** + * Registers a filter applied event handler. + * @param {FilterAppliedDelegate} callback - The callback to be executed whenever the event is emitted + */ + onLayerFilterApplied(callback: LayerFilterAppliedDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onLayerFilterAppliedHandlers, callback); + } + + /** + * Unregisters a filter applied event handler. + * @param {FilterAppliedDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offLayerFilterApplied(callback: LayerFilterAppliedDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onLayerFilterAppliedHandlers, callback); + } + /** * Register the layer identifier. Duplicate identifier are not allowed. * @@ -320,3 +350,18 @@ export type LayerStatusChangedEvent = { // The new layer status to assign to the layer path. layerStatus: TypeLayerStatus; }; + +/** + * Define a delegate for the event handler function signature + */ +type LayerFilterAppliedDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type LayerFilterAppliedEvent = { + // The layer path of the affected layer + layerPath: string; + // The filter + filter: string; +}; diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-dynamic.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-dynamic.ts index 91d25bae20d..e4c1bff3f48 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-dynamic.ts @@ -818,6 +818,12 @@ export class EsriDynamic extends AbstractGeoViewRaster { .getSource()! .updateParams({ layerDefs: `{"${layerConfig.layerId}": "${filterValueToUse}"}` }); layerConfig.olLayer!.changed(); + + // Emit event + MapEventProcessor.getMapViewerLayerAPI(this.mapId).registeredLayers[layerPath].emitLayerFilterApplied({ + layerPath, + filter: filterValueToUse, + }); } /** *************************************************************************************************************************** diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-image.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-image.ts index df6a2ce3c71..a2ce3646e0a 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-image.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/esri-image.ts @@ -381,6 +381,12 @@ export class EsriImage extends AbstractGeoViewRaster { }); source.updateParams({ [dimension]: filterValueToUse.replace(/\s*/g, '') }); layerConfig.olLayer!.changed(); + + // Emit event + MapEventProcessor.getMapViewerLayerAPI(this.mapId).registeredLayers[layerPath].emitLayerFilterApplied({ + layerPath, + filter: filterValueToUse, + }); } } } diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts b/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts index e1ce198c611..b9c18b32097 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/raster/wms.ts @@ -1079,6 +1079,12 @@ export class WMS extends AbstractGeoViewRaster { }); source.updateParams({ [dimension]: filterValueToUse.replace(/\s*/g, '') }); layerConfig.olLayer!.changed(); + + // Emit event + MapEventProcessor.getMapViewerLayerAPI(this.mapId).registeredLayers[layerPath].emitLayerFilterApplied({ + layerPath, + filter: filterValueToUse, + }); } } } diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts index e84ffd4f49e..41ef7005fef 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts @@ -399,5 +399,11 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { } layerConfig.olLayer?.changed(); + + // Emit event + MapEventProcessor.getMapViewerLayerAPI(this.mapId).registeredLayers[layerPath].emitLayerFilterApplied({ + layerPath, + filter: filterValueToUse, + }); } } diff --git a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts index 51cca68da0b..6f25126a331 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts @@ -96,6 +96,28 @@ export abstract class AbstractLayerSet { } } + /** + * Processes the layer name change in the layer-set. + * @param {string} name - The new layer name + * @param {string} layerPath - The layer path being affected + */ + public processNameChanged(name: string, layerPath: string): void { + // Call the overridable function to process a layer name change + this.onProcessNameChanged(name, layerPath); + } + + /** + * An overridable function for a layer-set to process a layer name change. + * @param {string} name - The new layer name + * @param {string} layerPath - The layer path being affected + */ + protected onProcessNameChanged(name: string, layerPath: string): void { + if (this.resultSet?.[layerPath]) { + // Inform that the layer set has been updated + this.onLayerSetUpdatedProcess(layerPath); + } + } + /** * Registers or Unregisters the layer in the layer-set, making sure the layer-set is aware of the layer. * @param {TypeLayerEntryConfig} layerConfig - The layer config diff --git a/packages/geoview-core/src/geo/layer/layer-sets/all-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/all-feature-info-layer-set.ts index 9eb42ed4396..a6342d08a80 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/all-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/all-feature-info-layer-set.ts @@ -120,6 +120,24 @@ export class AllFeatureInfoLayerSet extends AbstractLayerSet { } } + /** + * Overrides behaviour when layer name is changed. + * @param {string} name - The new layer name + * @param {string} layerPath - The layer path being affected + */ + protected override onProcessNameChanged(name: string, layerPath: string): void { + if (this.resultSet?.[layerPath]) { + // Change the layer name + this.resultSet[layerPath].data.layerName = name; + + // Call parent + super.onProcessNameChanged(name, layerPath); + + // Propagate to store + DataTableEventProcessor.propagateFeatureInfoToStore(this.mapId, layerPath, this.resultSet); + } + } + /** * Helper function used to launch the query on a layer to get all of its feature information. * @param {string} layerPath - The layerPath that will be queried diff --git a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts index f10bea9bcd4..1d3b9808008 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts @@ -143,6 +143,24 @@ export class FeatureInfoLayerSet extends AbstractLayerSet { } } + /** + * Overrides behaviour when layer name is changed. + * @param {string} name - The new layer name + * @param {string} layerPath - The layer path being affected + */ + protected override onProcessNameChanged(name: string, layerPath: string): void { + if (this.resultSet?.[layerPath]) { + // Change the layer name + this.resultSet[layerPath].data.layerName = name; + + // Call parent + super.onProcessNameChanged(name, layerPath); + + // Propagate to store + this.#propagateToStore(layerPath); + } + } + /** * Emits a query ended event to all handlers. * @param {QueryEndedEvent} event - The event to emit diff --git a/packages/geoview-core/src/geo/layer/layer-sets/legends-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/legends-layer-set.ts index 7344b87786c..57b7c38df80 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/legends-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/legends-layer-set.ts @@ -2,7 +2,7 @@ import { AbstractLayerSet } from '@/geo/layer/layer-sets/abstract-layer-set'; import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; import { ConfigBaseClass } from '@/core/utils/config/validation-classes/config-base-class'; import { logger } from '@/core/utils/logger'; -import { TypeLayerEntryConfig, TypeLayerStatus } from '@/geo/map/map-schema-types'; +import { TypeLayerEntryConfig, TypeLayerStatus, layerEntryIsGroupLayer } from '@/geo/map/map-schema-types'; import { TypeLegend } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; /** @@ -31,6 +31,35 @@ export class LegendsLayerSet extends AbstractLayerSet { this.resultSet[layerConfig.layerPath].data = undefined; } + /** + * Overrides the behavior to apply when unregistering a layer from the feature-info-layer-set. + * @param {TypeLayerEntryConfig} layerConfig - The layer config + */ + protected override onUnregisterLayer(layerConfig: TypeLayerEntryConfig): void { + // Log + logger.logTraceCore('LEGENDS-LAYER-SET - onUnregisterLayer', layerConfig.layerPath, Object.keys(this.resultSet)); + + // Call parent + super.onUnregisterLayer(layerConfig); + + // Delete from store + LegendEventProcessor.deleteLayerFromLegendLayers(this.mapId, layerConfig.layerPath); + } + + /** + * Registers or Unregisters the layer in the layer-set, making sure the layer-set is aware of the layer. + * @param {TypeLayerEntryConfig} layerConfig - The layer config + * @param {'add' | 'remove'} action - The action to perform: 'add' to register or 'remove' to unregister + */ + override registerOrUnregisterLayer(layerConfig: TypeLayerEntryConfig, action: 'add' | 'remove'): void { + // Group layers do not have an entry in this layer set, but need to be removed from legend layers + if (action === 'remove' && layerEntryIsGroupLayer(layerConfig)) { + // Delete from store + LegendEventProcessor.deleteLayerFromLegendLayers(this.mapId, layerConfig.layerPath); + } + super.registerOrUnregisterLayer(layerConfig, action); + } + /** * Overrides the behavior to apply when a layer status changed for a legends-layer-set. * @param {ConfigBaseClass} config - The layer config class @@ -113,6 +142,24 @@ export class LegendsLayerSet extends AbstractLayerSet { } } } + + /** + * Overrides behaviour when layer name is changed. + * @param {string} name - The new layer name + * @param {string} layerPath - The layer path being affected + */ + protected override onProcessNameChanged(name: string, layerPath: string): void { + if (this.resultSet?.[layerPath]) { + // Update name + this.resultSet[layerPath].layerName = name; + + // Call parent + super.onProcessNameChanged(name, layerPath); + + // Propagate to store + LegendEventProcessor.propagateLegendToStore(this.mapId, layerPath, this.resultSet[layerPath]); + } + } } export type TypeLegendResultSetEntry = { diff --git a/packages/geoview-core/src/geo/layer/layer.ts b/packages/geoview-core/src/geo/layer/layer.ts index f7a87dcdcb5..d2ab3c564c7 100644 --- a/packages/geoview-core/src/geo/layer/layer.ts +++ b/packages/geoview-core/src/geo/layer/layer.ts @@ -9,7 +9,7 @@ import { FeatureHighlight } from '@/geo/map/feature-highlight'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; import { ConfigValidation } from '@/core/utils/config/config-validation'; -import { generateId, whenThisThen } from '@/core/utils/utilities'; +import { createLocalizedString, generateId, whenThisThen } from '@/core/utils/utilities'; import { ConfigBaseClass, LayerStatusChangedEvent } from '@/core/utils/config/validation-classes/config-base-class'; import { logger } from '@/core/utils/logger'; import { AbstractGeoViewLayer, LayerRegistrationEvent } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; @@ -19,6 +19,8 @@ import { TypeLayerEntryConfig, mapConfigLayerEntryIsGeoCore, layerEntryIsGroupLayer, + TypeClassBreakStyleConfig, + TypeUniqueValueStyleConfig, } from '@/geo/map/map-schema-types'; import { GeoJSON, layerConfigIsGeoJSON } from '@/geo/layer/geoview-layers/vector/geojson'; import { GeoPackage, layerConfigIsGeoPackage } from '@/geo/layer/geoview-layers/vector/geopackage'; @@ -43,11 +45,15 @@ import { getMinOrMaxExtents } from '@/geo/utils/utilities'; import EventHelper, { EventDelegateBase } from '@/api/events/event-helper'; import { TypeOrderedLayerInfo } from '@/core/stores/store-interface-and-intial-values/map-state'; import { MapViewer } from '@/geo/map/map-viewer'; -import { api } from '@/app'; +import { AbstractGeoViewVector, api } from '@/app'; import { TimeSliderEventProcessor } from '@/api/event-processors/event-processor-children/time-slider-event-processor'; import { GeochartEventProcessor } from '@/api/event-processors/event-processor-children/geochart-event-processor'; import { SwiperEventProcessor } from '@/api/event-processors/event-processor-children/swiper-event-processor'; import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config'; +import { FeatureInfoEventProcessor } from '@/api/event-processors/event-processor-children/feature-info-event-processor'; +import { TypeLegendItem } from '@/core/components/layers/types'; +import { VectorLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-layer-entry-config'; +import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; export type TypeRegisteredLayers = { [layerPath: string]: TypeLayerEntryConfig }; @@ -106,6 +112,12 @@ export class LayerApi { // Keep all callback delegates references #onLayerAddedHandlers: LayerAddedDelegate[] = []; + #onLayerRemovedHandlers: LayerRemovedDelegate[] = []; + + #onLayerVisibilityToggledHandlers: LayerVisibilityToggledDelegate[] = []; + + #onLayerItemVisibilityToggledHandlers: LayerVisibilityToggledDelegate[] = []; + // Maximum time duration to wait when registering a layer for the time slider static #MAX_WAIT_TIME_SLIDER_REGISTRATION = 20000; @@ -766,67 +778,177 @@ export class LayerApi { EventHelper.offEvent(this.#onLayerAddedHandlers, callback); } + /** + * Emits an event to all handlers. + * @param {LayerRemovedEvent} event - The event to emit + * @private + */ + emitLayerRemoved(event: LayerRemovedEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onLayerRemovedHandlers, event); + } + + /** + * Registers a layer removed event handler. + * @param {LayerRemovedDelegate} callback - The callback to be executed whenever the event is emitted + */ + onLayerRemoved(callback: LayerRemovedDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onLayerRemovedHandlers, callback); + } + + /** + * Unregisters a layer removed event handler. + * @param {LayerRemovedDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offLayerRemoved(callback: LayerRemovedDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onLayerRemovedHandlers, callback); + } + + /** + * Emits layer visibility toggled event. + * @param {LayerVisibilityToggledEvent} event - The event to emit + */ + emitLayerVisibilityToggled(event: LayerVisibilityToggledEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onLayerVisibilityToggledHandlers, event); + } + + /** + * Registers a layer visibility toggled event handler. + * @param {LayerVisibilityToggledDelegate} callback - The callback to be executed whenever the event is emitted + */ + onLayerVisibilityToggled(callback: LayerVisibilityToggledDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onLayerVisibilityToggledHandlers, callback); + } + + /** + * Unregisters a layer visibility toggled event handler. + * @param {LayerVisibilityToggledDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offLayerVisibilityToggled(callback: LayerVisibilityToggledDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onLayerVisibilityToggledHandlers, callback); + } + + /** + * Emits layer item visibility toggled event. + * @param {LayerItemVisibilityToggledEvent} event - The event to emit + */ + #emitLayerItemVisibilityToggled(event: LayerItemVisibilityToggledEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onLayerItemVisibilityToggledHandlers, event); + } + + /** + * Registers a layer item visibility toggled event handler. + * @param {LayerItemVisibilityToggledDelegate} callback - The callback to be executed whenever the event is emitted + */ + onLayerItemVisibilityToggled(callback: LayerItemVisibilityToggledDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onLayerItemVisibilityToggledHandlers, callback); + } + + /** + * Unregisters a layer item visibility toggled event handler. + * @param {LayerItemVisibilityToggledDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offLayerItemVisibilityToggled(callback: LayerItemVisibilityToggledDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onLayerItemVisibilityToggledHandlers, callback); + } + /** * Removes all geoview layers from the map */ removeAllGeoviewLayers(): void { // For each Geoview layers - Object.values(this.geoviewLayers).forEach((layer: AbstractGeoViewLayer) => { + Object.keys(this.registeredLayers).forEach((layerPath) => { // Remove it - this.removeGeoviewLayer(layer.geoviewLayerId); + this.removeLayerUsingPath(layerPath); }); } - /** - * Removes a geoview layer from the map - * @param {string} geoviewLayerId - The geoview layer id to remove - */ - removeGeoviewLayer(geoviewLayerId: string): void { - // Redirect (weird, but at the time of writing for this refactor - this was what it was doing) - this.removeLayersUsingPath(geoviewLayerId); - } - /** * Removes a layer from the map using its layer path. The path may point to the root geoview layer * or a sub layer. - * @param {string} partialLayerPath - The path of the layer to be removed + * @param {string} layerPath - The path or ID of the layer to be removed */ - removeLayersUsingPath(partialLayerPath: string): void { + removeLayerUsingPath(layerPath: string): void { // A layer path is a slash seperated string made of the GeoView layer Id followed by the layer Ids - const partialLayerPathNodes = partialLayerPath.split('/'); + const layerPathNodes = layerPath.split('/'); // initialize these two constant now because we will delete the information used to get their values. - const indexToDelete = this.registeredLayers[partialLayerPath] - ? this.registeredLayers[partialLayerPath].parentLayerConfig?.listOfLayerEntryConfig.findIndex( - (layerConfig) => layerConfig === this.registeredLayers?.[partialLayerPath] + const indexToDelete = this.registeredLayers[layerPath] + ? this.registeredLayers[layerPath].parentLayerConfig?.listOfLayerEntryConfig.findIndex( + (layerConfig) => layerConfig === this.registeredLayers?.[layerPath] ) : undefined; - const listOfLayerEntryConfigAffected = this.registeredLayers[partialLayerPath]?.parentLayerConfig?.listOfLayerEntryConfig; - - Object.keys(this.registeredLayers).forEach((completeLayerPath) => { - const completeLayerPathNodes = completeLayerPath.split('/'); - const pathBeginningAreEqual = partialLayerPathNodes.reduce((areEqual, partialLayerPathNode, nodeIndex) => { - return areEqual && partialLayerPathNode === completeLayerPathNodes[nodeIndex]; - }, true); - if (pathBeginningAreEqual && this.getLayerEntryConfig(completeLayerPath)) { - this.unregisterLayer(this.getLayerEntryConfig(completeLayerPath)!); - delete this.registeredLayers[completeLayerPath]; + const listOfLayerEntryConfigAffected = this.registeredLayers[layerPath]?.parentLayerConfig?.listOfLayerEntryConfig; + + // Remove layer info from registered layers + Object.keys(this.registeredLayers).forEach((registeredLayerPath) => { + if (registeredLayerPath.startsWith(layerPath)) { + // Remove ol layer + this.mapViewer.map.removeLayer(this.registeredLayers[registeredLayerPath].olLayer as BaseLayer); + // Unregister layer + this.unregisterLayer(this.getLayerEntryConfig(registeredLayerPath)!); + // Remove from registered layers + delete MapEventProcessor.getMapViewerLayerAPI(this.mapId).registeredLayers[registeredLayerPath]; } }); + + // Remove from parents listOfLayerEntryConfig if (listOfLayerEntryConfigAffected) listOfLayerEntryConfigAffected.splice(indexToDelete!, 1); - if (this.geoviewLayers[partialLayerPath]) { - this.geoviewLayers[partialLayerPath].olRootLayer!.dispose(); - delete this.geoviewLayers[partialLayerPath]; - const { mapFeaturesConfig } = this.mapViewer; - if (mapFeaturesConfig.map.listOfGeoviewLayerConfig) - mapFeaturesConfig.map.listOfGeoviewLayerConfig = mapFeaturesConfig.map.listOfGeoviewLayerConfig.filter( - (geoviewLayerConfig) => geoviewLayerConfig.geoviewLayerId !== partialLayerPath + // Remove layer from geoview layers + if (this.geoviewLayers[layerPathNodes[0]]) { + const geoviewLayer = this.geoviewLayers[layerPathNodes[0]]; + + // If it is a single layer, remove geoview layer + if (layerPathNodes.length === 1 || (layerPathNodes.length === 2 && geoviewLayer.listOfLayerEntryConfig.length === 1)) { + geoviewLayer.olRootLayer!.dispose(); + delete this.geoviewLayers[layerPathNodes[0]]; + const { mapFeaturesConfig } = this.mapViewer; + + if (mapFeaturesConfig.map.listOfGeoviewLayerConfig) + mapFeaturesConfig.map.listOfGeoviewLayerConfig = mapFeaturesConfig.map.listOfGeoviewLayerConfig.filter( + (geoviewLayerConfig) => geoviewLayerConfig.geoviewLayerId !== layerPath + ); + } else if (layerPathNodes.length === 2) { + const updatedListOfLayerEntryConfig = geoviewLayer.listOfLayerEntryConfig.filter( + (entryConfig) => entryConfig.layerId !== layerPathNodes[1] ); + geoviewLayer.listOfLayerEntryConfig = updatedListOfLayerEntryConfig; + } else { + // For layer paths more than two deep, drill down through listOfLayerEntryConfigs to layer entry config to remove + let layerEntryConfig = geoviewLayer.listOfLayerEntryConfig.find((entryConfig) => entryConfig.layerId === layerPathNodes[1]); + + for (let i = 1; i < layerPathNodes.length; i++) { + if (i === layerPathNodes.length - 1 && layerEntryConfig) { + // When we get to the top level, remove the layer entry config + const updatedListOfLayerEntryConfig = layerEntryConfig.listOfLayerEntryConfig.filter( + (entryConfig) => entryConfig.layerId !== layerPathNodes[i] + ); + geoviewLayer.listOfLayerEntryConfig = updatedListOfLayerEntryConfig; + } else if (layerEntryConfig) { + // Not on the top level, so update to the latest + layerEntryConfig = layerEntryConfig.listOfLayerEntryConfig.find((entryConfig) => entryConfig.layerId === layerPathNodes[i]); + } + } + } } + // Emit about it + this.emitLayerRemoved({ layerPath }); + // Log - logger.logInfo(`Layer removed for ${partialLayerPath}`); + logger.logInfo(`Layer removed for ${layerPath}`); + + // Redirect to feature info delete + FeatureInfoEventProcessor.deleteFeatureInfo(this.mapId, layerPath); } /** @@ -1003,6 +1125,46 @@ export class LayerApi { } } + /** + * Toggle visibility of an item. + * @param {string} layerPath - The layer path of the layer to change. + * @param {TypeLegendItem} item - The item to change. + * @param {boolean} visibility - The visibility to set. + * @param {boolean} updateLegendLayers - Should legend layers be updated (here to avoid repeated rerendering when setting all items in layer). + */ + setItemVisibility(layerPath: string, item: TypeLegendItem, visibility: boolean, updateLegendLayers: boolean = true): void { + // Get registered layer config + const registeredLayer = this.registeredLayers[layerPath] as VectorLayerEntryConfig; + + if (visibility && !MapEventProcessor.getMapVisibilityFromOrderedLayerInfo(this.mapId, layerPath)) { + MapEventProcessor.setOrToggleMapLayerVisibility(this.mapId, layerPath, true); + } + + // Assign value to registered layer. This is use by applyFilter function to set visibility + // TODO: check if we need to refactor to centralize attribute setting.... + // TODO: know issue when we toggle a default visibility item https://github.com/Canadian-Geospatial-Platform/geoview/issues/1564 + if (registeredLayer.style![item.geometryType]?.styleType === 'classBreaks') { + const geometryStyleConfig = registeredLayer.style![item.geometryType]! as TypeClassBreakStyleConfig; + const classBreakStyleInfo = geometryStyleConfig.classBreakStyleInfo.find((styleInfo) => styleInfo.label === item.name); + if (classBreakStyleInfo) classBreakStyleInfo.visible = visibility; + else geometryStyleConfig.defaultVisible = visibility; + } else if (registeredLayer.style![item.geometryType]?.styleType === 'uniqueValue') { + const geometryStyleConfig = registeredLayer.style![item.geometryType]! as TypeUniqueValueStyleConfig; + const uniqueStyleInfo = geometryStyleConfig.uniqueValueStyleInfo.find((styleInfo) => styleInfo.label === item.name); + if (uniqueStyleInfo) uniqueStyleInfo.visible = visibility; + else geometryStyleConfig.defaultVisible = visibility; + } + + // Update the legend layers if necessary + if (updateLegendLayers) LegendEventProcessor.setItemVisibility(this.mapId, item, visibility); + + // Apply filter to layer + (this.getGeoviewLayer(layerPath) as AbstractGeoViewVector).applyViewFilter(layerPath, ''); + + // Emit event + this.#emitLayerItemVisibilityToggled({ layerPath, itemName: item.name, visibility }); + } + /** * Gets the max extent of all layers on the map, or of a provided subset of layers. * @@ -1027,6 +1189,62 @@ export class LayerApi { return bounds; } + + /** + * Set visibility of all geoview layers on the map + * + * @param {boolean} newValue - The new visibility. + */ + setAllLayersVisibility(newValue: boolean): void { + Object.keys(this.registeredLayers).forEach((layerPath) => { + this.setOrToggleLayerVisibility(layerPath, newValue); + }); + } + + /** + * Sets or toggles the visibility of a layer. + * + * @param {string} layerPath - The path of the layer. + * @param {boolean} newValue - The new value of visibility. + */ + setOrToggleLayerVisibility(layerPath: string, newValue?: boolean): void { + // Redirect to processor + MapEventProcessor.setOrToggleMapLayerVisibility(this.mapId, layerPath, newValue); + } + + /** + * Renames a layer. + * + * @param {string} layerPath - The path of the layer. + * @param {string} name - The new name to use. + */ + setLayerName(layerPath: string, name: string): void { + const layerConfig = this.registeredLayers[layerPath]; + if (layerConfig) { + layerConfig.layerName = createLocalizedString(name); + [this.legendsLayerSet, this.hoverFeatureInfoLayerSet, this.allFeatureInfoLayerSet, this.featureInfoLayerSet].forEach((layerSet) => { + // Process the layer status change + layerSet.processNameChanged(name, layerPath); + }); + } else { + logger.logError(`Unable to find layer ${layerPath}`); + } + } + + /** + * Redefine feature info fields. + * + * @param {string} layerPath - The path of the layer. + * @param {string} fieldNames - The new field names to use, separated by commas. + * @param {'aliasFields' | 'outfields'} fields - The fields to change. + */ + redefineFeatureFields(layerPath: string, fieldNames: string, fields: 'aliasFields' | 'outfields'): void { + const layerConfig = this.registeredLayers[layerPath]; + if (!layerConfig) logger.logError(`Unable to find layer ${layerPath}`); + else if (layerConfig.source?.featureInfo && layerConfig.source?.featureInfo.queryable !== false) + layerConfig.source.featureInfo[fields] = createLocalizedString(fieldNames); + else logger.logError(`${layerPath} is not queryable`); + } } /** @@ -1041,3 +1259,48 @@ export type LayerAddedEvent = { // The added layer layer: AbstractGeoViewLayer; }; + +/** + * Define a delegate for the event handler function signature + */ +type LayerRemovedDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type LayerRemovedEvent = { + // The added layer + layerPath: string; +}; + +/** + * Define a delegate for the event handler function signature + */ +type LayerVisibilityToggledDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type LayerVisibilityToggledEvent = { + // The layer path of the affected layer + layerPath: string; + // The new visibility + visibility: boolean; +}; + +/** + * Define a delegate for the event handler function signature + */ +type LayerItemVisibilityToggledDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type LayerItemVisibilityToggledEvent = { + // The layer path of the affected layer + layerPath: string; + // Name of the item being toggled + itemName: string; + // The new visibility + visibility: boolean; +}; diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index 85436a72690..b777546514e 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -1487,6 +1487,21 @@ export class MapViewer { return MapEventProcessor.zoomToExtent(this.mapId, extent, options); } + /** + * Zoom to specified extent or coordinate provided in lnglat. + * + * @param {Extent | Coordinate} extent - The extent or coordinate to zoom to. + * @param {FitOptions} options - The options to configure the zoomToExtent (default: { padding: [100, 100, 100, 100], maxZoom: 11 }). + */ + zoomToLngLatExtentOrCoordinate(extent: Extent | Coordinate, options?: FitOptions): Promise { + const fullExtent = extent.length === 2 ? [extent[0], extent[1], extent[0], extent[1]] : extent; + const projectedExtent = Projection.transformExtent( + fullExtent, + Projection.PROJECTION_NAMES.LNGLAT, + `EPSG:${this.getMapState().currentProjection}` + ); + return MapEventProcessor.zoomToExtent(this.mapId, projectedExtent, options); + } // #endregion /** diff --git a/packages/geoview-core/src/geo/utils/utilities.ts b/packages/geoview-core/src/geo/utils/utilities.ts index b5bfee25685..e977feb635f 100644 --- a/packages/geoview-core/src/geo/utils/utilities.ts +++ b/packages/geoview-core/src/geo/utils/utilities.ts @@ -10,6 +10,7 @@ import { Extent } from 'ol/extent'; import XYZ from 'ol/source/XYZ'; import TileLayer from 'ol/layer/Tile'; +import { Polygon } from 'ol/geom'; import { Cast, TypeJsonObject } from '@/core/types/global-types'; import { TypeFeatureStyle } from '@/geo/layer/geometry/geometry-types'; import { xmlToJson } from '@/core/utils/utilities'; @@ -298,7 +299,7 @@ export function getTranslateValues(element: HTMLElement): { * @param {number} value the value to format * @returns {string} the formatted value */ -export function coordFormnatDMS(value: number): string { +export function coordFormatDMS(value: number): string { // degree char const deg = String.fromCharCode(176); @@ -357,3 +358,43 @@ export function getMinOrMaxExtents(extentsA: Extent, extentsB: Extent, minmax = export const isVectorLayer = (layer: AbstractGeoViewLayer): boolean => { return layer?.type in VECTOR_LAYER; }; + +/** + * Convert an extent to a polygon + * + * @param {Extent} extent - The extent to convert + * @returns {Polygon} The created polygon + */ +export function extentToPolygon(extent: Extent): Polygon { + const polygon = new Polygon([ + [ + [extent[0], extent[1]], + [extent[0], extent[3]], + [extent[2], extent[3]], + [extent[2], extent[1]], + ], + ]); + return polygon; +} + +/** + * Convert an polygon to an extent + * + * @param {Polygon} polygon - The polygon to convert + * @returns {Extent} The created extent + */ +export function polygonToExtent(polygon: Polygon): Extent { + const outerRing = polygon.getCoordinates()[0]; + let minx = outerRing[0][0]; + let miny = outerRing[0][1]; + let maxx = outerRing[0][0]; + let maxy = outerRing[0][1]; + for (let i = 1; i < outerRing.length; i++) { + minx = Math.min(outerRing[i][0], minx); + miny = Math.min(outerRing[i][1], miny); + maxx = Math.max(outerRing[i][0], maxx); + maxy = Math.max(outerRing[i][1], maxy); + } + const extent: Extent = [minx, miny, maxx, maxy]; + return extent; +} diff --git a/packages/geoview-time-slider/src/index.tsx b/packages/geoview-time-slider/src/index.tsx index 82446a79809..4e97b91f990 100644 --- a/packages/geoview-time-slider/src/index.tsx +++ b/packages/geoview-time-slider/src/index.tsx @@ -11,10 +11,6 @@ import schema from '../schema.json'; import defaultConfig from '../default-config-time-slider-panel.json'; import { SliderProps } from './time-slider-types'; -export interface LayerProps { - layerPath: string; - layerName: string; -} export interface SliderFilterProps { title: string; description: string; diff --git a/packages/geoview-time-slider/src/time-slider-panel.tsx b/packages/geoview-time-slider/src/time-slider-panel.tsx index e3e362f28be..192ebe95244 100644 --- a/packages/geoview-time-slider/src/time-slider-panel.tsx +++ b/packages/geoview-time-slider/src/time-slider-panel.tsx @@ -5,6 +5,8 @@ import { useTimeSliderLayers, } from 'geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state'; import { useMapVisibleLayers } from 'geoview-core/src/core/stores/store-interface-and-intial-values/map-state'; +import { useLayerLegendLayers } from 'geoview-core/src/core/stores/store-interface-and-intial-values/layer-state'; +import { LegendEventProcessor } from 'geoview-core/src/api/event-processors/event-processor-children/legend-event-processor'; import { Box } from 'geoview-core/src/ui'; import { logger } from 'geoview-core/src/core/utils/logger'; @@ -35,6 +37,7 @@ export function TimeSliderPanel(props: TypeTimeSliderProps): JSX.Element { // get values from store const visibleLayers = useMapVisibleLayers() as string[]; const timeSliderLayers = useTimeSliderLayers(); + const legendLayers = useLayerLegendLayers(); /** * handle Layer list when clicked on each layer. @@ -70,12 +73,13 @@ export function TimeSliderPanel(props: TypeTimeSliderProps): JSX.Element { /** * Create layer tooltip * @param {TypeTimeSliderValues} timeSliderLayerInfo Time slider layer info. + * @param {string} name Time slider layer name. * @returns */ - const getLayerTooltip = (timeSliderLayerInfo: TypeTimeSliderValues): ReactNode => { + const getLayerTooltip = (timeSliderLayerInfo: TypeTimeSliderValues, name: string): ReactNode => { return ( - {timeSliderLayerInfo.name} + {name} {timeSliderLayerInfo.filtering && `: ${getFilterInfo(timeSliderLayerInfo)}`} ); @@ -89,15 +93,18 @@ export function TimeSliderPanel(props: TypeTimeSliderProps): JSX.Element { .filter((layer) => layer && layer.timeSliderLayerInfo) .map((layer) => { return { - layerName: layer.timeSliderLayerInfo.name, + layerName: LegendEventProcessor.findLayerByPath(legendLayers, layer.layerPath).layerName, layerPath: layer.layerPath, layerFeatures: getFilterInfo(layer.timeSliderLayerInfo), - tooltip: getLayerTooltip(layer.timeSliderLayerInfo), + tooltip: getLayerTooltip( + layer.timeSliderLayerInfo, + LegendEventProcessor.findLayerByPath(legendLayers, layer.layerPath).layerName + ), layerStatus: 'loaded', queryStatus: 'processed', } as LayerListEntry; }); - }, [timeSliderLayers, visibleLayers]); + }, [legendLayers, timeSliderLayers, visibleLayers]); useEffect(() => { // Log diff --git a/packages/geoview-time-slider/src/time-slider.tsx b/packages/geoview-time-slider/src/time-slider.tsx index 019edcf6a5c..5a028f43631 100644 --- a/packages/geoview-time-slider/src/time-slider.tsx +++ b/packages/geoview-time-slider/src/time-slider.tsx @@ -4,6 +4,8 @@ import { useTimeSliderLayers, useTimeSliderStoreActions, } from 'geoview-core/src/core/stores/store-interface-and-intial-values/time-slider-state'; +import { useLayerLegendLayers } from 'geoview-core/src/core/stores/store-interface-and-intial-values/layer-state'; +import { LegendEventProcessor } from 'geoview-core/src/api/event-processors/event-processor-children/legend-event-processor'; import { getLocalizedValue, getLocalizedMessage } from 'geoview-core/src/core/utils/utilities'; import { useAppDisplayLanguage } from 'geoview-core/src/core/stores/store-interface-and-intial-values/app-state'; import { logger } from 'geoview-core/src/core/utils/logger'; @@ -68,7 +70,6 @@ export function TimeSlider(props: TimeSliderProps): JSX.Element { const { title, description, - name, defaultValue, discreteValues, range, @@ -83,6 +84,10 @@ export function TimeSlider(props: TimeSliderProps): JSX.Element { reversed, } = useTimeSliderLayers()[layerPath]; + // Get name from legend layers + const legendLayers = useLayerLegendLayers(); + const name = LegendEventProcessor.findLayerByPath(legendLayers, layerPath).layerName; + // slider config useEffect(() => { // Log