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..1c0068649e3 --- /dev/null +++ b/packages/geoview-core/public/templates/demos/demo-function-event.html @@ -0,0 +1,226 @@ + + + + + + <%= 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

+
+ + + + + + 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..fc002518c43 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).removeLayersUsingPath(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..0b8b35e9577 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 @@ -82,6 +82,19 @@ export class TimeSliderEventProcessor extends AbstractEventProcessor { this.getTimesliderState(mapId)?.setterActions.removeTimeSliderLayer(layerPath); } + /** + * Set the name of a time slider layer. + * @param {string} mapId - The map id of the state to act on + * @param {string} layerPath - The layer path of the layer to change + * @param {string} name - The new layer name + */ + static setLayerName(mapId: string, layerPath: string, name: string): void { + if (this.getTimesliderState(mapId) && this.getTimesliderState(mapId)?.timeSliderLayers[layerPath]) { + // Redirect + this.getTimesliderState(mapId)?.setterActions.setName(layerPath, name); + } + } + /** * Get initial values for a layer's time slider states * 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/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..8945d5cbb96 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 @@ -53,6 +53,7 @@ export interface ITimeSliderState { setDelay: (layerPath: string, delay: number) => void; setFiltering: (layerPath: string, filter: boolean) => void; setLocked: (layerPath: string, locked: boolean) => void; + setName: (layerPath: string, name: string) => void; setReversed: (layerPath: string, locked: boolean) => void; setDefaultValue: (layerPath: string, defaultValue: string) => void; setValues: (layerPath: string, values: number[]) => void; @@ -179,6 +180,16 @@ export function initializeTimeSliderState(set: TypeSetStore, get: TypeGetStore): }, }); }, + setName(layerPath: string, name: string): void { + const sliderLayers = get().timeSliderState.timeSliderLayers; + sliderLayers[layerPath].name = name; + set({ + timeSliderState: { + ...get().timeSliderState, + timeSliderLayers: { ...sliderLayers }, + }, + }); + }, setReversed(layerPath: string, reversed: boolean): void { const sliderLayers = get().timeSliderState.timeSliderLayers; sliderLayers[layerPath].reversed = reversed; 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..d5c589225f2 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} layerPath - The layer path being affected + * @param {string} name - The new layer name + */ + public processNameChanged(layerPath: string, name: string): void { + // Call the overridable function to process a layer name change + this.onProcessNameChanged(layerPath, name); + } + + /** + * 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..ee74f95ea57 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,10 @@ export class LayerApi { // Keep all callback delegates references #onLayerAddedHandlers: LayerAddedDelegate[] = []; + #onLayerRemovedHandlers: LayerRemovedDelegate[] = []; + + #onLayerVisibilityToggledHandlers: LayerVisibilityToggledDelegate[] = []; + // Maximum time duration to wait when registering a layer for the time slider static #MAX_WAIT_TIME_SLIDER_REGISTRATION = 20000; @@ -766,14 +776,69 @@ 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); + } + /** * 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.removeGeoviewLayer(layerPath); }); } @@ -789,44 +854,81 @@ export class LayerApi { /** * 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 { + removeLayersUsingPath(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 +1105,43 @@ 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, ''); + } + /** * Gets the max extent of all layers on the map, or of a provided subset of layers. * @@ -1027,6 +1166,48 @@ 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(layerPath, name); + }); + TimeSliderEventProcessor.setLayerName(this.mapId, layerPath, name); + } else { + logger.logError(`Unable to find layer ${layerPath}`); + } + } } /** @@ -1041,3 +1222,31 @@ 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; +};