From 8f4d8b5267b42fdc0dfa1ecb761adc93ed5d922e Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 4 Oct 2024 13:26:08 -0400 Subject: [PATCH] 2259 osdp geocore (#2535) * feat(core): Apply custom geocore config Closes #2259 * fix build * fix uuid, init geochart * fix tab problem and layer def for date regex * rebase pnpm lock1 * Fix comments * fix commment * Fix outFields unwanted present * Fix build * fix typo * fix more typo --------- Co-authored-by: jolevesq --- common/config/rush/pnpm-lock.yaml | 13 +++- .../configs/navigator/27-geocore-custom.json | 42 ++++++++++ .../public/templates/demos-navigator.html | 1 + .../templates/demos/demo-osdp-water.html | 2 +- packages/geoview-core/schema.json | 9 ++- .../src/api/config/uuid-config-reader.ts | 39 +++++++++- .../geoview-core/src/api/plugin/plugin.ts | 2 +- .../core/components/footer-bar/footer-bar.tsx | 6 +- .../core/utils/config/config-validation.ts | 4 +- .../utils/config/reader/uuid-config-reader.ts | 76 ++++++++++++++----- .../geoview-core/src/core/utils/utilities.ts | 21 +++++ .../geoview-layers/raster/esri-dynamic.ts | 9 ++- .../geo/layer/geoview-layers/raster/wms.ts | 33 +------- .../vector/abstract-geoview-vector.ts | 10 ++- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 9 ++- .../src/geo/layer/gv-layers/raster/gv-wms.ts | 33 +------- .../gv-layers/vector/abstract-gv-vector.ts | 10 ++- .../layer/layer-sets/abstract-layer-set.ts | 46 +++++++++++ .../layer-sets/all-feature-info-layer-set.ts | 6 +- .../layer-sets/feature-info-layer-set.ts | 13 +++- packages/geoview-geochart/src/index.tsx | 5 +- 21 files changed, 286 insertions(+), 103 deletions(-) create mode 100644 packages/geoview-core/public/configs/navigator/27-geocore-custom.json diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 2a81223cd14..3c3457a3dd5 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -4899,6 +4899,13 @@ packages: fsevents: 2.3.3 dev: true + /chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + dependencies: + readdirp: 4.0.1 + dev: true + /chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -9061,6 +9068,11 @@ packages: picomatch: 2.3.1 dev: true + /readdirp@4.0.1: + resolution: {integrity: sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==} + engines: {node: '>= 14.16.0'} + dev: true + /rechoir@0.7.1: resolution: {integrity: sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==} engines: {node: '>= 0.10'} @@ -9283,7 +9295,6 @@ packages: parse-srcset: 1.0.2 postcss: 8.4.47 dev: false - /saxes@5.0.1: resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} engines: {node: '>=10'} diff --git a/packages/geoview-core/public/configs/navigator/27-geocore-custom.json b/packages/geoview-core/public/configs/navigator/27-geocore-custom.json new file mode 100644 index 00000000000..887d86ed35c --- /dev/null +++ b/packages/geoview-core/public/configs/navigator/27-geocore-custom.json @@ -0,0 +1,42 @@ +{ + "map": { + "interaction": "dynamic", + "viewSettings": { + "projection": 3978 + }, + "basemapOptions": { + "basemapId": "transport", + "shaded": true, + "labeled": false + }, + "listOfGeoviewLayerConfig": [ + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "ea4c0bdb-a63f-49a4-b14a-09c1560aad0b" + }, + { + "geoviewLayerId": "21b821cf-0f1c-40ee-8925-eab12d357668", + "geoviewLayerType": "geoCore" + } + ] + }, + "components": [ + "overview-map" + ], + "overviewMap": { + "hideOnZoom": 7 + }, + "footerBar": { + "tabs": { + "core": [ + "legend", + "layers", + "details", + "geochart", + "data-table" + ] + } + }, + "corePackages": [], + "theme": "geo.ca" +} \ No newline at end of file diff --git a/packages/geoview-core/public/templates/demos-navigator.html b/packages/geoview-core/public/templates/demos-navigator.html index 54ccd68b03f..7edefd049c1 100644 --- a/packages/geoview-core/public/templates/demos-navigator.html +++ b/packages/geoview-core/public/templates/demos-navigator.html @@ -147,6 +147,7 @@

Configurations Navigator

+ diff --git a/packages/geoview-core/public/templates/demos/demo-osdp-water.html b/packages/geoview-core/public/templates/demos/demo-osdp-water.html index b94818cf515..be5ec2e6e57 100644 --- a/packages/geoview-core/public/templates/demos/demo-osdp-water.html +++ b/packages/geoview-core/public/templates/demos/demo-osdp-water.html @@ -54,7 +54,7 @@

Water

'map': { 'interaction': 'dynamic', 'viewSettings': { - 'projection': 3978 + 'projection': 3857 }, 'basemapOptions': { 'basemapId': 'transport', diff --git a/packages/geoview-core/schema.json b/packages/geoview-core/schema.json index 4e245b9943c..959c99496b8 100644 --- a/packages/geoview-core/schema.json +++ b/packages/geoview-core/schema.json @@ -61,7 +61,14 @@ }, "domain": { "description": "An array of values that constitute the domain.", - "type": "array" + "oneOf": [ + { + "type": "null" + }, + { + "type": "array" + } + ] } }, "required": ["name", "alias", "type", "domain"] diff --git a/packages/geoview-core/src/api/config/uuid-config-reader.ts b/packages/geoview-core/src/api/config/uuid-config-reader.ts index 910ad63d12e..3cd63d58cd4 100644 --- a/packages/geoview-core/src/api/config/uuid-config-reader.ts +++ b/packages/geoview-core/src/api/config/uuid-config-reader.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'; import { TypeJsonObject, TypeJsonArray, toJsonObject } from '@config/types/config-types'; import { CV_CONST_LAYER_TYPES } from '@config/types/config-constants'; -import { createLocalizedString } from '@/core/utils/utilities'; +import { createLocalizedString, deepMergeObjects } from '@/core/utils/utilities'; import { logger } from '@/core/utils/logger'; // The GeoChart Json object coming out of the GeoCore response @@ -58,6 +58,11 @@ export class UUIDmapConfigReader { if (layer) { const { layerType, layerEntries, name, url, id, serverType, isTimeAware } = layer; + // Get Geocore custom config layer entries values + // TODO: The proof of concept is done only for WMS layers. We need to implement other layer types after the refactor + // TODO.CONT: We need to support config for the geoviewLAyer and children layer entries... + const customGeocoreLayerConfig = this.#getGeocoreCustomLayerConfig(result, lang); + const isFeature = (url as string).indexOf('FeatureServer') > -1; if (layerType === CV_CONST_LAYER_TYPES.ESRI_DYNAMIC && !isFeature) { @@ -126,12 +131,20 @@ export class UUIDmapConfigReader { }); (geoviewLayerConfig.listOfLayerEntryConfig as TypeJsonObject[]) = (layerEntries as TypeJsonArray).map( (item): TypeJsonObject => { - return toJsonObject({ + const originalConfig = { layerId: `${item.id}`, source: { serverType: serverType === undefined ? 'mapserver' : serverType, }, - }); + }; + + // Overwrite default from geocore custom config + const mergedConfig = deepMergeObjects( + originalConfig as unknown as TypeJsonObject, + customGeocoreLayerConfig as unknown as TypeJsonObject + ); + + return mergedConfig; } ); listOfGeoviewLayerConfig.push(geoviewLayerConfig); @@ -298,6 +311,26 @@ export class UUIDmapConfigReader { return listOfGeoviewLayerConfig; } + /** + * Reads the layers config from uuid request result + * @param {AxiosResponse} result - the uuid request result + * @param {string} lang - the language to use to read results + * @returns {TypeJsonObject} the layers snippet configs + * @private + */ + static #getGeocoreCustomLayerConfig(result: AxiosResponse, lang: string): TypeJsonObject { + // If no custon geocore information + if (!result?.data || !result.data.response || !result.data.response.gcs || !Array.isArray(result.data.response.gcs)) return {}; + + // TODO: Once refactor done rename to layers + // TODO: Before moving to the new api, layers will need to link to ids inside the gsc response so we can customize group, or specific id + // Find custom layer entry configuration + const foundConfigs = result.data.response.gcs.map((gcs) => gcs?.[lang]?.layersNew as TypeJsonObject); + + return foundConfigs[0] || {}; + } + + // GV This is important, it sets the geochart config, we need to relink // TODO: Check - Commented out as not called anymore by method `getGVConfigFromUUIDs`, but maybe it should still? // /** // * Reads and parses GeoChart configs from uuid request result diff --git a/packages/geoview-core/src/api/plugin/plugin.ts b/packages/geoview-core/src/api/plugin/plugin.ts index b4f34e07d5a..05fcdf840df 100644 --- a/packages/geoview-core/src/api/plugin/plugin.ts +++ b/packages/geoview-core/src/api/plugin/plugin.ts @@ -32,7 +32,7 @@ export abstract class Plugin { // eslint-disable-next-line @typescript-eslint/no-explicit-any static loadScript(pluginId: string): Promise { return new Promise((resolve, reject) => { - const existingScript = document.getElementById(pluginId); + const existingScript = document.querySelector(`script#${pluginId}`); if (!existingScript) { // Get the main script URL diff --git a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx index a99e0a3dde1..a8c0862fdd1 100644 --- a/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx +++ b/packages/geoview-core/src/core/components/footer-bar/footer-bar.tsx @@ -116,14 +116,14 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null { // inject guide tab at last position of tabs. return Object.keys({ ...tabsList, ...{ guide: {} } }).map((tab, index) => { return { - id: `${mapId}-${tab}${index}`, + id: `${tab}`, value: index, label: `${camelCase(tab)}.title`, icon: allTabs[tab]?.icon ?? '', content: allTabs[tab]?.content ?? '', } as TypeTabs; }); - }, [memoTabs, tabsList, mapId]); + }, [memoTabs, tabsList]); /** * Calculate resize values from popover values defined in store. @@ -192,7 +192,7 @@ export function FooterBar(props: FooterBarProps): JSX.Element | null { logger.logTraceUseEffect('FOOTER-TABS - arrayOfLayerDataBatch', arrayOfLayerDataBatch, selectedTab, isCollapsed); // If we're on the details panel and the footer is collapsed - if (selectedTab === `${mapId}-details-2` && isCollapsed) { + if (selectedTab === `details` && isCollapsed) { // Uncollapse it setFooterBarIsCollapsed(false); } diff --git a/packages/geoview-core/src/core/utils/config/config-validation.ts b/packages/geoview-core/src/core/utils/config/config-validation.ts index 72817a9a24d..a626b772f91 100644 --- a/packages/geoview-core/src/core/utils/config/config-validation.ts +++ b/packages/geoview-core/src/core/utils/config/config-validation.ts @@ -387,7 +387,7 @@ export class ConfigValidation { const validateLocalizedString = (config: TypeJsonObject): void => { if (typeof config === 'object') { Object.keys(config).forEach((key) => { - if (!key.startsWith('_') && typeof config[key] === 'object') { + if (!key.startsWith('_') && config[key] !== null && typeof config[key] === 'object') { if (config?.[key]?.en || config?.[key]?.fr) { // delete empty localized strings if (!config[key].en && !config[key].fr) delete config[key]; @@ -420,7 +420,7 @@ export class ConfigValidation { const propagateLocalizedString = (config: TypeJsonObject): void => { if (typeof config === 'object') { Object.keys(config).forEach((key) => { - if (!key.startsWith('_') && typeof config[key] === 'object') { + if (!key.startsWith('_') && config[key] !== null && typeof config[key] === 'object') { // Leaving the commented line here in case a developer needs to quickly uncomment it again to troubleshoot // logger.logDebug(`Key=${key}`, config[key]); if (config?.[key]?.en || config?.[key]?.fr) diff --git a/packages/geoview-core/src/core/utils/config/reader/uuid-config-reader.ts b/packages/geoview-core/src/core/utils/config/reader/uuid-config-reader.ts index b20a02dc703..4c88314ba15 100644 --- a/packages/geoview-core/src/core/utils/config/reader/uuid-config-reader.ts +++ b/packages/geoview-core/src/core/utils/config/reader/uuid-config-reader.ts @@ -13,7 +13,7 @@ import { TypeGeoJSONLayerConfig } from '@/geo/layer/geoview-layers/vector/geojso import { TypeGeoPackageLayerConfig } from '@/geo/layer/geoview-layers/vector/geopackage'; import { TypeXYZTilesConfig } from '@/geo/layer/geoview-layers/raster/xyz-tiles'; import { TypeVectorTilesConfig } from '@/geo/layer/geoview-layers/raster/vector-tiles'; -import { createLocalizedString } from '@/core/utils/utilities'; +import { createLocalizedString, deepMergeObjects } from '@/core/utils/utilities'; import { logger } from '@/core/utils/logger'; import { WfsLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/wfs-layer-entry-config'; import { OgcFeatureLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/ogc-layer-entry-config'; @@ -83,13 +83,22 @@ export class UUIDmapConfigReader { const layer = data.layers[0]; if (layer) { + // Get RCS values const { layerType, layerEntries, name, url, id, serverType, isTimeAware } = layer; + // Remove rcs. and .[lang] from geocore response + const idClean = `${(id as string).split('.')[1]}`; + + // Get Geocore custom config layer entries values + // TODO: Modification done only for WMS and esriDynamic... If we have esriFeature, esriImage later, we will need to fix + // TODO.CONT: These 4 types are the only one stored in RCS + const customGeocoreLayerConfig = this.#getGeocoreCustomLayerConfig(result, lang); + const isFeature = (url as string).indexOf('FeatureServer') > -1; if (layerType === CONST_LAYER_TYPES.ESRI_DYNAMIC && !isFeature) { const geoviewLayerConfig: TypeEsriDynamicLayerConfig = { - geoviewLayerId: `${(id as string).split('.')[1]}`, // Remove rcs. and .[lang] from geocore response + geoviewLayerId: idClean, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.ESRI_DYNAMIC, @@ -97,7 +106,7 @@ export class UUIDmapConfigReader { listOfLayerEntryConfig: [], }; geoviewLayerConfig.listOfLayerEntryConfig = (layerEntries as TypeJsonArray).map((item): EsriDynamicLayerEntryConfig => { - const esriDynamicLayerEntryConfig = new EsriDynamicLayerEntryConfig({ + const originalConfig = { geoviewLayerConfig, schemaTag: CONST_LAYER_TYPES.ESRI_DYNAMIC, entryType: CONST_LAYER_ENTRY_TYPES.RASTER_IMAGE, @@ -105,7 +114,15 @@ export class UUIDmapConfigReader { source: { dataAccessPath: createLocalizedString(url), }, - } as unknown as EsriDynamicLayerEntryConfig); + }; + + // Overwrite default from geocore custom config + const mergedConfig = deepMergeObjects( + originalConfig as unknown as TypeJsonObject, + customGeocoreLayerConfig as unknown as TypeJsonObject + ); + const esriDynamicLayerEntryConfig = new EsriDynamicLayerEntryConfig(mergedConfig as unknown as EsriDynamicLayerEntryConfig); + return esriDynamicLayerEntryConfig; }); listOfGeoviewLayerConfig.push(geoviewLayerConfig); @@ -117,7 +134,7 @@ export class UUIDmapConfigReader { const layerId = (url as string).split('/').pop(); const geoviewLayerConfig: TypeEsriFeatureLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(serviceUrl), geoviewLayerType: CONST_LAYER_TYPES.ESRI_FEATURE, @@ -139,7 +156,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.ESRI_FEATURE) { const geoviewLayerConfig: TypeEsriFeatureLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.ESRI_FEATURE, @@ -162,7 +179,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.WMS) { const geoviewLayerConfig: TypeWMSLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.WMS, @@ -170,7 +187,7 @@ export class UUIDmapConfigReader { listOfLayerEntryConfig: [], }; geoviewLayerConfig.listOfLayerEntryConfig = (layerEntries as TypeJsonArray).map((item): OgcWmsLayerEntryConfig => { - const wmsLayerEntryConfig = new OgcWmsLayerEntryConfig({ + const originalConfig = { geoviewLayerConfig, schemaTag: CONST_LAYER_TYPES.WMS, entryType: CONST_LAYER_ENTRY_TYPES.RASTER_IMAGE, @@ -179,13 +196,21 @@ export class UUIDmapConfigReader { dataAccessPath: createLocalizedString(url), serverType: (serverType === undefined ? 'mapserver' : serverType) as TypeOfServer, }, - } as OgcWmsLayerEntryConfig); + }; + + // Overwrite default from geocore custom config + const mergedConfig = deepMergeObjects( + originalConfig as unknown as TypeJsonObject, + customGeocoreLayerConfig as unknown as TypeJsonObject + ); + const wmsLayerEntryConfig = new OgcWmsLayerEntryConfig(mergedConfig as unknown as OgcWmsLayerEntryConfig); + return wmsLayerEntryConfig; }); listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.WFS) { const geoviewLayerConfig: TypeWFSLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.WFS, @@ -209,7 +234,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.OGC_FEATURE) { const geoviewLayerConfig: TypeOgcFeatureLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.OGC_FEATURE, @@ -232,7 +257,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.GEOJSON) { const geoviewLayerConfig: TypeGeoJSONLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.GEOJSON, @@ -255,7 +280,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.XYZ_TILES) { const geoviewLayerConfig: TypeXYZTilesConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.XYZ_TILES, @@ -277,7 +302,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.VECTOR_TILES) { const geoviewLayerConfig: TypeVectorTilesConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.VECTOR_TILES, @@ -299,7 +324,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.GEOPACKAGE) { const geoviewLayerConfig: TypeGeoPackageLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), geoviewLayerType: CONST_LAYER_TYPES.GEOPACKAGE, isTimeAware: isTimeAware as boolean, @@ -321,7 +346,7 @@ export class UUIDmapConfigReader { listOfGeoviewLayerConfig.push(geoviewLayerConfig); } else if (layerType === CONST_LAYER_TYPES.IMAGE_STATIC) { const geoviewLayerConfig: TypeImageStaticLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.IMAGE_STATIC, @@ -346,7 +371,7 @@ export class UUIDmapConfigReader { // GV: Everything needed to create the geoview layer is in the URL. The layerId of the layerEntryConfig is not used, // GV: but we need to create a layerEntryConfig in the list for the layer to be displayed. const geoviewLayerConfig: TypeEsriImageLayerConfig = { - geoviewLayerId: `${id}`, + geoviewLayerId: `${idClean}`, geoviewLayerName: createLocalizedString(name), metadataAccessPath: createLocalizedString(url), geoviewLayerType: CONST_LAYER_TYPES.ESRI_IMAGE, @@ -372,6 +397,23 @@ export class UUIDmapConfigReader { return listOfGeoviewLayerConfig; } + /** + * Reads the layers config from uuid request result + * @param {AxiosResponse} result - the uuid request result + * @param {string} lang - the language to use to read results + * @returns {TypeJsonObject} the layers snippet configs + * @private + */ + static #getGeocoreCustomLayerConfig(result: AxiosResponse, lang: string): TypeJsonObject { + // If no custon geocore information + if (!result?.data || !result.data.response || !result.data.response.gcs || !Array.isArray(result.data.response.gcs)) return {}; + + // Find custom layer entry configuration + const foundConfigs = result.data.response.gcs.map((gcs) => gcs?.[lang]?.layers as TypeJsonObject); + + return foundConfigs[0] || {}; + } + /** * Reads and parses GeoChart configs from uuid request result * @param {AxiosResponse} result the uuid request result diff --git a/packages/geoview-core/src/core/utils/utilities.ts b/packages/geoview-core/src/core/utils/utilities.ts index d35a28a835b..015146b0059 100644 --- a/packages/geoview-core/src/core/utils/utilities.ts +++ b/packages/geoview-core/src/core/utils/utilities.ts @@ -44,6 +44,27 @@ export function getLocalizedMessage(localizedKey: string, language: TypeDisplayL return trans(localizedKey); } +/** + * Deep merge objects togheter. Latest object will overwrite value on previous one + * if property exist. + * + * @param {TypeJsonObject} objects - The objects to deep merge + * @returns {TypeJsonObject} The merged object + */ +export function deepMergeObjects(...objects: TypeJsonObject[]): TypeJsonObject { + const deepCopyObjects = objects.map((object) => JSON.parse(JSON.stringify(object))); + return deepCopyObjects.reduce((merged, current) => ({ ...merged, ...current }), {}); +} + +/** + * Check if an object is empty + * @param {object} obj - The object to test + * @returns true if the object is empty, false otherwise + */ +export function isObjectEmpty(obj: object): boolean { + return Object.keys(obj).length === 0; +} + /** * Get the URL of main script cgpv-main so we can access the assets * @returns {string} the URL of the main script 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 b091d65659d..928fd94e81a 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 @@ -944,11 +944,16 @@ export class EsriDynamic extends AbstractGeoViewRaster { // Convert date constants using the externalFragmentsOrder derived from the externalDateFormat // TODO: Standardize the regex across all layer types + // OLD REGEX, not working anymore, test before standardization + // ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( + // /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + // ), const searchDateEntry = [ - ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( - /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + ...filterValueToUse.matchAll( + /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/gi ), ]; + searchDateEntry.reverse(); searchDateEntry.forEach((dateFound) => { // If the date has a time zone, keep it as is, otherwise reverse its time zone by changing its sign 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 1b648fa6787..1520ec6ecd8 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 @@ -772,7 +772,7 @@ export class WMS extends AbstractGeoViewRaster { featureMember = { plain_text: { '#text': response.data } }; } if (featureMember) { - const featureInfoResult = this.#formatWmsFeatureInfoResult(featureMember, layerConfig, clickCoordinate); + const featureInfoResult = this.#formatWmsFeatureInfoResult(featureMember, clickCoordinate); return featureInfoResult; } } @@ -989,19 +989,13 @@ export class WMS extends AbstractGeoViewRaster { * Translate the get feature information result set to the TypeFeatureInfoEntry[] used by GeoView. * * @param {TypeJsonObject} featureMember An object formatted using the query syntax. - * @param {OgcWmsLayerEntryConfig} layerConfig The layer configuration. * @param {Coordinate} clickCoordinate The coordinate where the user has clicked. * * @returns {TypeFeatureInfoEntry[]} The feature info table. * @private */ // GV Layers Refactoring - Obsolete (in layers) - #formatWmsFeatureInfoResult( - featureMember: TypeJsonObject, - layerConfig: OgcWmsLayerEntryConfig, - clickCoordinate: Coordinate - ): TypeFeatureInfoEntry[] { - const outfields = layerConfig?.source?.featureInfo?.outfields; + #formatWmsFeatureInfoResult(featureMember: TypeJsonObject, clickCoordinate: Coordinate): TypeFeatureInfoEntry[] { const queryResult: TypeFeatureInfoEntry[] = []; let featureKeyCounter = 0; @@ -1044,28 +1038,7 @@ export class WMS extends AbstractGeoViewRaster { }); }; createFieldEntries(featureMember); - - if (!outfields) queryResult.push(featureInfoEntry); - else { - fieldKeyCounter = 0; - const fieldsToDelete = Object.keys(featureInfoEntry.fieldInfo).filter((fieldName) => { - if (outfields.find((outfield) => outfield.name === fieldName)) { - const fieldIndex = outfields.findIndex((outfield) => outfield.name === fieldName); - featureInfoEntry.fieldInfo[fieldName]!.fieldKey = fieldKeyCounter++; - featureInfoEntry.fieldInfo[fieldName]!.alias = outfields![fieldIndex].alias; - featureInfoEntry.fieldInfo[fieldName]!.dataType = outfields![fieldIndex].type; - return false; // keep this entry - } - - return true; // delete this entry - }); - - fieldsToDelete.forEach((entryToDelete) => { - delete featureInfoEntry.fieldInfo[entryToDelete]; - }); - - queryResult.push(featureInfoEntry); - } + queryResult.push(featureInfoEntry); return queryResult; } 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 1dddde08b9d..76925624559 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 @@ -583,11 +583,17 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { if (combineLegendFilter) layerConfig.layerFilter = filter; // Convert date constants using the externalFragmentsOrder derived from the externalDateFormat + // TODO: Standardize the regex across all layer types + // OLD REGEX, not working anymore, test before standardization + // ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( + // /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + // ), const searchDateEntry = [ - ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( - /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + ...filterValueToUse.matchAll( + /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/gi ), ]; + searchDateEntry.reverse(); searchDateEntry.forEach((dateFound) => { // If the date has a time zone, keep it as is, otherwise reverse its time zone by changing its sign diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 1e55d707c49..4f415f4c79c 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -691,11 +691,16 @@ export class GVEsriDynamic extends AbstractGVRaster { // Convert date constants using the externalFragmentsOrder derived from the externalDateFormat // TODO: Standardize the regex across all layer types + // OLD REGEX, not working anymore, test before standardization + // ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( + // /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + // ), const searchDateEntry = [ - ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( - /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + ...filterValueToUse.matchAll( + /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/gi ), ]; + searchDateEntry.reverse(); searchDateEntry.forEach((dateFound) => { // If the date has a time zone, keep it as is, otherwise reverse its time zone by changing its sign diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-wms.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-wms.ts index 1c2dd8a3ed4..c1039b1b3aa 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-wms.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-wms.ts @@ -169,7 +169,7 @@ export class GVWMS extends AbstractGVRaster { } } else featureMember = { plain_text: { '#text': response.data } }; if (featureMember) { - const featureInfoResult = this.#formatWmsFeatureInfoResult(featureMember, layerConfig, clickCoordinate); + const featureInfoResult = this.#formatWmsFeatureInfoResult(featureMember, clickCoordinate); return featureInfoResult; } } @@ -397,17 +397,11 @@ export class GVWMS extends AbstractGVRaster { /** * Translates the get feature information result set to the TypeFeatureInfoEntry[] used by GeoView. * @param {TypeJsonObject} featureMember - An object formatted using the query syntax. - * @param {OgcWmsLayerEntryConfig} layerConfig - The layer configuration. * @param {Coordinate} clickCoordinate - The coordinate where the user has clicked. * @returns {TypeFeatureInfoEntry[]} The feature info table. * @private */ - #formatWmsFeatureInfoResult( - featureMember: TypeJsonObject, - layerConfig: OgcWmsLayerEntryConfig, - clickCoordinate: Coordinate - ): TypeFeatureInfoEntry[] { - const outfields = layerConfig?.source?.featureInfo?.outfields; + #formatWmsFeatureInfoResult(featureMember: TypeJsonObject, clickCoordinate: Coordinate): TypeFeatureInfoEntry[] { const queryResult: TypeFeatureInfoEntry[] = []; let featureKeyCounter = 0; @@ -450,28 +444,7 @@ export class GVWMS extends AbstractGVRaster { }); }; createFieldEntries(featureMember); - - if (!outfields) queryResult.push(featureInfoEntry); - else { - fieldKeyCounter = 0; - const fieldsToDelete = Object.keys(featureInfoEntry.fieldInfo).filter((fieldName) => { - if (outfields.find((outfield) => outfield.name === fieldName)) { - const fieldIndex = outfields.findIndex((outfield) => outfield.name === fieldName); - featureInfoEntry.fieldInfo[fieldName]!.fieldKey = fieldKeyCounter++; - featureInfoEntry.fieldInfo[fieldName]!.alias = outfields![fieldIndex].alias; - featureInfoEntry.fieldInfo[fieldName]!.dataType = outfields![fieldIndex].type; - return false; // keep this entry - } - - return true; // delete this entry - }); - - fieldsToDelete.forEach((entryToDelete) => { - delete featureInfoEntry.fieldInfo[entryToDelete]; - }); - - queryResult.push(featureInfoEntry); - } + queryResult.push(featureInfoEntry); return queryResult; } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts b/packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts index e5c94cee2c2..ae87356e7f7 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/vector/abstract-gv-vector.ts @@ -208,11 +208,17 @@ export abstract class AbstractGVVector extends AbstractGVLayer { if (combineLegendFilter) layerConfig.layerFilter = filter; // Convert date constants using the externalFragmentsOrder derived from the externalDateFormat + // TODO: Standardize the regex across all layer types + // OLD REGEX, not working anymore, test before standardization + // ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( + // /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + // ), const searchDateEntry = [ - ...`${filterValueToUse?.replaceAll(/\s{2,}/g, ' ').trim()} `.matchAll( - /(?<=^date\b\s')[\d/\-T\s:+Z]{4,25}(?=')|(?<=[(\s]date\b\s')[\d/\-T\s:+Z]{4,25}(?=')/gi + ...filterValueToUse.matchAll( + /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/gi ), ]; + searchDateEntry.reverse(); searchDateEntry.forEach((dateFound) => { // If the date has a time zone, keep it as is, otherwise reverse its time zone by changing its sign 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 0ad069af5bb..2879434daba 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 @@ -2,6 +2,8 @@ import EventHelper, { EventDelegateBase } from '@/api/events/event-helper'; import { QueryType, TypeFeatureInfoEntry, + TypeFeatureInfoLayerConfig, + TypeLayerEntryConfig, TypeLayerStatus, TypeLocation, TypeResultSet, @@ -444,6 +446,50 @@ export abstract class AbstractLayerSet { return !((layer.getLayerConfig(layerPath) as AbstractBaseLayerEntryConfig)?.initialSettings?.states?.queryable === false); } + /** + * Align records with informatiom provided by OutFields from layer config. + * This will update fields in and delete unwanted fields from the arrayOfRecords + * @param {TypeLayerEntryConfig} layerPath - Path of the layer to get config from. + * @param {TypeFeatureInfoEntry[]} arrayOfRecords - Features to delete fields from. + * @protected + * @static + */ + protected static alignRecordsWithOutFields(layerEntryConfig: TypeLayerEntryConfig, arrayOfRecords: TypeFeatureInfoEntry[]): void { + // If source featureInfo is provided, continue + if (layerEntryConfig.source && layerEntryConfig.source.featureInfo) { + const sourceFeatureInfo = layerEntryConfig.source!.featureInfo as TypeFeatureInfoLayerConfig; + + // If outFields is provided, compare record fields with outFields to remove unwanted one + // If there is no outFields, this will be created in the next function patchMissingMetadataIfNecessary + if (sourceFeatureInfo.outfields) { + const outFields = sourceFeatureInfo.outfields; + + // Loop the array of records to delete fields or align fields info for each record + arrayOfRecords.forEach((recordOriginal) => { + // Create a copy to avoid the no param reassign ESLint rule + const record = { ...recordOriginal }; + let fieldKeyCounter = 0; + + const fieldsToDelete = Object.keys(record.fieldInfo).filter((fieldName) => { + if (outFields.find((outfield) => outfield.name === fieldName)) { + const fieldIndex = outFields.findIndex((outfield) => outfield.name === fieldName); + record.fieldInfo[fieldName]!.fieldKey = fieldKeyCounter++; + record.fieldInfo[fieldName]!.alias = outFields![fieldIndex].alias; + record.fieldInfo[fieldName]!.dataType = outFields![fieldIndex].type; + return false; // keep this entry + } + + return true; // delete this entry + }); + + fieldsToDelete.forEach((entryToDelete) => { + delete record.fieldInfo[entryToDelete]; + }); + }); + } + } + } + /** * Emits an event to all registered handlers. * @param {LayerSetUpdatedEvent} event - The event to emit 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 b88a7fb844a..e2d539d7e43 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 @@ -1,5 +1,5 @@ import { DataTableEventProcessor } from '@/api/event-processors/event-processor-children/data-table-event-processor'; -import { QueryType } from '@/geo/map/map-schema-types'; +import { QueryType, TypeLayerEntryConfig } from '@/geo/map/map-schema-types'; import { AbstractGeoViewLayer } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; import { AbstractGVLayer } from '../gv-layers/abstract-gv-layer'; import { AbstractBaseLayer } from '../gv-layers/abstract-base-layer'; @@ -126,6 +126,10 @@ export class AllFeatureInfoLayerSet extends AbstractLayerSet { // Wait for promise to resolve const arrayOfRecords = await promiseResult; + // Use the response to align arrayOfRecords fields with layerConfig fields + if (arrayOfRecords?.length) + AbstractLayerSet.alignRecordsWithOutFields(this.layerApi.getLayerEntryConfig(layerPath) as TypeLayerEntryConfig, arrayOfRecords); + // Keep the features retrieved this.resultSet[layerPath].features = arrayOfRecords; 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 20499d3f0ea..7fccf9eb363 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 @@ -154,8 +154,15 @@ export class FeatureInfoLayerSet extends AbstractLayerSet { // When the promise is done, propagate to store promiseResult .then((arrayOfRecords) => { + // Use the response to align arrayOfRecords fields with layerConfig fields + if (arrayOfRecords?.length) + AbstractLayerSet.alignRecordsWithOutFields( + this.layerApi.getLayerEntryConfig(layerPath) as TypeLayerEntryConfig, + arrayOfRecords + ); + // Use the response to possibly patch the layer config metadata - if (arrayOfRecords?.length) this.patchMissingMetadataIfNecessary(layerPath, arrayOfRecords[0]); + if (arrayOfRecords?.length) this.#patchMissingMetadataIfNecessary(layerPath, arrayOfRecords[0]); // Keep the features retrieved this.resultSet[layerPath].features = arrayOfRecords; @@ -250,9 +257,9 @@ export class FeatureInfoLayerSet extends AbstractLayerSet { * Updates outfields, aliases and data types from query result if not provided in metadata * @param {string} layerPath - Path of the layer to update. * @param {TypeFeatureInfoEntry} record - Feature info to parse. + * @private */ - patchMissingMetadataIfNecessary(layerPath: string, record: TypeFeatureInfoEntry): void { - // TODO Make sure this works with solution to #2259 + #patchMissingMetadataIfNecessary(layerPath: string, record: TypeFeatureInfoEntry): void { // Set up feature info for layers that did not include it in the metadata const layerEntryConfig = this.layerApi.getLayerEntryConfig(layerPath) as TypeLayerEntryConfig; diff --git a/packages/geoview-geochart/src/index.tsx b/packages/geoview-geochart/src/index.tsx index f7891d338d5..5a6b44e5607 100644 --- a/packages/geoview-geochart/src/index.tsx +++ b/packages/geoview-geochart/src/index.tsx @@ -4,6 +4,7 @@ import { TypeTabs } from 'geoview-core/src/ui/tabs/tabs'; import { ChartIcon } from 'geoview-core/src/ui/icons'; import { GeochartEventProcessor } from 'geoview-core/src/api/event-processors/event-processor-children/geochart-event-processor'; +import { isObjectEmpty } from 'geoview-core/src/core/utils/utilities'; import schema from '../schema.json'; import defaultConfig from '../default-config-geochart.json'; import { GeoChartPanel } from './geochart-panel'; @@ -61,8 +62,8 @@ class GeoChartFooterPlugin extends FooterPlugin { * Overrides the addition of the GeoChart Footer Plugin to make sure to set the chart configs into the store. */ override onAdd(): void { - // Initialize the store with geochart provided configuration - GeochartEventProcessor.setGeochartCharts(this.pluginProps.mapId, this.configObj.charts); + // Initialize the store with geochart provided configuration if there is one + if (!isObjectEmpty(this.configObj.charts)) GeochartEventProcessor.setGeochartCharts(this.pluginProps.mapId, this.configObj.charts); // Call parent super.onAdd();