From 2a03afa847aae37c35def2c416b7d444c4ade37d Mon Sep 17 00:00:00 2001 From: Damon Ulmi <63123585+DamonU2@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:40:17 -0700 Subject: [PATCH 1/2] feat(event): Event added to API for map ready (#2510) --- packages/geoview-core/src/api/api.ts | 71 ++++++++++++++++++- .../map-event-processor.ts | 5 +- packages/geoview-core/src/core/app-start.tsx | 11 +-- .../geoview-core/src/geo/map/map-viewer.ts | 2 +- 4 files changed, 76 insertions(+), 13 deletions(-) diff --git a/packages/geoview-core/src/api/api.ts b/packages/geoview-core/src/api/api.ts index c4eaee47331..27b66789e90 100644 --- a/packages/geoview-core/src/api/api.ts +++ b/packages/geoview-core/src/api/api.ts @@ -33,6 +33,9 @@ export class API { // utilities object utilities; + // Keep all callback delegates references + #onMapViewerReadyHandlers: MapViewerReadyDelegate[] = []; + // Keep all callback delegates references #onMapAddedToDivHandlers: MapAddedToDivDelegate[] = []; @@ -54,6 +57,32 @@ export class API { API.#manageKeyboardFocus(); } + // TODO Add getter function and make maps property private + /** + * Sets a map viewer in maps, or removes one if the mapViewer property is null. + * @param {string} mapId - ID of the map + * @param {MapViewer | null} mapViewer - The viewer to be added or null to remove + * @param {(mapViewer: MapViewer) => void} onMapViewerInit - Function to run on map init + */ + setMapViewer(mapId: string, mapViewer: MapViewer | null, onMapViewerInit?: (mapViewer: MapViewer) => void): void { + if (mapViewer) { + if (this.maps[mapId]) logger.logError(`Cannot add map. Map with ID ${mapId} already exists`); + else { + this.maps[mapId] = mapViewer; + + // Register a handler (which will only happen once) for when the map viewer will get initialized. + // At the time of writing, this happens later, asynchronously, via the components/map/map.tsx when 'MapViewer.initMap()' is called. + // That should be fixed eventually, but that refactoring is out of the scope at the time of writing. So, I'm doing like this for now. + this.maps[mapId].onMapInit((viewer) => { + // MapViewer has been created and initialized, callback about it + onMapViewerInit?.(viewer); + // Emit that viewer is ready + this.#emitMapViewerReady({ mapId }); + }); + } + } else delete this.maps[mapId]; + } + /** * Apply outline to elements when keyboard is use to navigate * Code from: https://github.com/MaxMaeder/keyboardFocus.js @@ -123,6 +152,33 @@ export class API { return Promise.reject(new Error(`Div with id ${divId} does not exist`)); } + /** + * Emits a map viewer ready event to all handlers. + * @private + */ + #emitMapViewerReady(event: MapViewerReadyEvent): void { + // Emit the event for all handlers + EventHelper.emitEvent(this, this.#onMapViewerReadyHandlers, event); + } + + /** + * Registers a map viewer ready event callback. + * @param {MapViewerReadyDelegate} callback - The callback to be executed whenever the event is emitted + */ + onMapViewerReady(callback: MapViewerReadyDelegate): void { + // Register the event handler + EventHelper.onEvent(this.#onMapViewerReadyHandlers, callback); + } + + /** + * Unregisters a map viewer ready event callback. + * @param {MapViewerReadyDelegate} callback - The callback to stop being called whenever the event is emitted + */ + offMapViewerReady(callback: MapViewerReadyDelegate): void { + // Unregister the event handler + EventHelper.offEvent(this.#onMapViewerReadyHandlers, callback); + } + /** * Emits an event to all handlers. * @param {MapAddedToDivEvent} event - The event to emit @@ -152,6 +208,19 @@ export class API { } } +/** + * Define a delegate for the event handler function signature + */ +type MapViewerReadyDelegate = EventDelegateBase; + +/** + * Define an event for the delegate + */ +export type MapViewerReadyEvent = { + // The added map + mapId: string; +}; + /** * Define a delegate for the event handler function signature */ @@ -161,6 +230,6 @@ type MapAddedToDivDelegate = EventDelegateBase; * Define an event for the delegate */ export type MapAddedToDivEvent = { - // The added layer + // The added map mapId: string; }; 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 ec7da9699db..728baafae89 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 @@ -17,6 +17,7 @@ import { TypeViewSettings, TypePointMarker, } from '@config/types/map-schema-types'; +import { cloneDeep } from 'lodash'; import { api } from '@/app'; import { LayerApi } from '@/geo/layer/layer'; import { MapViewer, TypeMapState, TypeMapMouseInfo } from '@/geo/map/map-viewer'; @@ -1050,7 +1051,9 @@ export class MapEventProcessor extends AbstractEventProcessor { : undefined, initialSettings, style: legendLayerInfo!.styleConfig ? legendLayerInfo!.styleConfig : undefined, - source: (layerEntryConfig! as VectorLayerEntryConfig).source ? (layerEntryConfig! as VectorLayerEntryConfig).source : undefined, + source: (layerEntryConfig! as VectorLayerEntryConfig).source + ? cloneDeep((layerEntryConfig! as VectorLayerEntryConfig).source) + : undefined, entryType: listOfLayerEntryConfig.length ? 'group' : undefined, listOfLayerEntryConfig: listOfLayerEntryConfig.length ? listOfLayerEntryConfig : [], }; diff --git a/packages/geoview-core/src/core/app-start.tsx b/packages/geoview-core/src/core/app-start.tsx index 418265f77ab..966775b9887 100644 --- a/packages/geoview-core/src/core/app-start.tsx +++ b/packages/geoview-core/src/core/app-start.tsx @@ -35,7 +35,6 @@ export type TypeMapContext = { */ interface AppStartProps { mapFeaturesConfig: TypeMapFeaturesConfig; - // eslint-disable-next-line react/require-default-props onMapViewerInit?: (mapViewer: MapViewer) => void; } @@ -75,17 +74,9 @@ function AppStart(props: AppStartProps): JSX.Element { // TODO: use store, remove the use of feature by viewer class and use state to gather values if (!(mapId in api.maps)) { const mapViewer = new MapViewer(mapFeaturesConfig, i18nInstance); - api.maps[mapId] = mapViewer; + api.setMapViewer(mapId, mapViewer, onMapViewerInit); } - // Register a handler (which will only happen once) for when the map viewer will get initialized. - // At the time of writing, this happens later, asynchronously, via the components/map/map.tsx when 'MapViewer.initMap()' is called. - // That should be fixed eventually, but that refactoring is out of the scope at the time of writing. So, I'm doing like this for now. - api.maps[mapId].onMapInit((mapViewer) => { - // MapViewer has been created and initialized, callback about it - onMapViewerInit?.(mapViewer); - }); - return ( diff --git a/packages/geoview-core/src/geo/map/map-viewer.ts b/packages/geoview-core/src/geo/map/map-viewer.ts index 61e66ef7679..87bf5f0d4c6 100644 --- a/packages/geoview-core/src/geo/map/map-viewer.ts +++ b/packages/geoview-core/src/geo/map/map-viewer.ts @@ -1201,7 +1201,7 @@ export class MapViewer { if (deleteContainer) mapContainer.remove(); // Delete the map instance from the maps array, will delete attached plugins - delete api.maps[this.mapId]; + api.setMapViewer(this.mapId, null); // Return the map container to be remove return mapContainer; From 6ac8c8d8cc5a5858912411afdd13e4a3764d03af Mon Sep 17 00:00:00 2001 From: Yves Choquette <53231475+ychoquet@users.noreply.github.com> Date: Fri, 27 Sep 2024 07:36:51 -0400 Subject: [PATCH 2/2] 2520-Important corrections for add layer (#2521) --- .../public/datasets/geojson/metadata-new.meta | 48 +++---- .../raster-config/wms-config.ts | 11 +- .../entry-config-base-class.ts | 121 ++++++++++++++++-- 3 files changed, 134 insertions(+), 46 deletions(-) diff --git a/packages/geoview-core/public/datasets/geojson/metadata-new.meta b/packages/geoview-core/public/datasets/geojson/metadata-new.meta index 645cb50792f..7b6a31e1b3a 100644 --- a/packages/geoview-core/public/datasets/geojson/metadata-new.meta +++ b/packages/geoview-core/public/datasets/geojson/metadata-new.meta @@ -274,13 +274,11 @@ } }, "style": { - "Point": { - "styleType": "simple", - "label": "Point label", - "settings": { - "type": "simpleSymbol", - "symbol": "star" - } + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" } } }, @@ -338,13 +336,11 @@ } }, "style": { - "Point": { - "styleType": "simple", - "label": "Point label", - "settings": { - "type": "simpleSymbol", - "symbol": "star" - } + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" } } }, @@ -397,13 +393,11 @@ } }, "style": { - "Point": { - "styleType": "simple", - "label": "Point label", - "settings": { - "type": "simpleSymbol", - "symbol": "star" - } + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" } } }, @@ -456,13 +450,11 @@ } }, "style": { - "Point": { - "styleType": "simple", - "label": "Point label", - "settings": { - "type": "simpleSymbol", - "symbol": "star" - } + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" } } } diff --git a/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/wms-config.ts b/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/wms-config.ts index 825815b0e3c..6934cbb3aaf 100644 --- a/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/wms-config.ts +++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/wms-config.ts @@ -393,12 +393,15 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { layerConfigsToQuery.forEach((layerConfig: WmsLayerEntryConfig, layerIndex: number) => { // verify if the a request with the same layerId has already been sent up to now. for (i = 0; layerConfigsToQuery[i].layerId !== layerConfig.layerId; i++); - if (i === layerIndex) + if (i === layerIndex) { // if the layer found is the same as the current layer index, // this is the first time we execute this request - promisedArrayOfMetadata.push(this.#executeServiceMetadataRequest(`${this.metadataAccessPath}?Layers=${layerConfig.layerId}`)); - // otherwise, we are already waiting for the same request and we will wait for it to finish. - else promisedArrayOfMetadata.push(promisedArrayOfMetadata[i]); + const urlToQuery = this.metadataAccessPath.includes('?') + ? `${this.metadataAccessPath}&Layers=${layerConfig.layerId}` + : `${this.metadataAccessPath}?Layers=${layerConfig.layerId}`; + promisedArrayOfMetadata.push(this.#executeServiceMetadataRequest(urlToQuery)); + // otherwise, we are already waiting for the same request and we will wait for it to finish. + } else promisedArrayOfMetadata.push(promisedArrayOfMetadata[i]); }); // Since we use Promise.all, If one of the Promise awaited fails, then the whole service metadata fetching will fail. diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts index 886f37e3e01..792ab7e373d 100644 --- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts +++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts @@ -1,8 +1,9 @@ import cloneDeep from 'lodash/cloneDeep'; import { CV_DEFAULT_LAYER_INITIAL_SETTINGS } from '@config/types/config-constants'; -import { TypeJsonObject } from '@config/types/config-types'; +import { toJsonObject, TypeJsonObject } from '@config/types/config-types'; import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config'; +import { layerEntryIsGroupLayer } from '@config/types/type-guards'; import { TypeGeoviewLayerType, TypeLayerEntryType, @@ -56,7 +57,7 @@ export abstract class EntryConfigBaseClass { bounds: Extent | undefined; /** Layer entry data type. */ - entryType: TypeLayerEntryType; + entryType?: TypeLayerEntryType; // GV NOTE START **************************************************************************************************** // The following attributes use the 'definite assignment assertion' (! after the property name) to indicate that @@ -151,16 +152,6 @@ export abstract class EntryConfigBaseClass { // ================= // #region PROTECTED - /** - * The getter method that returns the language used to create the sublayer. - * - * @returns {TypeDisplayLanguage} The language associated to the config. - * @protected - */ - protected getLanguage(): TypeDisplayLanguage { - return this.#language; - } - /** * Validate the node configuration using the schema associated to its layer type. * @protected @@ -197,6 +188,11 @@ export abstract class EntryConfigBaseClass { return this.#geoviewLayerConfig.geoviewLayerType; } + /** Set the geoview layer that owns this sub-layer configuration. */ + setGeoviewLayerConfig(geoviewLayerConfig: AbstractGeoviewLayerConfig): void { + this.#geoviewLayerConfig = geoviewLayerConfig; + } + /** The geoview layer that owns this sub-layer configuration. */ getGeoviewLayerConfig(): AbstractGeoviewLayerConfig { return this.#geoviewLayerConfig; @@ -221,9 +217,11 @@ export abstract class EntryConfigBaseClass { /** * Method used to set the EntryConfigBaseClass error flag to true. Once this operation has been performed, the layer entry * config is no longer considered viable. + * + * @param {boolean} value The value to assign to the flag. */ - setErrorDetectedFlag(): void { - this.#errorDetectedFlag = true; + setErrorDetectedFlag(value = true): void { + this.#errorDetectedFlag = value; } /** @@ -235,6 +233,15 @@ export abstract class EntryConfigBaseClass { return this.#errorDetectedFlag; } + /** + * Method used to set the parent node. + * + * @param {EntryConfigBaseClass | undefined} parentNode The parent node. + */ + setParentNode(parentNode: EntryConfigBaseClass | undefined): void { + this.#parentNode = parentNode; + } + /** * The getter method that returns the parentNode. * @@ -244,6 +251,24 @@ export abstract class EntryConfigBaseClass { return this.#parentNode; } + /** + * The setter method that sets the language used to create the sublayer. + * + * @param {TypeDisplayLanguage} language The language associated to the config. + */ + setLanguage(language: TypeDisplayLanguage): void { + this.#language = language; + } + + /** + * The getter method that returns the language used to create the sublayer. + * + * @returns {TypeDisplayLanguage} The language associated to the config. + */ + getLanguage(): TypeDisplayLanguage { + return this.#language; + } + /** * This method returns the json string of the entry configuration. The output representation is a multi-line indented * string. Indentation can be controled using the ident parameter. Private variables are not serialized. @@ -264,6 +289,74 @@ export abstract class EntryConfigBaseClass { this.minScale = 0; this.maxScale = 0; } + + /** + * Create a clone of this node. This method is mainly used to clone a node from the layer tree to store a copy in the + * list of layer entry config of the GeoView Layer. It was created to preserve the private fields created using the # + * operator because cloneDeep doesn't copy them to the cloned instance. + * + * @param {EntryConfigBaseClass | undefined} parentNode The layer group that owns this node. + * + * @returns {EntryConfigBaseClass} The clone copy of the node. + */ + clone(parentNode: EntryConfigBaseClass | undefined = undefined): EntryConfigBaseClass { + let cloneOfTheNode: EntryConfigBaseClass = cloneDeep(this); + + // Remove the following properties to avoid schema validation errors. + delete cloneOfTheNode.layerName; + delete cloneOfTheNode.entryType; + if ('listOfLayerEntryConfig' in cloneOfTheNode) cloneOfTheNode.listOfLayerEntryConfig = []; + + // Create a new instance using the cloned config. + if (cloneOfTheNode.isLayerGroup) + cloneOfTheNode = this.#geoviewLayerConfig.createGroupNode( + toJsonObject(cloneOfTheNode), + this.#language, + this.#geoviewLayerConfig, + parentNode + )!; + else + cloneOfTheNode = this.#geoviewLayerConfig.createLeafNode( + toJsonObject(cloneOfTheNode), + this.#language, + this.#geoviewLayerConfig, + parentNode + )!; + // Restore the layerName and the private properties. + cloneOfTheNode.layerName = this.layerName; + cloneOfTheNode.setErrorDetectedFlag(this.#errorDetectedFlag); + cloneOfTheNode.setLayerMetadata(this.#layerMetadata); + return cloneOfTheNode; + } + + /** + * The getter method that returns the sublayer configuration. If the layer path doesn't exists, return undefined. + * + * @returns {EntryConfigBaseClass | undefined} The sublayer configuration. + */ + getSubLayerConfig(layerPath: string): EntryConfigBaseClass | undefined { + // The node is a group + if (this.isLayerGroup && 'listOfLayerEntryConfig' in this) { + const pathItems = layerPath.split('/'); + if (pathItems[0] !== this.layerId) return undefined; + if (pathItems.length === 1) return this; + let { listOfLayerEntryConfig } = this; + let nodeFound: EntryConfigBaseClass | undefined; + for (let i = 1; i < pathItems.length; i++) { + nodeFound = (listOfLayerEntryConfig as EntryConfigBaseClass[]).find( + (layerEntryConfig) => layerEntryConfig.layerId === pathItems[i] + ); + if (!nodeFound) break; + listOfLayerEntryConfig = layerEntryIsGroupLayer(nodeFound) ? nodeFound.listOfLayerEntryConfig : []; + } + return nodeFound; + } + + // The node is a leaf. + if (layerPath === this.layerId) return this; + return undefined; + } + // #endregion PUBLIC // #endregion METHODS // #endregion CLASS HEADER