diff --git a/packages/geoview-core/public/templates/config-sandbox.html b/packages/geoview-core/public/templates/config-sandbox.html index f578833c74d..92f468dcb21 100644 --- a/packages/geoview-core/public/templates/config-sandbox.html +++ b/packages/geoview-core/public/templates/config-sandbox.html @@ -225,6 +225,8 @@

Sandbox Configuration

+ +      URL:  @@ -318,11 +320,20 @@

Sandbox Configuration

GeoView Layer Type: - + + + + + + + + +      URL:  @@ -446,7 +457,7 @@

Sanbox Map

generateLayerPathErrorFlag = (listOfLayerEntryConfig, buffer = '', prefix = '') => { listOfLayerEntryConfig.forEach((layer) => { buffer = buffer ? `${buffer}\n` : ''; - buffer = `${buffer}${layer.getLayerPath()}: ${layer.getErrorDetectedFlag() ? 'ERROR' : 'Ok'}`; + buffer = `${buffer}${layer?.getLayerPath?.() || layer.layerName}: ${layer?.getErrorDetectedFlag?.() ? 'ERROR' : 'Ok'}`; if (layer.isLayerGroup) buffer = generateLayerPathErrorFlag(layer.listOfLayerEntryConfig, buffer, prefix ? `${prefix}.${layer.layerId}` : layer.layerId); }); return buffer; @@ -656,6 +667,8 @@

Sanbox Map

esriDynamic: 'https://maps-cartes.ec.gc.ca/arcgis/rest/services/CESI/MapServer/', esriFeature: 'https://maps-cartes.services.geo.ca/server_serveur/rest/services/NRCan/Temporal_Test_Bed_en/MapServer/', ogcWms: 'https://geo.weather.gc.ca/geomet', + esriImage: 'NOT IMPLEMENTED', // 'https://www5.agr.gc.ca/atlas/rest/services/imageservices/annual_crop_inventory_2022/ImageServer', + GeoJSON: 'NOT IMPLEMENTED', // 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/geojson/metadata.json', }; // Get the GeoView Layer Type drop-down object used to select the type of layer. @@ -685,8 +698,8 @@

Sanbox Map

// Type drop-down with the resulting value. serviceUrlAreaTab2.addEventListener('keyup', (e) => { if (sampleUrlTab2.checked) return; - GeoviewLayerTypeDropDownTab2.value = cgpv.api.config.guessLayerType(serviceUrlAreaTab2.value); - if (!GeoviewLayerTypeDropDownTab2.value) GeoviewLayerTypeDropDownTab2.value = 'unknown'; + const value = cgpv.api.config.guessLayerType(serviceUrlAreaTab2.value); + GeoviewLayerTypeDropDownTab2.value = value || 'unknown'; }); // Initialise the GeoView Layer Type drop-down value and set input field accordingly @@ -843,6 +856,15 @@

Sanbox Map

esriDynamic: 'https://maps-cartes.ec.gc.ca/arcgis/rest/services/CESI/MapServer/', esriFeature: 'https://maps-cartes.services.geo.ca/server_serveur/rest/services/NRCan/Temporal_Test_Bed_en/MapServer/', ogcWms: 'https://geo.weather.gc.ca/geomet', + esriImage: 'https://www5.agr.gc.ca/atlas/rest/services/imageservices/annual_crop_inventory_2022/ImageServer', + GeoJSON: 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/geojson/metadata.json', + CSV: 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/csv-files/Station_List_Minus_HQ-MELCC.csv', + ogcFeature: 'https://b6ryuvakk5.execute-api.us-east-1.amazonaws.com/dev/collections', + GeoPackage: 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/geopackages/rivers.gpkg', + ogcWfs: 'https://ahocevar.com/geoserver/wfs?REQUEST=GetCapabilities&VERSION=2.0.0&SERVICE=WFS', + xyzTiles: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', + vectorTiles: 'https://tiles.arcgis.com/tiles/HsjBaDykC1mjhXz9/arcgis/rest/services/CBMT3978_v11/VectorTileServer/tile/{z}/{y}/{x}.pbf', + imageStatic: 'https://datacube-prod-data-public.s3.ca-central-1.amazonaws.com/store/imagery/aerial/napl/napl-ring-of-fire/napl-ring-of-fire-1954-08-07-60k-thumbnail.png', }; // Get the GeoView Layer Type drop-down object used to select the type of layer. @@ -905,23 +927,31 @@

Sanbox Map

return item.trim(); }) : []; - // Call the ConfigApi to get the Layer Tree - const metadataLayerTree = await cgpv.api.config.createMetadataLayerTree( - serviceUrlAreaTab4.value.trim(), - layerTypeTab4, - layerListTab4, - languageTab4 - ); - // Output instanciation result - metadataLayerTreeString = `${JSON.stringify(metadataLayerTree, (key, value) => { - if (['', 'layerId', 'layerName', 'isLayerGroup', 'listOfLayerEntryConfig'].includes(key)) return value; - if (/^\d+$/.test(key)) return value; - return undefined; - }, 2)}\n`; - layerPathErrorFlag = generateLayerPathErrorFlag(metadataLayerTree); - - if (metadataLayerTree) printMessage(messageTab4, 'Layer tree is valid'); - else printMessage(messageTab4, 'Cannot generate layer tree, see console for details...', 'error'); + try { + // Call the ConfigApi to get the Layer Tree + const metadataLayerTree = await cgpv.api.config.createMetadataLayerTree( + serviceUrlAreaTab4.value.trim(), + layerTypeTab4, + layerListTab4, + languageTab4 + ); + + // Output instanciation result + metadataLayerTreeString = `${JSON.stringify(metadataLayerTree, (key, value) => { + // Only display essential information + if (['', 'layerId', 'layerName', 'isLayerGroup', 'listOfLayerEntryConfig'].includes(key)) return value; + if (/^\d+$/.test(key)) return value; + return undefined; + }, 2)}\n`; + layerPathErrorFlag = generateLayerPathErrorFlag(metadataLayerTree); + + printMessage(messageTab4, 'Layer tree is valid'); + } catch (error) { + metadataLayerTreeString = ''; + layerPathErrorFlag = '[]'; + printMessage(messageTab4, 'Cannot generate layer tree, see console for details...', 'error'); + cgpv.logger.logError('Cannot generate layer tree, see console for details\n', error); + } elementSelectorTab4.dispatchEvent(new Event('change')); }); diff --git a/packages/geoview-core/src/api/config/config-api.ts b/packages/geoview-core/src/api/config/config-api.ts index 77acb1c302f..4c65c22eb83 100644 --- a/packages/geoview-core/src/api/config/config-api.ts +++ b/packages/geoview-core/src/api/config/config-api.ts @@ -1,4 +1,5 @@ import cloneDeep from 'lodash/cloneDeep'; +import mergeWith from 'lodash/mergeWith'; import { CV_DEFAULT_MAP_FEATURE_CONFIG, CV_CONFIG_GEOCORE_TYPE, CV_CONST_LAYER_TYPES } from '@config/types/config-constants'; import { TypeJsonValue, TypeJsonObject, toJsonObject, TypeJsonArray, Cast } from '@config/types/config-types'; @@ -12,7 +13,15 @@ import { } from '@config/types/map-schema-types'; import { MapConfigError } from '@config/types/classes/config-exceptions'; -import { generateId, isJsonString, removeCommentsFromJSON } from '@/core/utils/utilities'; +import { + createLocalizedString, + findPropertyNameByRegex, + generateId, + getXMLHttpRequest, + isJsonString, + removeCommentsFromJSON, + xmlToJson, +} from '@/core/utils/utilities'; import { logger } from '@/core//utils/logger'; /** @@ -486,7 +495,7 @@ export class ConfigApi { geoviewLayerId: generateId(), geoviewLayerName: { en: 'unknown', fr: 'inconnu' }, geoviewLayerType: layerType, - metadataAccessPath: { en: serviceAccessString, fr: serviceAccessString }, + metadataAccessPath: createLocalizedString(serviceAccessString), listOfLayerEntryConfig: listOfLayerId.map((layerId) => { return { layerId }; }), @@ -515,10 +524,118 @@ export class ConfigApi { listOfLayerId: TypeJsonArray = [], language: TypeDisplayLanguage = 'en' ): Promise { - const geoviewLayerConfig = await ConfigApi.createLayerConfig(serviceAccessString, layerType, listOfLayerId, language); + // GV: TEMPORARY SECTION TO BE DELETED WHEN ALL LAYER TYPES ARE IMPLEMENTED + // GV: THE CODE IN THIS SECTION IS NOT PERMANANT BECAUSE THE CORRESPONDING + // GV: GEOVIEW LAYER CLASSES HAVE NOT YET BEEN IMPLEMENTED. + // GV: BEGINNING OF TEMPORARY SECTION + async function fetchJsonMetadata(url: string): Promise { + const response = await fetch(`${url}?f=json`); + return response.json(); + } + + async function fetchXmlMetadata(url: string): Promise { + let metadataUrl = url; + // check if url contains metadata parameters for the getCapabilities request and reformat the urls + const getCapabilitiesUrl = + metadataUrl!.indexOf('?') > -1 ? metadataUrl.substring(metadataUrl!.indexOf('?')) : `?service=WFS&request=GetCapabilities`; + metadataUrl = metadataUrl!.indexOf('?') > -1 ? metadataUrl.substring(0, metadataUrl!.indexOf('?')) : metadataUrl; + if (metadataUrl) { + const metadataString = await getXMLHttpRequest(`${metadataUrl}${getCapabilitiesUrl}`); + if (metadataString === '{}') throw new MapConfigError('Unable to build metadata layer tree (empty metadata).'); + else { + // need to pass a xmldom to xmlToJson + const xmlDOMCapabilities = new DOMParser().parseFromString(metadataString, 'text/xml'); + const xmlJsonCapabilities = xmlToJson(xmlDOMCapabilities); + const capabilitiesObject = findPropertyNameByRegex(xmlJsonCapabilities, /(?:WFS_Capabilities)/); + return capabilitiesObject as TypeJsonObject; + } + } else throw new MapConfigError('Unable to build metadata layer tree (empty metadata url).'); + } + + let jsonData: TypeJsonObject; + switch (layerType) { + case 'ogcWfs': + jsonData = (await fetchXmlMetadata(serviceAccessString))?.FeatureTypeList?.FeatureType; + if (Array.isArray(jsonData)) + return (jsonData as TypeJsonArray).map((layer) => { + return Cast({ + layerId: layer.Name['#text'], + layerName: layer.Title['#text'], + }); + }); + return []; + break; + case 'ogcFeature': + jsonData = await fetchJsonMetadata(serviceAccessString); + if (jsonData.collections) + return (jsonData.collections as TypeJsonArray).map((layer) => { + return Cast({ + layerId: layer.id, + layerName: layer.title, + }); + }); + if (jsonData.id) + return [ + Cast({ + layerId: jsonData.id, + layerName: jsonData.title, + }), + ]; + return []; + break; + case 'esriImage': + jsonData = await fetchJsonMetadata(serviceAccessString); + if (jsonData.name) + return [ + Cast({ + layerId: jsonData.name, + layerName: jsonData.name, + }), + ]; + return []; + break; + case 'GeoJSON': + if ( + serviceAccessString.toLowerCase().split('?')[0].endsWith('.json') || + serviceAccessString.toLowerCase().split('?')[0].endsWith('.geojson') + ) { + jsonData = await fetchJsonMetadata(serviceAccessString.split('?')[0]); + jsonData = mergeWith(jsonData, cloneDeep(jsonData), (property, sourceValue) => { + if (property.en || property.fr) return sourceValue[language] || sourceValue.en || sourceValue.fr; + return undefined; + }); + return Cast(jsonData.listOfLayerEntryConfig); + } + return []; + break; + case 'CSV': + case 'xyzTiles': + case 'imageStatic': + case 'vectorTiles': + case 'GeoPackage': + return []; + break; + default: + break; + } + + // GV: END OF TEMPORARY SECTION + + const geoviewLayerConfig = await ConfigApi.createLayerConfig(serviceAccessString, layerType, [], language); + if (geoviewLayerConfig && !geoviewLayerConfig.getErrorDetectedFlag()) { + // set layer tree creation filter (only the layerIds specified will be retained). + // If an empty Array [] is used, the layer tree will be built using the service metadata. + geoviewLayerConfig.setMetadataLayerTree( + Cast( + listOfLayerId.map((layerId) => { + return { layerId }; + }) + ) + ); + await geoviewLayerConfig.fetchServiceMetadata(); - if (!geoviewLayerConfig.getErrorDetectedFlag()) return geoviewLayerConfig.getMetadataLayerTree(); + if (!geoviewLayerConfig.getErrorDetectedFlag()) return geoviewLayerConfig.getMetadataLayerTree()!; } throw new MapConfigError('Unable to build metadata layer tree.'); } diff --git a/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-esri-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-esri-layer-config.ts index 0ba5b0b7ad4..47e76d6bff6 100644 --- a/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-esri-layer-config.ts +++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-esri-layer-config.ts @@ -1,12 +1,12 @@ -import { toJsonObject, TypeJsonObject, TypeJsonArray } from '@config/types/config-types'; +import { toJsonObject, TypeJsonObject, TypeJsonArray, Cast } from '@config/types/config-types'; import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config'; import { TypeDisplayLanguage, TypeStyleGeometry } from '@config/types/map-schema-types'; import { EsriGroupLayerConfig } from '@config/types/classes/sub-layer-config/group-node/esri-group-layer-config'; import { layerEntryIsGroupLayer } from '@config/types/type-guards'; -import { ConfigError, GeoviewLayerInvalidParameterError } from '@config/types/classes/config-exceptions'; +import { ConfigError, GeoviewLayerConfigError, GeoviewLayerInvalidParameterError } from '@config/types/classes/config-exceptions'; import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class'; -import { getXMLHttpRequest } from '@/core/utils/utilities'; +import { createLocalizedString, getXMLHttpRequest } from '@/core/utils/utilities'; import { logger } from '@/core/utils/logger'; // ======================== @@ -53,37 +53,41 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye // #region OVERRIDE /** * Get the service metadata from the metadataAccessPath and store it in a protected property of the geoview layer. - * @override + * Verify that all sublayers defined in the listOfLayerEntryConfig exist in the metadata and fetch all sublayers metadata. + * If the metadata layer tree property is defined, build it using the service metadata. + * @override @async */ override async fetchServiceMetadata(): Promise { - const metadataString = await getXMLHttpRequest(`${this.metadataAccessPath}?f=json`); - if (metadataString !== '{}') { - let jsonMetadata: TypeJsonObject; - try { - // On rare occasions, the value returned is not a JSON string, but rather an HTML string, which is an error. - jsonMetadata = JSON.parse(metadataString); - } catch (error) { - jsonMetadata = toJsonObject({ error }); - } - // Other than the error generated above, if the returned JSON object is valid and contains the error property, something went wrong - if ('error' in jsonMetadata) { - // In the event of a service metadata reading error, we report the geoview layer and all its sublayers as being in error. - this.setErrorDetectedFlag(); - this.setErrorDetectedFlagForAllLayers(this.listOfLayerEntryConfig); - logger.logError(`Error detected while reading ESRI metadata for geoview layer ${this.geoviewLayerId}.`, jsonMetadata.error); - } else { - this.setServiceMetadata(jsonMetadata); + try { + const metadataString = await getXMLHttpRequest(`${this.metadataAccessPath}?f=json`); + if (metadataString && metadataString !== '{}') { + let jsonMetadata: TypeJsonObject; + try { + // On rare occasions, the value returned is not a JSON string, but rather an HTML string, which is an error. + jsonMetadata = JSON.parse(metadataString); + } catch (error) { + logger.logError('The service metadata request returned an invalid JSON string.\n', error); + throw new GeoviewLayerConfigError('Invalid JSON string'); + } + // Other than the error generated above, if the returned JSON object is valid and contains the error property, something went wrong + if ('error' in jsonMetadata) { + logger.logError('The service metadata request returned an an error object.\n', jsonMetadata.error); + throw new GeoviewLayerConfigError('See error description above'); + } else { + this.setServiceMetadata(jsonMetadata); + this.listOfLayerEntryConfig = this.#processListOfLayerEntryConfig(this.listOfLayerEntryConfig); + await this.fetchListOfLayerMetadata(); - this.listOfLayerEntryConfig = this.#processListOfLayerEntryConfig(this.listOfLayerEntryConfig); - // When a list of layer entries is specified, the layer tree is the same as the resulting listOfLayerEntryConfig of the geoview instance. - // Otherwise, a layer tree is built using all the layers that compose the metadata. - this.setMetadataLayerTree(this.listOfLayerEntryConfig.length ? this.listOfLayerEntryConfig : this.createLayerTree()); - await this.fetchListOfLayerMetadata(); + await this.#createLayerTree(); + } + } else { + throw new GeoviewLayerConfigError('An empty metadata object was returned'); } - } else { + } catch (error) { + // In the event of a service metadata reading error, we report the geoview layer and all its sublayers as being in error. this.setErrorDetectedFlag(); this.setErrorDetectedFlagForAllLayers(this.listOfLayerEntryConfig); - logger.logError(`Error detected while reading ESRI metadata for geoview layer ${this.geoviewLayerId}. An empty object was returned.`); + logger.logError(`Error detected while reading ESRI metadata for geoview layer ${this.geoviewLayerId}.\n`, error); } } // #endregion OVERRIDE @@ -145,7 +149,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye if (layerFound && layerFound.type !== 'Group Layer') { const layerConfig = toJsonObject({ layerId: layerFound.id.toString(), - layerName: { en: layerFound.name, fr: layerFound.name }, + layerName: createLocalizedString(layerFound.name), geometryType: AbstractGeoviewEsriLayerConfig.convertEsriGeometryTypeToOLGeometryType(layerFound.geometryType as string), }); return this.createLeafNode(layerConfig, this.getLanguage(), this, parentNode)!; @@ -174,7 +178,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye accumulator.push( toJsonObject({ layerId: layer.id.toString(), - layerName: { en: layer.name, fr: layer.name }, + layerName: createLocalizedString(layer.name), geometryType: AbstractGeoviewEsriLayerConfig.convertEsriGeometryTypeToOLGeometryType(layer.geometryType as string), }) ); @@ -185,7 +189,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye return toJsonObject({ layerId: parentId === -1 ? groupName : `${parentId}`, - layerName: { en: groupName, fr: groupName }, + layerName: createLocalizedString(groupName), isLayerGroup: true, listOfLayerEntryConfig, }); @@ -217,13 +221,54 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye } } + /** + * Create the layer tree associated to the GeoView layer if the layer tree filter stored in the metadataLayerTree private property + * is set. + * @private + */ + async #createLayerTree(): Promise { + let layerTreeFilter = this.getMetadataLayerTree(); + if (layerTreeFilter !== undefined) { + if (layerTreeFilter.length === 0) { + this.setMetadataLayerTree(this.#processListOfLayerEntryConfig(this.#createLayerTreeFromServiceMetadata())); + } else { + if (layerTreeFilter.length > 1) { + layerTreeFilter = [ + Cast({ + layerId: this.geoviewLayerId, + layerName: createLocalizedString(this.geoviewLayerName), + isLayerGroup: true, + listOfLayerEntryConfig: layerTreeFilter, + }), + ]; + } + + // Instanciate the sublayer list. + layerTreeFilter = layerTreeFilter + ?.map((layerFilter) => { + if (layerEntryIsGroupLayer(layerFilter)) + return this.createGroupNode(Cast(layerFilter), this.getLanguage(), this); + return this.createLeafNode(Cast(layerFilter), this.getLanguage(), this); + }) + // When a sublayer cannot be created, the value returned is undefined. These values will be filtered. + ?.filter((subLayerConfig) => { + return subLayerConfig; + }) as EntryConfigBaseClass[]; + + this.applyDefaultValues(); + this.setMetadataLayerTree(this.#processListOfLayerEntryConfig(layerTreeFilter)); + } + await this.fetchListOfLayerMetadata(this.getMetadataLayerTree()); + } + } + /** * Create the layer tree using the service metadata. * * @returns {TypeJsonObject[]} The layer tree created from the metadata. - * @protected + * @private */ - protected createLayerTree(): EntryConfigBaseClass[] { + #createLayerTreeFromServiceMetadata(): EntryConfigBaseClass[] { const layers = this.getServiceMetadata().layers as TypeJsonArray; if (layers.length > 1) { const groupName = this.getServiceMetadata().mapName as string; @@ -235,7 +280,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye this.createLeafNode( toJsonObject({ layerId: layers[0].id.toString(), - layerName: { en: layers[0].name, fr: layers[0].name }, + layerName: createLocalizedString(layers[0].name)!, geometryType: AbstractGeoviewEsriLayerConfig.convertEsriGeometryTypeToOLGeometryType(layers[0].geometryType as string), }), this.getLanguage(), diff --git a/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-layer-config.ts index a489c19c52c..f24308075b6 100644 --- a/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-layer-config.ts +++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/abstract-geoview-layer-config.ts @@ -32,8 +32,13 @@ export abstract class AbstractGeoviewLayerConfig { /** The metadata returned by the service endpoint. */ #serviceMetadata: TypeJsonObject = {}; - /** The metadata layer tree definition */ - #metadataLayerTree: EntryConfigBaseClass[] = []; + /** + * Before the call to fetchServiceMetadata, this property contains the tree filter. The value specified will guide the layer + * tree process. if the value is undefined, the layer tree will not be created. if it is an empty array, The layer tree will + * be created for all layers found in the service metadata. If the array is not empty, only the layerIds specified will be + * retained. When the fetchServiceMetadata call returns, this property is undefined or it contains a layer tree. + */ + #metadataLayerTree?: EntryConfigBaseClass[]; // #endregion PRIVATE PROPERTIES // ========================= @@ -208,16 +213,6 @@ export abstract class AbstractGeoviewLayerConfig { // ================= // #region PROTECTED - /** - * @protected - * The setter method that sets the metadataLayerTree private property. - * - * @param {TypeJsonObject} metadataLayerTree The GeoView service metadata. - */ - protected setMetadataLayerTree(metadataLayerTree: EntryConfigBaseClass[]): void { - this.#metadataLayerTree = metadataLayerTree; - } - /** * @protected * The getter method that returns the language used to create the geoview layer. @@ -229,14 +224,16 @@ export abstract class AbstractGeoviewLayerConfig { } /** - * Fetch the metadata of all layer entry configurations defined in the list of layer entry config. + * Fetch the metadata of all layer entry configurations defined in the list of layer entry config + * or the ressulting layer tree. * * @returns {Promise} A promise that will resolve when the process has completed. * @protected @async */ - protected async fetchListOfLayerMetadata(): Promise { + protected async fetchListOfLayerMetadata(layerTreeFilter: EntryConfigBaseClass[] | undefined = undefined): Promise { // The root of the GeoView layer tree is an array that contains only one node. - const rootLayer = this.listOfLayerEntryConfig[0]; + // If the layer tree is provided, use it. Otherwise use the list of layer entry config. + const rootLayer = layerTreeFilter ? layerTreeFilter[0] : this.listOfLayerEntryConfig[0]; try { if (rootLayer) { @@ -279,10 +276,19 @@ export abstract class AbstractGeoviewLayerConfig { * * @returns {EntryConfigBaseClass[]} The metadata layer tree. */ - getMetadataLayerTree(): EntryConfigBaseClass[] { + getMetadataLayerTree(): EntryConfigBaseClass[] | undefined { return this.#metadataLayerTree; } + /** + * The setter method that sets the metadataLayerTree private property. + * + * @param {TypeJsonObject} metadataLayerTree The GeoView service metadata. + */ + setMetadataLayerTree(metadataLayerTree: EntryConfigBaseClass[]): void { + this.#metadataLayerTree = metadataLayerTree; + } + /** * The getter method that returns the errorDetected flag. * 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 87931450940..2a0a7cf6468 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 @@ -3,7 +3,7 @@ import WMSCapabilities from 'ol/format/WMSCapabilities'; import { CV_CONST_LAYER_TYPES, CV_GEOVIEW_SCHEMA_PATH } from '@config/types/config-constants'; import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config'; import { WmsGroupLayerConfig } from '@config/types/classes/sub-layer-config/group-node/wms-group-layer-config'; -import { toJsonObject, TypeJsonArray, TypeJsonObject } from '@config/types/config-types'; +import { Cast, toJsonObject, TypeJsonArray, TypeJsonObject } from '@config/types/config-types'; import { TypeDisplayLanguage } from '@config/types/map-schema-types'; import { WmsLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/raster/wms-layer-entry-config'; import { EntryConfigBaseClass } from '@config/types/classes/sub-layer-config/entry-config-base-class'; @@ -11,7 +11,7 @@ import { layerEntryIsGroupLayer } from '@config/types/type-guards'; import { ConfigError, GeoviewLayerConfigError, GeoviewLayerInvalidParameterError } from '@config/types/classes/config-exceptions'; import { logger } from '@/core/utils/logger'; -import { xmlToJson } from '@/core/utils/utilities'; +import { createLocalizedString, xmlToJson } from '@/core/utils/utilities'; export type TypeWmsLayerNode = WmsGroupLayerConfig | WmsLayerEntryConfig; @@ -32,8 +32,8 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { /** The layer entries to use from the GeoView layer. */ declare listOfLayerEntryConfig: TypeWmsLayerNode[]; - // #endregion PROPERTIES + // =================== // #region CONSTRUCTOR /** @@ -44,17 +44,38 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { */ constructor(geoviewLayerConfig: TypeJsonObject, language: TypeDisplayLanguage) { super(geoviewLayerConfig, language); - const metadataAccessPathItems = this.metadataAccessPath.split(/\?Layers=/i); - const lastPathItem = metadataAccessPathItems[1]; - if (lastPathItem) { - // The metadataAccessPath ends with a layer parameter. It is therefore a path to a data layer rather than a path to service metadata. - // We therefore need to correct the configuration by separating the layer parameter and the path to the service metadata. - [this.metadataAccessPath] = metadataAccessPathItems; - if (this.listOfLayerEntryConfig.length) { - this.setErrorDetectedFlag(); - logger.logError('When an WMS metadataAccessPath ends with a layerId, the listOfLayerEntryConfig must be empty.'); + + // If user has provided parameters, extract the layers=layerId and transfer the layerId to the listOfLayerEntryConfig. + const metadataAccessPathItems = this.metadataAccessPath.split('?'); + if (metadataAccessPathItems.length > 2) { + // More than one question mark is an error. + this.setErrorDetectedFlag(); + logger.logError(`Invalid metadataAccessPath.\nmetadataAccessPath="${this.metadataAccessPath}"`); + } else if (metadataAccessPathItems.length === 2) { + const [metadataAccessPath, metadataAccessParameters] = metadataAccessPathItems; + const layerIndex = metadataAccessParameters + .toLowerCase() + .split('&') + .findIndex((parameter) => parameter.startsWith('layers')); + if (layerIndex !== -1) { + // The metadataAccessPath contains the Layers= parameter. When this is the case, the listOfLayerEntryConfig must be empty + // because we will transfer the layerId following this parameter to the listOfLayerEntryConfig and remove the Layer= + // parameter from the metadataAccessPath. + if (this.listOfLayerEntryConfig.length) { + this.setErrorDetectedFlag(); + logger.logError('When a WMS metadataAccessPath contains the Layers= parameter, the listOfLayerEntryConfig must be empty.'); + } else { + const parametersArray = metadataAccessParameters.split('&'); + // Extract the layerId from the original parameters array. + const layerId = parametersArray[layerIndex].split('=')[1]; + // Filter out the layers= parameter. + const newParameters = parametersArray.filter((parameter, i) => i !== layerIndex).join('&'); + // Rebuild the metadataAccessPath + this.metadataAccessPath = `${metadataAccessPath}${newParameters ? `?${newParameters}` : ''}`; + // Create the root node of the listOfLayerEntryConfig using the layerId. + this.listOfLayerEntryConfig = [this.createLeafNode(toJsonObject({ layerId }), language, this)! as TypeWmsLayerNode]; + } } - this.listOfLayerEntryConfig = [this.createLeafNode(toJsonObject({ layerId: lastPathItem }), language, this)! as TypeWmsLayerNode]; } } // #endregion CONSTRUCTOR @@ -122,7 +143,9 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { } /** - * Get the service metadata from the metadataAccessPath and store it in the private property of the geoview layer. + * Get the service metadata from the metadataAccessPath and store it in a protected property of the geoview layer. + * Verify that all sublayers defined in the listOfLayerEntryConfig exist in the metadata and fetch all sublayers metadata. + * If the metadata layer tree property is defined, build it using the service metadata. * @override @async */ override async fetchServiceMetadata(): Promise { @@ -141,14 +164,15 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { } } - (this.listOfLayerEntryConfig as EntryConfigBaseClass[]) = this.#processListOfLayerEntryConfig(this.listOfLayerEntryConfig); + if (!this.getErrorDetectedFlag()) { + (this.listOfLayerEntryConfig as EntryConfigBaseClass[]) = this.#processListOfLayerEntryConfig(this.listOfLayerEntryConfig); + await this.fetchListOfLayerMetadata(); - // When a list of layer entries is specified, the layer tree is the same as the resulting listOfLayerEntryConfig of the geoview instance. - // Otherwise, a layer tree is built using all the layers that compose the metadata. - this.setMetadataLayerTree(this.listOfLayerEntryConfig.length ? this.listOfLayerEntryConfig : this.createLayerTree()); - await this.fetchListOfLayerMetadata(); + await this.#createLayerTree(); + } } // #endregion OVERRIDE + // =============== // #region PRIVATE @@ -210,7 +234,7 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { // Create the layer using the metadata const layerConfig = toJsonObject({ layerId, - layerName: { en: layerFound.Title, fr: layerFound.Title }, + layerName: createLocalizedString(layerFound.Title), }); return this.createLeafNode(layerConfig, this.getLanguage(), this, parentNode)!; } @@ -226,12 +250,13 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { */ #createGroupNodeJsonConfig = (groupId: string, metadataLayerArray: TypeJsonObject[]): TypeJsonObject => { const listOfLayerEntryConfig = metadataLayerArray.reduce((accumulator, layer) => { - if ('Layer' in layer) accumulator.push(this.#createGroupNodeJsonConfig(layer.Name as string, layer.Layer as TypeJsonObject[])); + if ('Layer' in layer && Array.isArray(layer.Layer)) + accumulator.push(this.#createGroupNodeJsonConfig(layer.Name as string, layer.Layer as TypeJsonObject[])); else { accumulator.push( toJsonObject({ layerId: layer.Name, - layerName: { en: layer.Name, fr: layer.Name }, + layerName: createLocalizedString(layer.Name), }) ); } @@ -240,7 +265,7 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { return toJsonObject({ layerId: groupId, - layerName: { en: groupId, fr: groupId }, + layerName: createLocalizedString(groupId), isLayerGroup: true, listOfLayerEntryConfig, }); @@ -298,16 +323,14 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { */ async #fetchUsingGetCapabilities(): Promise { try { - const serviceMetadata = await WmsLayerConfig.#executeServiceMetadataRequest( - `${this.metadataAccessPath}?service=WMS&version=1.3.0&request=GetCapabilities` - ); + const serviceMetadata = await WmsLayerConfig.#executeServiceMetadataRequest(this.metadataAccessPath); this.setServiceMetadata(serviceMetadata); this.#processMetadataInheritance(); } catch (error) { // In the event of a service metadata reading error, we report the geoview layer and all its sublayers as being in error. this.setErrorDetectedFlag(); this.setErrorDetectedFlagForAllLayers(this.listOfLayerEntryConfig); - logger.logError(`Error detected while reading WMS metadata for geoview layer ${this.geoviewLayerId}.`); + logger.logError(`Error detected while reading WMS metadata for geoview layer ${this.geoviewLayerId}.\n`, error); } } @@ -321,11 +344,39 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { * @private @async */ static async #executeServiceMetadataRequest(url: string): Promise { - const response = await fetch(url); + let newUrl: string; + + // Use user-provided parameters, if applicable. + const metadataAccessPathItems = url.split('?'); + if (metadataAccessPathItems.length === 2) { + const [metadataAccessPath, metadataAccessParameters] = metadataAccessPathItems; + // Get the list of parameters (a lower case version). + const lowerParameters = metadataAccessParameters.toLowerCase().split('&'); + // Get the list of parameters (as provided by the user bercause layerIds are case sensitive). + const originalParameters = metadataAccessParameters.split('&'); + // Find parameters index. + const serviceIndex = lowerParameters.findIndex((parameter) => parameter.startsWith('service')); + const versionIndex = lowerParameters.findIndex((parameter) => parameter.startsWith('version')); + const requestIndex = lowerParameters.findIndex((parameter) => parameter.startsWith('request')); + const layersIndex = lowerParameters.findIndex((parameter) => parameter.startsWith('layers')); + // Get user-provided value or default value + const service = serviceIndex !== -1 ? originalParameters[serviceIndex] : 'service=WMS'; + const version = versionIndex !== -1 ? originalParameters[versionIndex] : 'version=1.3.0'; + const request = requestIndex !== -1 ? originalParameters[requestIndex] : 'request=GetCapabilities'; + const layers = layersIndex !== -1 ? `&${originalParameters[layersIndex]}` : ''; + // URL reconstruction using calculated values. + newUrl = `${metadataAccessPath}?${service}&${version}&${request}${layers}`; + } else { + // If no parameter was specified, use default values. + newUrl = `${url}?service=WMS&version=1.3.0&request=GetCapabilities`; + } + + const response = await fetch(newUrl); const capabilitiesString = await response.text(); const xmlDomResponse = new DOMParser().parseFromString(capabilitiesString, 'text/xml'); - const errorObject = xmlToJson(xmlDomResponse)?.['ogc:ServiceExceptionReport']?.['ogc:ServiceException']; + const jsonResponse = xmlToJson(xmlDomResponse); + const errorObject = jsonResponse?.['ogc:ServiceExceptionReport']?.['ogc:ServiceException']; if (errorObject) throw new GeoviewLayerConfigError(errorObject['#text'] as string); const parser = new WMSCapabilities(); @@ -355,9 +406,7 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { // if the layer found is the same as the current layer index, // this is the first time we execute this request promisedArrayOfMetadata.push( - WmsLayerConfig.#executeServiceMetadataRequest( - `${this.metadataAccessPath}?service=WMS&version=1.3.0&request=GetCapabilities&Layers=${layerConfig.layerId}` - ) + WmsLayerConfig.#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]); @@ -519,19 +568,58 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { }); } } - if (layer?.Layer !== undefined) (layer.Layer as TypeJsonArray).forEach((subLayer) => this.#processMetadataInheritance(layer, subLayer)); + if (layer?.Layer !== undefined && Array.isArray(layer.layer)) + (layer.Layer as TypeJsonArray).forEach((subLayer) => this.#processMetadataInheritance(layer, subLayer)); + } + + /** + * Create the layer tree associated to the GeoView layer if the layer tree filter stored in the metadataLayerTree private property + * is set. + * @private + */ + async #createLayerTree(): Promise { + let layerTreeFilter = this.getMetadataLayerTree(); + if (layerTreeFilter !== undefined) { + if (layerTreeFilter.length === 0) { + this.setMetadataLayerTree(this.#processListOfLayerEntryConfig(this.#createLayerTreeFromServiceMetadata() as TypeWmsLayerNode[])); + } else { + if (layerTreeFilter.length > 1) { + layerTreeFilter = [ + Cast({ + layerId: this.geoviewLayerId, + layerName: createLocalizedString(this.geoviewLayerName), + isLayerGroup: true, + listOfLayerEntryConfig: layerTreeFilter, + }), + ]; + } + + // Instanciate the sublayer list. + layerTreeFilter = layerTreeFilter + ?.map((layerFilter) => { + if (layerEntryIsGroupLayer(layerFilter)) + return this.createGroupNode(Cast(layerFilter), this.getLanguage(), this); + return this.createLeafNode(Cast(layerFilter), this.getLanguage(), this); + }) + // When a sublayer cannot be created, the value returned is undefined. These values will be filtered. + ?.filter((subLayerConfig) => { + return subLayerConfig; + }) as EntryConfigBaseClass[]; + + this.applyDefaultValues(); + this.setMetadataLayerTree(this.#processListOfLayerEntryConfig(layerTreeFilter as TypeWmsLayerNode[])); + } + await this.fetchListOfLayerMetadata(this.getMetadataLayerTree()); + } } - // #endregion PRIVATE - // ================= - // #region PROTECTED /** * Create the layer tree using the service metadata. * * @returns {TypeJsonObject[]} The layer tree created from the metadata. - * @protected + * @private */ - protected createLayerTree(): EntryConfigBaseClass[] { + #createLayerTreeFromServiceMetadata(): EntryConfigBaseClass[] { const metadataLayer = this.getServiceMetadata().Capability.Layer; // If it is a group layer, then create it. if ('Layer' in metadataLayer) { @@ -544,13 +632,13 @@ export class WmsLayerConfig extends AbstractGeoviewLayerConfig { // Create a single layer using the metadata const layerConfig = toJsonObject({ layerId: metadataLayer.Name, - layerName: { en: metadataLayer.Name, fr: metadataLayer.Name }, + layerName: createLocalizedString(metadataLayer.Name), }); return [this.createLeafNode(layerConfig, this.getLanguage(), this)!]; } + // #endregion PRIVATE - // #endregion PROTECTED - // ================= + // ============== // #region STATIC /** **************************************************************************************************************************** * This method search recursively the layerId in the layer entry of the capabilities.