fetchJSONData();
});
- // Set the initial configuration of the drop-down to 'Simple' and load it
- configLoaderTab1.value = './configs/validator/01-simple.json';
+ // Set the initial configuration of the drop-down to 'Simple EsriFeature' and load it
+ configLoaderTab1.value = './configs/validator/01-simple-esri-feature.json';
configLoaderTab1.dispatchEvent(new Event('change'));
// Create the listener for the Element Displayed drop-down located at the bottom of the output zone.
@@ -652,6 +655,7 @@
Sanbox Map
geoCore: '12acd145-626a-49eb-b850-0a59c9bc7506',
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',
};
// Get the GeoView Layer Type drop-down object used to select the type of layer.
@@ -838,6 +842,7 @@
Sanbox Map
unknown: 'unknown',
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',
};
// Get the GeoView Layer Type drop-down object used to select the type of layer.
@@ -908,7 +913,11 @@
Sanbox Map
languageTab4
);
// Output instanciation result
- metadataLayerTreeString = `${JSON.stringify(metadataLayerTree, undefined, 2)}\n`;
+ 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');
diff --git a/packages/geoview-core/src/api/config/config-api.ts b/packages/geoview-core/src/api/config/config-api.ts
index 3ebc6f253e5..77acb1c302f 100644
--- a/packages/geoview-core/src/api/config/config-api.ts
+++ b/packages/geoview-core/src/api/config/config-api.ts
@@ -468,7 +468,7 @@ export class ConfigApi {
): Promise {
let geoviewLayerConfig: TypeJsonObject | undefined;
- // If the lyerType is a GeoCore, we translate it to its GeoView configuration.
+ // If the layerType is a GeoCore, we translate it to its GeoView configuration.
if (layerType === CV_CONFIG_GEOCORE_TYPE) {
try {
const layerConfig = { geoviewLayerId: serviceAccessString, geoviewLayerType: layerType };
@@ -499,7 +499,7 @@ export class ConfigApi {
}
/**
- * Create the layer tree from the service metadata.
+ * Create the layer tree from the service metadata. If an error is detected, throw an error.
*
* @param {string} serviceAccessString The service access string (a URL or a layer identifier).
* @param {TypeGeoviewLayerType | CV_CONFIG_GEOCORE_TYPE} layerType The GeoView layer type or 'geoCore'.
@@ -520,6 +520,6 @@ export class ConfigApi {
await geoviewLayerConfig.fetchServiceMetadata();
if (!geoviewLayerConfig.getErrorDetectedFlag()) return geoviewLayerConfig.getMetadataLayerTree();
}
- return [];
+ throw new MapConfigError('Unable to build metadata layer tree.');
}
}
diff --git a/packages/geoview-core/src/api/config/types/classes/config-exceptions.ts b/packages/geoview-core/src/api/config/types/classes/config-exceptions.ts
index 3c64849cc91..659ab93f17f 100644
--- a/packages/geoview-core/src/api/config/types/classes/config-exceptions.ts
+++ b/packages/geoview-core/src/api/config/types/classes/config-exceptions.ts
@@ -36,33 +36,6 @@ export class GeoviewLayerConfigError extends ConfigError {
}
}
-export class GeoviewLayerMandatoryError extends ConfigError {
- messageList: Record = {
- LayerNameMandatory: 'Property geoviewLayerName is mandatory for GeoView layer',
- };
-
- messageKey: string;
-
- messageVariables: string[];
-
- constructor(messageKey: string, messageVariables: string[]) {
- super();
- this.messageKey = messageKey;
- this.messageVariables = messageVariables;
-
- // Override the message
- const messageFragments = this.messageList[messageKey].split('<=>');
- const message = messageFragments.reduce((accumulator, stringToAppend, i) => {
- return i < messageVariables.length ? `${accumulator}${stringToAppend}${messageVariables[i]}` : `${accumulator}${stringToAppend}`;
- }, '');
- this.message = message;
-
- // Set the prototype explicitly (as recommended by TypeScript doc)
- // https://github.com/microsoft/TypeScript-wiki/blob/81fe7b91664de43c02ea209492ec1cea7f3661d0/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work)
- Object.setPrototypeOf(this, MapConfigError.prototype);
- }
-}
-
export class GeoviewLayerInvalidParameterError extends ConfigError {
messageList: Record = {
LayerIdNotFound: 'Cannot find layer id <=> in the metadata.',
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 3fba2e70eb6..0ba5b0b7ad4 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
@@ -2,7 +2,7 @@ import { toJsonObject, TypeJsonObject, TypeJsonArray } from '@config/types/confi
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 { layerEntryIsAbstractBaseLayerEntryConfig, layerEntryIsGroupLayer } from '@config/types/type-guards';
+import { layerEntryIsGroupLayer } from '@config/types/type-guards';
import { ConfigError, GeoviewLayerInvalidParameterError } from '@config/types/classes/config-exceptions';
import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';
@@ -78,7 +78,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
// 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(this.listOfLayerEntryConfig);
+ await this.fetchListOfLayerMetadata();
}
} else {
this.setErrorDetectedFlag();
@@ -102,6 +102,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
#processListOfLayerEntryConfig(listOfLayerEntryConfig: EntryConfigBaseClass[]): EntryConfigBaseClass[] {
return listOfLayerEntryConfig.map((subLayer) => {
if (subLayer.getErrorDetectedFlag()) return subLayer;
+
if (layerEntryIsGroupLayer(subLayer)) {
// The next line replace the listOfLayerEntryConfig stored in the subLayer parameter
// Since subLayer is the function parameter, we must disable the eslint no-param-reassign
@@ -109,6 +110,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
subLayer.listOfLayerEntryConfig = this.#processListOfLayerEntryConfig(subLayer.listOfLayerEntryConfig);
return subLayer;
}
+
try {
return this.#createLayerEntryNode(parseInt(subLayer.layerId, 10), subLayer.getParentNode());
} catch (error) {
@@ -120,11 +122,13 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
}
/**
- * Create a layer entry node for a specific layerId using the service metadata.
+ * Create a layer entry node for a specific layerId using the service metadata. The node returned can be a
+ * layer or a group layer.
*
* @param {number} layerId The layer id to use for the subLayer creation.
+ * @param {EntryConfigBaseClass | undefined} parentNode The layer's parent node.
*
- * @returns {EntryConfigBaseClass[]} The subLayer created from the metadata.
+ * @returns {EntryConfigBaseClass} The subLayer created from the metadata.
* @private
*/
#createLayerEntryNode(layerId: number, parentNode: EntryConfigBaseClass | undefined): EntryConfigBaseClass {
@@ -148,12 +152,12 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
}
// Create the layer group from the array of layers
- const jsonConfig = this.#createGroupNode(parseInt(layerFound.id as string, 10), layerFound?.name as string);
+ const jsonConfig = this.#createGroupNodeJsonConfig(parseInt(layerFound.id as string, 10), layerFound?.name as string);
return this.createGroupNode(jsonConfig, this.getLanguage(), this, parentNode)!;
}
/**
- * Create a group node for a specific layer id.
+ * Create a group node JSON configuration for a specific layer id.
*
* @param {number} parentId The layer id of the parent node.
* @param {string} groupName The name to assign to the group node.
@@ -161,11 +165,11 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
* @returns {TypeJsonObject} A json configuration that can be used to create the group node.
* @private
*/
- #createGroupNode = (parentId: number, groupName: string): TypeJsonObject => {
- const layers = this.getServiceMetadata().layers as TypeJsonObject[];
- const listOfLayerEntryConfig = layers.reduce((accumulator, layer) => {
+ #createGroupNodeJsonConfig = (parentId: number, groupName: string): TypeJsonObject => {
+ const layersArray = this.getServiceMetadata().layers as TypeJsonObject[];
+ const listOfLayerEntryConfig = layersArray.reduce((accumulator, layer) => {
if (layer.parentLayerId === parentId) {
- if (layer.type === 'Group Layer') accumulator.push(this.#createGroupNode(layer.id as number, layer.name as string));
+ if (layer.type === 'Group Layer') accumulator.push(this.#createGroupNodeJsonConfig(layer.id as number, layer.name as string));
else {
accumulator.push(
toJsonObject({
@@ -186,32 +190,9 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
listOfLayerEntryConfig,
});
};
-
- /**
- * Fetch the metadata using the list of layer entry configuration provided.
- *
- * @param {EntryConfigBaseClass[]} listOfLayerEntryConfig The list of layer entry config to process.
- *
- * @returns {Promise} A promise that will resolve when the process has completed.
- * @private
- */
- async #fetchListOfLayerMetadata(listOfLayerEntryConfig: EntryConfigBaseClass[]): Promise {
- const listOfLayerMetadata: Promise[] = [];
- listOfLayerEntryConfig.forEach((subLayerConfig) => {
- if (layerEntryIsGroupLayer(subLayerConfig)) {
- listOfLayerMetadata.push(subLayerConfig.fetchLayerMetadata());
- listOfLayerMetadata.push(this.#fetchListOfLayerMetadata(subLayerConfig.listOfLayerEntryConfig));
- } else if (layerEntryIsAbstractBaseLayerEntryConfig(subLayerConfig)) {
- listOfLayerMetadata.push(subLayerConfig.fetchLayerMetadata());
- }
- });
-
- await Promise.allSettled(listOfLayerMetadata);
- logger.logDebug(listOfLayerEntryConfig);
- }
-
// #endregion PRIVATE
// #region PROTECTED
+
/**
* Converts an esri geometry type string to a TypeStyleGeometry.
* @param {string} esriGeometryType - The esri geometry type to convert
@@ -246,7 +227,7 @@ export abstract class AbstractGeoviewEsriLayerConfig extends AbstractGeoviewLaye
const layers = this.getServiceMetadata().layers as TypeJsonArray;
if (layers.length > 1) {
const groupName = this.getServiceMetadata().mapName as string;
- return [new EsriGroupLayerConfig(this.#createGroupNode(-1, groupName), this.getLanguage(), this)];
+ return [new EsriGroupLayerConfig(this.#createGroupNodeJsonConfig(-1, groupName), this.getLanguage(), this)];
}
if (layers.length === 1)
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 1fa0b078ef8..a489c19c52c 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
@@ -1,12 +1,12 @@
-import merge from 'lodash/merge';
+import mergeWith from 'lodash/mergeWith';
import cloneDeep from 'lodash/cloneDeep';
import { Cast, TypeJsonObject, TypeJsonArray } from '@config/types/config-types';
import { TypeGeoviewLayerType, TypeDisplayLanguage } from '@config/types/map-schema-types';
-import { isvalidComparedToInputSchema, normalizeLocalizedString } from '@config/utils';
-import { EsriGroupLayerConfig } from '@config/types/classes/sub-layer-config/group-node/esri-group-layer-config';
+import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema, normalizeLocalizedString } from '@config/utils';
import { layerEntryIsGroupLayer } from '@config/types/type-guards';
-import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';
+import { EntryConfigBaseClass } from '@config/types/classes/sub-layer-config/entry-config-base-class';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
import { generateId } from '@/core/utils/utilities';
import { logger } from '@/core/utils/logger';
@@ -129,7 +129,7 @@ export abstract class AbstractGeoviewLayerConfig {
// Instanciate the sublayer list.
this.listOfLayerEntryConfig = (this.#userGeoviewLayerConfig.listOfLayerEntryConfig as TypeJsonArray)
?.map((subLayerConfig) => {
- if (layerEntryIsGroupLayer(subLayerConfig)) return new EsriGroupLayerConfig(subLayerConfig, language, this);
+ if (layerEntryIsGroupLayer(subLayerConfig)) return this.createGroupNode(subLayerConfig, language, this);
return this.createLeafNode(subLayerConfig, language, this);
})
// When a sublayer cannot be created, the value returned is undefined. These values will be filtered.
@@ -227,6 +227,29 @@ export abstract class AbstractGeoviewLayerConfig {
protected getLanguage(): TypeDisplayLanguage {
return this.#language;
}
+
+ /**
+ * Fetch the metadata of all layer entry configurations defined in the list of layer entry config.
+ *
+ * @returns {Promise} A promise that will resolve when the process has completed.
+ * @protected @async
+ */
+ protected async fetchListOfLayerMetadata(): Promise {
+ // The root of the GeoView layer tree is an array that contains only one node.
+ const rootLayer = this.listOfLayerEntryConfig[0];
+
+ try {
+ if (rootLayer) {
+ // If an error has been detected, there is a problem with the metadata and the layer is unusable.
+ if (rootLayer.getErrorDetectedFlag()) return;
+
+ await rootLayer.fetchLayerMetadata();
+ }
+ } catch (error) {
+ logger.logError(`An error occured while reading the metadata for the layerPath ${rootLayer.getLayerPath()}.`, error);
+ rootLayer.setErrorDetectedFlag();
+ }
+ }
// #endregion PROTECTED
// ==============
@@ -347,10 +370,21 @@ export abstract class AbstractGeoviewLayerConfig {
return sublayer;
});
};
- this.listOfLayerEntryConfig = merge(
- this.listOfLayerEntryConfig,
- convertUserConfigToInternalConfig(geoviewLayerConfig.listOfLayerEntryConfig as TypeJsonArray)
- );
+ const internalConfig = convertUserConfigToInternalConfig(geoviewLayerConfig.listOfLayerEntryConfig as TypeJsonArray);
+ this.listOfLayerEntryConfig = mergeWith(this.listOfLayerEntryConfig, internalConfig, (target, newValue, key) => {
+ // Keep the listOfLayerEntryConfig as it is. Do not replace it with the user' array. Only the internal properties will be replaced.
+ // This is because the newValue is not an instance of EntryConfigBaseClass. The type of the newValue property is a plain JSON object.
+ if (key === 'listOfLayerEntryConfig') return undefined;
+ // Replace arrays with user config arrays if they exist.
+ if (Array.isArray(target) && Array.isArray(newValue)) return newValue;
+ return undefined;
+ });
+
+ if (!isvalidComparedToInternalSchema(this.getGeoviewLayerSchema(), this, true)) {
+ throw new GeoviewLayerConfigError(
+ `GeoView internal configuration ${this.geoviewLayerId} is invalid compared to the internal schema specification.`
+ );
+ }
}
/**
diff --git a/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/esri-dynamic-config.ts b/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/esri-dynamic-config.ts
index 95584395eba..4cf72dcc068 100644
--- a/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/esri-dynamic-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/esri-dynamic-config.ts
@@ -34,11 +34,11 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
// #region OVERRIDE
/**
- * @protected @override
* The getter method that returns the geoview layer schema to use for the validation. Each geoview layer type knows what
* section of the schema must be used to do its validation.
*
* @returns {string} The GeoView layer schema associated to the config.
+ * @protected @override
*/
protected override getGeoviewLayerSchema(): string {
/** The GeoView layer schema associated to EsriDynamicLayerConfig */
@@ -46,7 +46,6 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
}
/**
- * @override
* The method used to implement the class factory model that returns the instance of the class based on the sublayer
* type needed.
*
@@ -56,6 +55,7 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
* @param {EntryConfigBaseClass} parentNode The The parent node that owns this layer or undefined if it is the root layer..
*
* @returns {EntryConfigBaseClass} The sublayer instance or undefined if there is an error.
+ * @override
*/
override createLeafNode(
layerConfig: TypeJsonObject,
@@ -67,7 +67,6 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
}
/**
- * @override
* The method used to implement the class factory model that returns the instance of the class based on the group
* type needed.
*
@@ -77,6 +76,7 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
* @param {EntryConfigBaseClass} parentNode The The parent node that owns this layer or undefined if it is the root layer..
*
* @returns {EntryConfigBaseClass} The sublayer instance or undefined if there is an error.
+ * @override
*/
override createGroupNode(
layerConfig: TypeJsonObject,
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
new file mode 100644
index 00000000000..87931450940
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/raster-config/wms-config.ts
@@ -0,0 +1,592 @@
+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 { 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';
+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';
+
+export type TypeWmsLayerNode = WmsGroupLayerConfig | WmsLayerEntryConfig;
+
+// ========================
+// #region CLASS DEFINITION
+
+/**
+ * The WMS geoview layer class.
+ */
+export class WmsLayerConfig extends AbstractGeoviewLayerConfig {
+ // ==================
+ // #region PROPERTIES
+
+ /**
+ * Type of GeoView layer.
+ */
+ override geoviewLayerType = CV_CONST_LAYER_TYPES.WMS;
+
+ /** The layer entries to use from the GeoView layer. */
+ declare listOfLayerEntryConfig: TypeWmsLayerNode[];
+
+ // #endregion PROPERTIES
+ // ===================
+ // #region CONSTRUCTOR
+ /**
+ * The class constructor.
+ *
+ * @param {TypeJsonObject} geoviewLayerConfig The layer configuration we want to instanciate.
+ * @param {TypeDisplayLanguage} language The initial language to use when interacting with the map feature configuration.
+ */
+ 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.');
+ }
+ this.listOfLayerEntryConfig = [this.createLeafNode(toJsonObject({ layerId: lastPathItem }), language, this)! as TypeWmsLayerNode];
+ }
+ }
+ // #endregion CONSTRUCTOR
+
+ // ===============
+ // #region METHODS
+ /*
+ * Methods are listed in the following order: abstract, override, private, protected, public and static.
+ */
+
+ // ================
+ // #region OVERRIDE
+ /**
+ * The getter method that returns the geoview layer schema to use for the validation. Each geoview layer type knows what
+ * section of the schema must be used to do its validation.
+ *
+ * @returns {string} The GeoView layer schema associated to the config.
+ * @protected @override
+ */
+ protected override getGeoviewLayerSchema(): string {
+ /** The GeoView layer schema associated to WmsLayerConfig */
+ return CV_GEOVIEW_SCHEMA_PATH.WMS;
+ }
+
+ /**
+ * The method used to implement the class factory model that returns the instance of the class based on the sublayer
+ * type needed.
+ *
+ * @param {TypeJsonObject} layerConfig The sublayer configuration.
+ * @param {TypeDisplayLanguage} language The initial language to use when interacting with the geoview layer.
+ * @param {AbstractGeoviewLayerConfig} geoviewConfig The GeoView instance that owns the sublayer.
+ * @param {EntryConfigBaseClass} parentNode The The parent node that owns this layer or undefined if it is the root layer.
+ *
+ * @returns {EntryConfigBaseClass} The sublayer instance or undefined if there is an error.
+ * @override
+ */
+ override createLeafNode(
+ layerConfig: TypeJsonObject,
+ language: TypeDisplayLanguage,
+ geoviewConfig: AbstractGeoviewLayerConfig,
+ parentNode?: EntryConfigBaseClass
+ ): EntryConfigBaseClass {
+ return new WmsLayerEntryConfig(layerConfig, language, geoviewConfig, parentNode);
+ }
+
+ /**
+ * The method used to implement the class factory model that returns the instance of the class based on the group
+ * type needed.
+ *
+ * @param {TypeJsonObject} layerConfig The group node configuration.
+ * @param {TypeDisplayLanguage} language The initial language to use when interacting with the geoview layer.
+ * @param {AbstractGeoviewLayerConfig} geoviewConfig The GeoView instance that owns the sublayer.
+ * @param {EntryConfigBaseClass} parentNode The The parent node that owns this layer or undefined if it is the root layer.
+ *
+ * @returns {EntryConfigBaseClass} The sublayer instance or undefined if there is an error.
+ * @override
+ */
+ override createGroupNode(
+ layerConfig: TypeJsonObject,
+ language: TypeDisplayLanguage,
+ geoviewConfig: AbstractGeoviewLayerConfig,
+ parentNode?: EntryConfigBaseClass
+ ): EntryConfigBaseClass {
+ return new WmsGroupLayerConfig(layerConfig, language, geoviewConfig, parentNode);
+ }
+
+ /**
+ * Get the service metadata from the metadataAccessPath and store it in the private property of the geoview layer.
+ * @override @async
+ */
+ override async fetchServiceMetadata(): Promise {
+ const metadataAccessPathIsXmlFile = this.metadataAccessPath.slice(-4).toLowerCase() === '.xml';
+ if (metadataAccessPathIsXmlFile) {
+ // XML metadata is a special case that does not use GetCapabilities to get the metadata
+ await this.#fetchXmlServiceMetadata(this.metadataAccessPath);
+ } else {
+ const layerConfigsToQuery = this.#getLayersToQuery(this.listOfLayerEntryConfig);
+ if (layerConfigsToQuery.length === 0) {
+ // Use 'request=GetCapabilities' to get the service metadata
+ await this.#fetchUsingGetCapabilities();
+ } else {
+ // Uses 'request=GetCapabilities&Layers=' to get the service metadata.
+ await this.#fetchUsingGetCapabilitiesAndLayers(layerConfigsToQuery);
+ }
+ }
+
+ (this.listOfLayerEntryConfig as EntryConfigBaseClass[]) = 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();
+ }
+ // #endregion OVERRIDE
+ // ===============
+ // #region PRIVATE
+
+ /**
+ * A recursive method to process the listOfLayerEntryConfig. The goal is to process each valid sublayer, searching the service's
+ * metadata to verify the layer's existence and whether it is a layer group, in order to determine the node's final structure.
+ * If it is a layer group, it will be created.
+ *
+ * @param {TypeWmsLayerNode[]} listOfLayerEntryConfig the list of sublayers to process.
+ *
+ * @returns {TypeWmsLayerNode[]} the new list of sublayer configurations.
+ * @private
+ */
+ #processListOfLayerEntryConfig(listOfLayerEntryConfig: TypeWmsLayerNode[]): EntryConfigBaseClass[] {
+ return listOfLayerEntryConfig.map((subLayer) => {
+ if (subLayer.getErrorDetectedFlag()) return subLayer;
+
+ if (layerEntryIsGroupLayer(subLayer)) {
+ // The next line replace the listOfLayerEntryConfig stored in the subLayer parameter
+ // Since subLayer is the function parameter, we must disable the eslint no-param-reassign
+ // eslint-disable-next-line no-param-reassign
+ subLayer.listOfLayerEntryConfig = this.#processListOfLayerEntryConfig(subLayer.listOfLayerEntryConfig as TypeWmsLayerNode[]);
+ return subLayer;
+ }
+
+ try {
+ return this.#createLayerEntryNode(subLayer.layerId, subLayer.getParentNode());
+ } catch (error) {
+ subLayer.setErrorDetectedFlag();
+ logger.logError((error as ConfigError).message, error);
+ return subLayer;
+ }
+ });
+ }
+
+ /**
+ * Create a layer entry node for a specific layerId using the service metadata. The node returned can be a
+ * layer or a group layer.
+ *
+ * @param {string} layerId The layer id to use for the subLayer creation.
+ * @param {TypeWmsLayerNode | undefined} parentNode The layer's parent node.
+ *
+ * @returns {TypeWmsLayerNode} The subLayer created from the metadata.
+ * @private
+ */
+ #createLayerEntryNode(layerId: string, parentNode: EntryConfigBaseClass | undefined): EntryConfigBaseClass {
+ // If we cannot find the layerId in the layer definitions, throw an error.
+ const layerFound = WmsLayerConfig.getLayerMetadataEntry(layerId, this.getServiceMetadata().Capability.Layer);
+ if (!layerFound) {
+ throw new GeoviewLayerInvalidParameterError('LayerIdNotFound', [layerId?.toString()]);
+ }
+
+ // if the layerFound is a group layer, create a the layer group.
+ if ('Layer' in layerFound) {
+ const jsonConfig = this.#createGroupNodeJsonConfig(layerId, layerFound.Layer as TypeJsonObject[]);
+ return this.createGroupNode(jsonConfig, this.getLanguage(), this, parentNode)!;
+ }
+
+ // Create the layer using the metadata
+ const layerConfig = toJsonObject({
+ layerId,
+ layerName: { en: layerFound.Title, fr: layerFound.Title },
+ });
+ return this.createLeafNode(layerConfig, this.getLanguage(), this, parentNode)!;
+ }
+
+ /**
+ * Create a group node JSON config for a specific layer id.
+ *
+ * @param {string} groupId The layer group id of the node.
+ * @param {TypeJsonObject[]} metadataLayerArray The metadata array of layer to assign to the group node.
+ *
+ * @returns {TypeJsonObject} A json configuration that can be used to create the group node.
+ * @private
+ */
+ #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[]));
+ else {
+ accumulator.push(
+ toJsonObject({
+ layerId: layer.Name,
+ layerName: { en: layer.Name, fr: layer.Name },
+ })
+ );
+ }
+ return accumulator;
+ }, [] as TypeJsonObject[]);
+
+ return toJsonObject({
+ layerId: groupId,
+ layerName: { en: groupId, fr: groupId },
+ isLayerGroup: true,
+ listOfLayerEntryConfig,
+ });
+ };
+
+ /** ***************************************************************************************************************************
+ * This method reads the service metadata from a XML metadataAccessPath.
+ *
+ * @param {string} metadataUrl The localized value of the metadataAccessPath
+ *
+ * @returns {Promise} A promise that the execution is completed.
+ * @private
+ */
+ async #fetchXmlServiceMetadata(metadataUrl: string): Promise {
+ try {
+ const parser = new WMSCapabilities();
+ const response = await fetch(metadataUrl);
+ const capabilitiesString = await response.text();
+ this.setServiceMetadata(parser.read(capabilitiesString));
+ if (this.getServiceMetadata()) {
+ this.#processMetadataInheritance();
+ this.metadataAccessPath = this.getServiceMetadata().Capability.Request.GetMap.DCPType[0].HTTP.Get.OnlineResource as string;
+ } else throw new GeoviewLayerConfigError('Unable to read the metadata, value returned is empty.');
+ } 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}.`);
+ }
+ }
+
+ /** ***************************************************************************************************************************
+ * This method converts the tree structure of the listOfLayerEntreyConfig to a one dimensionnal array of layers
+ * that will be used in the GetCapabilities.
+ *
+ * @param {TypeWmsLayerNode[]} The list of layer entry config.
+ *
+ * @returns {WmsLayerEntryConfig[]} The array of layer configurations.
+ * @private
+ */
+ #getLayersToQuery(listOfLayerEntryConfig: TypeWmsLayerNode[]): WmsLayerEntryConfig[] {
+ const returnValue = listOfLayerEntryConfig.reduce((accumulator, layerConfig) => {
+ if (layerEntryIsGroupLayer(layerConfig))
+ // eslint-disable-next-line no-param-reassign
+ accumulator = accumulator.concat(this.#getLayersToQuery(layerConfig.listOfLayerEntryConfig as TypeWmsLayerNode[]));
+ else accumulator.push(layerConfig);
+ return accumulator;
+ }, [] as WmsLayerEntryConfig[]);
+ return returnValue;
+ }
+
+ /** ***************************************************************************************************************************
+ * This method fetch the service metadata using a getCapabilities request.
+ * @private
+ */
+ async #fetchUsingGetCapabilities(): Promise {
+ try {
+ const serviceMetadata = await WmsLayerConfig.#executeServiceMetadataRequest(
+ `${this.metadataAccessPath}?service=WMS&version=1.3.0&request=GetCapabilities`
+ );
+ 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}.`);
+ }
+ }
+
+ /** ***************************************************************************************************************************
+ * This method reads service metadata using the URL provided. The syntax can be “request=GetCapabilities” or
+ * “request=GetCapabilities&Layers=layerId”. Callers are responsible for handling any errors that occur here.
+ *
+ * @param {string} url The GetCapabilities request to execute
+ *
+ * @returns {Promise} A promise that the execution is completed.
+ * @private @async
+ */
+ static async #executeServiceMetadataRequest(url: string): Promise {
+ const response = await fetch(url);
+ const capabilitiesString = await response.text();
+
+ const xmlDomResponse = new DOMParser().parseFromString(capabilitiesString, 'text/xml');
+ const errorObject = xmlToJson(xmlDomResponse)?.['ogc:ServiceExceptionReport']?.['ogc:ServiceException'];
+ if (errorObject) throw new GeoviewLayerConfigError(errorObject['#text'] as string);
+
+ const parser = new WMSCapabilities();
+ const serviceMetadata: TypeJsonObject = parser.read(capabilitiesString);
+ if (serviceMetadata?.Capability?.Layer) return serviceMetadata;
+ throw new GeoviewLayerConfigError(`Unable to read the metadata, value returned doesn't contain Capability, Layer or is empty.`);
+ }
+
+ /** ***************************************************************************************************************************
+ * This method fetch the service metadata using a getCapabilities&Layers= request. To allow geomet service metadata to be
+ * retrieved using the "Layers" parameter in the request URL, we need to process each layer individually and merge all layer
+ * metadata at the end. Even though the "Layers" parameter is ignored by other WMS servers, the drawback of this method is
+ * sending unnecessary requests while only one GetCapabilities could be used when the server publishes a small set of metadata.
+ * Which is not the case for the Geomet service.
+ *
+ * @param {WmsLayerEntryConfig} layerConfigsToQuery The array of layers to process.
+ * @private
+ */
+ async #fetchUsingGetCapabilitiesAndLayers(layerConfigsToQuery: WmsLayerEntryConfig[]): Promise {
+ try {
+ const promisedArrayOfMetadata: Promise[] = [];
+ let i: number;
+ 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 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}`
+ )
+ );
+ // 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.
+ const arrayOfMetadata = await Promise.all(promisedArrayOfMetadata);
+
+ // Initialize service metadata using index 0 of arrayOfMetadata as a starting point. Other indexes will be added to it.
+ this.setServiceMetadata(arrayOfMetadata[0]);
+ for (i = 1; i < arrayOfMetadata.length; i++) {
+ if (!WmsLayerConfig.getLayerMetadataEntry(layerConfigsToQuery[i].layerId, this.getServiceMetadata().Capability.Layer)) {
+ const metadataLayerPathToAdd = this.#getMetadataLayerPath(layerConfigsToQuery[i].layerId!, arrayOfMetadata[i]!.Capability.Layer);
+ this.#addLayerToMetadataInstance(
+ metadataLayerPathToAdd,
+ this.getServiceMetadata().Capability.Layer,
+ arrayOfMetadata[i]!.Capability.Layer
+ );
+ }
+ }
+ 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}.\n${
+ (error as GeoviewLayerConfigError).message || ''
+ }`,
+ error
+ );
+ }
+ }
+
+ /** ***************************************************************************************************************************
+ * This method find the layer path that lead to the layer identified by the layerName. Values stored in the array tell us which
+ * direction to use to get to the layer. A value of -1 tells us that the Layer property is an object. Other values tell us that
+ * the Layer property is an array and the value is the index to follow. If the layer can not be found, the returned value is
+ * an empty array.
+ *
+ * @param {string} layerName The layer name to be found
+ * @param {TypeJsonObject} layerProperty The layer property from the metadata
+ * @param {number[]} pathToTheLayerProperty The path leading to the parent of the layerProperty parameter
+ *
+ * @returns {number[]} An array containing the path to the layer or [] if not found.
+ * @private
+ */
+ #getMetadataLayerPath(layerName: string, layerProperty: TypeJsonObject, pathToTheParentLayer: number[] = []): number[] {
+ const newLayerPath = [...pathToTheParentLayer];
+ if (Array.isArray(layerProperty)) {
+ for (let i = 0; i < layerProperty.length; i++) {
+ newLayerPath.push(i);
+ if ('Name' in layerProperty[i] && layerProperty[i].Name === layerName) return newLayerPath;
+ if ('Layer' in layerProperty[i]) {
+ return this.#getMetadataLayerPath(layerName, layerProperty[i].Layer, newLayerPath);
+ }
+ }
+ } else {
+ newLayerPath.push(-1);
+ if ('Name' in layerProperty && layerProperty.Name === layerName) return newLayerPath;
+ if ('Layer' in layerProperty) {
+ return this.#getMetadataLayerPath(layerName, layerProperty.Layer, newLayerPath);
+ }
+ }
+ return [];
+ }
+
+ /** ***************************************************************************************************************************
+ * This method merge the layer identified by the path stored in the metadataLayerPathToAdd array to the metadata property of
+ * the WMS instance. Values stored in the path array tell us which direction to use to get to the layer. A value of -1 tells us
+ * that the Layer property is an object. In this case, it is assumed that the metadata objects at this level only differ by the
+ * layer property to add. Other values tell us that the Layer property is an array and the value is the index to follow. If at
+ * this level in the path the layers have the same name, we move to the next level. Otherwise, the layer can be added.
+ *
+ * @param {number[]} metadataLayerPathToAdd The layer name to be found
+ * @param {TypeJsonObject | undefined} metadataLayer The metadata layer that will receive the new layer
+ * @param {TypeJsonObject} layerToAdd The layer property to add
+ * @private
+ */
+ #addLayerToMetadataInstance(
+ metadataLayerPathToAdd: number[],
+ metadataLayer: TypeJsonObject | undefined,
+ layerToAdd: TypeJsonObject
+ ): void {
+ if (metadataLayerPathToAdd.length === 0 || !metadataLayer) return;
+ if (metadataLayerPathToAdd[0] === -1)
+ this.#addLayerToMetadataInstance(metadataLayerPathToAdd.slice(1), metadataLayer.Layer, layerToAdd.Layer);
+ else {
+ const metadataLayerFound = (metadataLayer as TypeJsonArray).find(
+ (layerEntry) => layerEntry.Name === layerToAdd[metadataLayerPathToAdd[0]].Name
+ );
+ if (metadataLayerFound)
+ this.#addLayerToMetadataInstance(
+ metadataLayerPathToAdd.slice(1),
+ metadataLayerFound.Layer,
+ layerToAdd[metadataLayerPathToAdd[0]].Layer
+ );
+ else (metadataLayer as TypeJsonArray).push(layerToAdd[metadataLayerPathToAdd[0]]);
+ }
+ }
+
+ /** ***************************************************************************************************************************
+ * This method propagate the WMS metadata inherited values.
+ *
+ * @param {TypeJsonObject} parentLayer The parent layer that contains the inherited values
+ * @param {TypeJsonObject | undefined} layer The layer property from the metadata that will inherit the values
+ * @private
+ */
+ #processMetadataInheritance(
+ parentLayer?: TypeJsonObject,
+ layer: TypeJsonObject | undefined = this.getServiceMetadata().Capability.Layer
+ ): void {
+ if (parentLayer && layer) {
+ // Table 7 — Inheritance of Layer properties specified in the standard with 'replace' behaviour.
+ // eslint-disable-next-line no-param-reassign
+ if (layer.EX_GeographicBoundingBox === undefined) layer.EX_GeographicBoundingBox = parentLayer.EX_GeographicBoundingBox;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.queryable === undefined) layer.queryable = parentLayer.queryable;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.cascaded === undefined) layer.cascaded = parentLayer.cascaded;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.opaque === undefined) layer.opaque = parentLayer.opaque;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.noSubsets === undefined) layer.noSubsets = parentLayer.noSubsets;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.fixedWidth === undefined) layer.fixedWidth = parentLayer.fixedWidth;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.fixedHeight === undefined) layer.fixedHeight = parentLayer.fixedHeight;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.MinScaleDenominator === undefined) layer.MinScaleDenominator = parentLayer.MinScaleDenominator;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.MaxScaleDenominator === undefined) layer.MaxScaleDenominator = parentLayer.MaxScaleDenominator;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.BoundingBox === undefined) layer.BoundingBox = parentLayer.BoundingBox;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.Dimension === undefined) layer.Dimension = parentLayer.Dimension;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.Attribution === undefined) layer.Attribution = parentLayer.Attribution;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.MaxScaleDenominator === undefined) layer.MaxScaleDenominator = parentLayer.MaxScaleDenominator;
+ // eslint-disable-next-line no-param-reassign
+ if (layer.MaxScaleDenominator === undefined) layer.MaxScaleDenominator = parentLayer.MaxScaleDenominator;
+ // Table 7 — Inheritance of Layer properties specified in the standard with 'add' behaviour.
+ // AuthorityURL inheritance is not implemented in the following code.
+ if (parentLayer.Style) {
+ // eslint-disable-next-line no-param-reassign
+ if (!layer.Style as TypeJsonArray) (layer.Style as TypeJsonArray) = [];
+ (parentLayer.Style as TypeJsonArray).forEach((parentStyle) => {
+ const styleFound = (layer.Style as TypeJsonArray).find((styleEntry) => styleEntry.Name === parentStyle.Name);
+ if (!styleFound) (layer.Style as TypeJsonArray).push(parentStyle);
+ });
+ }
+ if (parentLayer.CRS) {
+ // eslint-disable-next-line no-param-reassign
+ if (!layer.CRS as TypeJsonArray) (layer.CRS as TypeJsonArray) = [];
+ (parentLayer.CRS as TypeJsonArray).forEach((parentCRS) => {
+ const crsFound = (layer.CRS as TypeJsonArray).find((crsEntry) => crsEntry.Name === parentCRS);
+ if (!crsFound) (layer.CRS as TypeJsonArray).push(parentCRS);
+ });
+ }
+ }
+ if (layer?.Layer !== undefined) (layer.Layer as TypeJsonArray).forEach((subLayer) => this.#processMetadataInheritance(layer, subLayer));
+ }
+
+ // #endregion PRIVATE
+ // =================
+ // #region PROTECTED
+ /**
+ * Create the layer tree using the service metadata.
+ *
+ * @returns {TypeJsonObject[]} The layer tree created from the metadata.
+ * @protected
+ */
+ protected createLayerTree(): EntryConfigBaseClass[] {
+ const metadataLayer = this.getServiceMetadata().Capability.Layer;
+ // If it is a group layer, then create it.
+ if ('Layer' in metadataLayer) {
+ // Sometime, the Name property is undefined. However, the Title property is mandatory.
+ const groupId = (metadataLayer.Name || metadataLayer.Title) as string;
+ const jsonConfig = this.#createGroupNodeJsonConfig(groupId, metadataLayer.Layer as TypeJsonArray);
+ return [this.createGroupNode(jsonConfig, this.getLanguage(), this)!];
+ }
+
+ // Create a single layer using the metadata
+ const layerConfig = toJsonObject({
+ layerId: metadataLayer.Name,
+ layerName: { en: metadataLayer.Name, fr: metadataLayer.Name },
+ });
+ return [this.createLeafNode(layerConfig, this.getLanguage(), this)!];
+ }
+
+ // #endregion PROTECTED
+ // =================
+ // #region STATIC
+ /** ****************************************************************************************************************************
+ * This method search recursively the layerId in the layer entry of the capabilities.
+ *
+ * @param {string} layerId The layer identifier that must exists on the server.
+ * @param {TypeJsonObject | undefined} layer The layer entry from the capabilities that will be searched.
+ *
+ * @returns {TypeJsonObject | null} The found layer from the capabilities or null if not found.
+ * @static
+ */
+ static getLayerMetadataEntry(layerId: string, layer: TypeJsonObject | undefined): TypeJsonObject | null {
+ // return null if the metadata doesn't have a Layer property in Capability (metadata fetch failed).
+ if (!layer) return null;
+
+ // if we have a named layer with the right name, return the layer found.
+ if ('Name' in layer && (layer.Name as string) === layerId) return layer;
+
+ if ('Layer' in layer) {
+ // if we have a layer group, search recursively in it.
+ if (Array.isArray(layer.Layer)) {
+ for (let i = 0; i < layer.Layer.length; i++) {
+ const layerFound = WmsLayerConfig.getLayerMetadataEntry(layerId, layer.Layer[i]);
+ // return the layer if it has been found.
+ if (layerFound) return layerFound;
+ }
+ // the recursive search failed, we return a null.
+ return null;
+ }
+ // If the Layer property is not an array, use a single call to the getLayerMetadataEntryto test the layer's existance.
+ return WmsLayerConfig.getLayerMetadataEntry(layerId, layer.Layer);
+ }
+
+ // If we get here, the layer doesn't exist, we return a null.
+ return null;
+ }
+ // #endregion PROTECTED
+ // #endregion METHODS
+ // #endregion CLASS DEFINITION
+}
diff --git a/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts b/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
index e0143a7da57..58ee496d0bf 100644
--- a/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/map-feature-config.ts
@@ -2,9 +2,10 @@ import cloneDeep from 'lodash/cloneDeep';
import defaultsDeep from 'lodash/defaultsDeep';
import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config';
-import { EsriDynamicLayerConfig } from '@config/types/classes/geoview-config/raster-config/esri-dynamic-config';
import { Cast, toJsonObject, TypeJsonArray, TypeJsonObject } from '@config/types/config-types';
+import { EsriDynamicLayerConfig } from '@config/types/classes/geoview-config/raster-config/esri-dynamic-config';
import { EsriFeatureLayerConfig } from '@config/types/classes/geoview-config/vector-config/esri-feature-config';
+import { WmsLayerConfig } from '@config/types/classes/geoview-config/raster-config/wms-config';
import {
CV_BASEMAP_ID,
CV_BASEMAP_LABEL,
@@ -458,8 +459,8 @@ export class MapFeatureConfig {
// return new OgcFeatureLayerConfig(layerConfig);
// case CONST_LAYER_TYPES.WFS:
// return new WfsLayerConfig(layerConfig);
- // case CONST_LAYER_TYPES.WMS:
- // return new WmsLayerConfig(layerConfig);
+ case CV_CONST_LAYER_TYPES.WMS:
+ return new WmsLayerConfig(layerConfig, language);
default:
// TODO: Restore the commented line and remove the next line when we have converted our code to the new framework.
// logger.logError(`Invalid GeoView layerType (${layerConfig.geoviewLayerType}).`);
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/esri-group-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/esri-group-layer-config.ts
index 151e29245e0..75114f42c79 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/esri-group-layer-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/esri-group-layer-config.ts
@@ -3,6 +3,8 @@ import axios from 'axios';
import { TypeJsonObject } from '@config/types/config-types';
import { GroupLayerEntryConfig } from '@config/types/classes/sub-layer-config/group-node/group-layer-entry-config';
import { Extent } from '@config/types/map-schema-types';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
+import { isvalidComparedToInternalSchema } from '@config/utils';
import { logger } from '@/core/utils/logger';
import { Projection } from '@/geo/utils/projection';
@@ -45,27 +47,34 @@ export class EsriGroupLayerConfig extends GroupLayerEntryConfig {
this.setLayerMetadata(serviceMetadata);
// parse the raw service metadata and build the geoview configuration.
this.#parseServiceMetadata();
- return;
+ } else {
+ // The layer exists and we can fetch its metadata and parse it.
+ const serviceUrl = serviceMetadata.metadataAccessPath as string;
+ const queryUrl = serviceUrl.endsWith('/') ? `${serviceUrl}${this.layerId}` : `${serviceUrl}/${this.layerId}`;
+
+ try {
+ const { data } = await axios.get(`${queryUrl}?f=json`);
+ if ('error' in data) logger.logError('Error detected while reading layer metadata.', data.error);
+ else {
+ // The metadata used are the layer metadata.
+ this.setLayerMetadata(data);
+ // Parse the raw layer metadata and build the geoview configuration.
+ this.#parseLayerMetadata();
+ return;
+ }
+ } catch (error) {
+ logger.logError('Error detected while reading Layer metadata.', error);
+ this.setErrorDetectedFlag();
+ }
}
- // The layer exists and we can fetch its metadata and parse it.
- const serviceUrl = serviceMetadata.metadataAccessPath as string;
- const queryUrl = serviceUrl.endsWith('/') ? `${serviceUrl}${this.layerId}` : `${serviceUrl}/${this.layerId}`;
-
- try {
- const { data } = await axios.get(`${queryUrl}?f=json`);
- if ('error' in data) logger.logError('Error detected while reading layer metadata.', data.error);
- else {
- // The metadata used are the layer metadata.
- this.setLayerMetadata(data);
- // Parse the raw layer metadata and build the geoview configuration.
- this.#parseLayerMetadata();
- return;
- }
- } catch (error) {
- logger.logError('Error detected while reading Layer metadata.', error);
+ await this.fetchListOfLayerMetadata();
+
+ if (!isvalidComparedToInternalSchema(this.getSchemaPath(), this, true)) {
+ throw new GeoviewLayerConfigError(
+ `GeoView internal configuration ${this.getLayerPath()} is invalid compared to the internal schema specification.`
+ );
}
- this.setErrorDetectedFlag();
}
// #endregion OVERRIDE
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/group-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/group-layer-entry-config.ts
index 279d4664927..6005421f7d7 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/group-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/group-layer-entry-config.ts
@@ -79,6 +79,26 @@ export abstract class GroupLayerEntryConfig extends EntryConfigBaseClass {
}
// #endregion OVERRIDE
+ // #region PROTECTED
+ /**
+ * Fetch the metadata of all layer entry configurations defined in the layer tree.
+ *
+ * @returns {Promise} A promise that will resolve when the process has completed.
+ * @protected @async
+ */
+ protected async fetchListOfLayerMetadata(): Promise {
+ const arrayOfLayerPromises: Promise[] = [];
+ this.listOfLayerEntryConfig.forEach((subLayerConfig) => {
+ arrayOfLayerPromises.push(subLayerConfig.fetchLayerMetadata());
+ });
+
+ const awaitedPromises = await Promise.allSettled(arrayOfLayerPromises);
+ awaitedPromises.forEach((promise, i) => {
+ if (promise.status === 'rejected') this.listOfLayerEntryConfig[i].setErrorDetectedFlag();
+ });
+ }
+ // #endregion PROTECTED
+
// #region PUBLIC
/**
* Scan the list of sublayers for duplicates. If duplicates exist, mark them as an error layer.
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wms-group-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wms-group-layer-config.ts
new file mode 100644
index 00000000000..c9b97ef88f4
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wms-group-layer-config.ts
@@ -0,0 +1,84 @@
+import { GroupLayerEntryConfig } from '@config/types/classes/sub-layer-config/group-node/group-layer-entry-config';
+import { Extent } from '@config/types/map-schema-types';
+import { WmsLayerConfig } from '@config/types/classes/geoview-config/raster-config/wms-config';
+import { isvalidComparedToInternalSchema } from '@config/utils';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
+
+import { validateExtentWhenDefined } from '@/geo/utils/utilities';
+
+// ========================
+// #region CLASS HEADER
+/**
+ * Base type used to define the common implementation of an ESRI GeoView sublayer to display on the map.
+ */
+export class WmsGroupLayerConfig extends GroupLayerEntryConfig {
+ // ===============
+ // #region METHODS
+ /*
+ * Methods are listed in the following order: abstract, override, private, protected, public and static.
+ */
+
+ // ================
+ // #region OVERRIDE
+ /** ***************************************************************************************************************************
+ * This method is used to fetch, parse and extract the relevant information from the metadata of the group layer.
+ * The same method signature is used by layer group nodes and leaf nodes (layers).
+ * @override @async
+ */
+ override async fetchLayerMetadata(): Promise {
+ // If an error has already been detected, then the layer is unusable.
+ if (this.getErrorDetectedFlag()) return;
+
+ // WMS service metadata contains the layer's metadata. We don't have to fetch again.
+ const layerMetadata = WmsLayerConfig.getLayerMetadataEntry(
+ this.layerId,
+ this.getGeoviewLayerConfig().getServiceMetadata().Capability.Layer
+ );
+ if (layerMetadata) {
+ // The layer group exists in the service metadata, use its metadata
+ this.setLayerMetadata(layerMetadata);
+ } else {
+ // User defined layer groups cannot be found in the service metadata, we will use the topmost layer's metadata.
+ this.setLayerMetadata(this.getGeoviewLayerConfig().getServiceMetadata().Capability.Layer);
+ }
+
+ // Parse the raw layer metadata and build the geoview configuration.
+ this.#parseLayerMetadata();
+
+ await this.fetchListOfLayerMetadata();
+
+ if (!isvalidComparedToInternalSchema(this.getSchemaPath(), this, true)) {
+ throw new GeoviewLayerConfigError(
+ `GeoView internal configuration ${this.getLayerPath()} is invalid compared to the internal schema specification.`
+ );
+ }
+ }
+
+ // #endregion OVERRIDE
+
+ // ===============
+ // #region PRIVATE
+ /** ***************************************************************************************************************************
+ * This method is used to analyze metadata and extract the relevant information from a group layer based on a definition
+ * provided by the WMS service.
+ * @private
+ */
+ #parseLayerMetadata(): void {
+ const layerMetadata = this.getLayerMetadata();
+
+ this.layerName = layerMetadata.Title as string;
+
+ if (layerMetadata?.Attribution?.Title) this.attributions.push(layerMetadata.Attribution.Title as string);
+
+ this.initialSettings.states!.queryable = (layerMetadata.queryable || false) as boolean;
+
+ this.minScale = (layerMetadata.MinScaleDenominator as number) || 0;
+ this.maxScale = (layerMetadata.MaxScaleDenominator as number) || 0;
+
+ this.initialSettings.extent = validateExtentWhenDefined(layerMetadata.EX_GeographicBoundingBox as Extent);
+ this.initialSettings.bounds = this.initialSettings.extent;
+ }
+ // #endregion PRIVATE
+ // #endregion METHODS
+ // #endregion CLASS DEFINITION
+}
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts
index e8b25095262..e15848541e9 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts
@@ -8,6 +8,8 @@ import { logger } from '@/core/utils/logger';
import { DateMgt, TimeDimensionESRI } from '@/core/utils/date-mgt';
import { Projection } from '@/geo/utils/projection';
import { validateExtentWhenDefined } from '@/geo/utils/utilities';
+import { isvalidComparedToInternalSchema } from '@/api/config/utils';
+import { GeoviewLayerConfigError } from '../../config-exceptions';
// ========================
// #region CLASS DEFINITION
@@ -45,6 +47,12 @@ export abstract class AbstractBaseEsriLayerEntryConfig extends AbstractBaseLayer
this.setLayerMetadata(data);
// Parse the raw layer metadata and build the geoview configuration.
this.parseLayerMetadata();
+
+ if (!isvalidComparedToInternalSchema(this.getSchemaPath(), this, true)) {
+ throw new GeoviewLayerConfigError(
+ `GeoView internal configuration ${this.getLayerPath()} is invalid compared to the internal schema specification.`
+ );
+ }
return;
}
} catch (error) {
@@ -142,7 +150,7 @@ export abstract class AbstractBaseEsriLayerEntryConfig extends AbstractBaseLayer
name: fieldEntry.name,
alias: fieldEntry.alias,
type: AbstractBaseEsriLayerEntryConfig.#convertEsriFieldType(fieldEntry.type as string),
- domaine: Cast(fieldEntry.domain),
+ domain: Cast(fieldEntry.domain),
})
);
});
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config.ts
index 128f76bd76c..c808f7c1c11 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config.ts
@@ -1,4 +1,4 @@
-import { TypeBaseSourceInitialConfig, TypeTemporalDimension, TypeStyleGeometry, TypeLayerEntryType } from '@config/types/map-schema-types';
+import { TypeTemporalDimension, TypeStyleGeometry, TypeLayerEntryType } from '@config/types/map-schema-types';
import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';
// ========================
@@ -12,9 +12,6 @@ export abstract class AbstractBaseLayerEntryConfig extends EntryConfigBaseClass
/** The geometry type of the leaf node. */
geometryType?: TypeStyleGeometry;
- /** Source settings to apply to the GeoView vector layer source at creation time. */
- source?: TypeBaseSourceInitialConfig;
-
/** Optional temporal dimension. */
temporalDimension?: TypeTemporalDimension;
// #endregion PUBLIC PROPERTIES
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/esri-dynamic-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/esri-dynamic-layer-entry-config.ts
index 4343e55b210..5bc0dae804e 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/esri-dynamic-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/esri-dynamic-layer-entry-config.ts
@@ -15,20 +15,21 @@ import { EsriBaseRenderer, createStyleUsingEsriRenderer } from '@/api/config/esr
/**
* The ESRI dynamic geoview sublayer class.
*/
+
export class EsriDynamicLayerEntryConfig extends AbstractBaseEsriLayerEntryConfig {
// =========================
- // #region PUBLIC PROPERTIES
+ // #region PROPERTIES
/** Source settings to apply to the GeoView image layer source at creation time. */
declare source: TypeSourceEsriDynamicInitialConfig;
/** Style to apply to the raster layer. */
style?: TypeStyleConfig;
- // #endregion PUBLIC PROPERTIES
+ // #endregion PROPERTIES
// ===============
// #region METHODS
/*
- * Methods are listed in the following order: abstract, override, private, protected and public.
+ * Methods are listed in the following order: abstract, override, private, protected, public and static.
*/
// ==========================
// #region OVERRIDE
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/wms-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/wms-layer-entry-config.ts
new file mode 100644
index 00000000000..dfcf88e1693
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/raster/wms-layer-entry-config.ts
@@ -0,0 +1,148 @@
+import { CV_CONST_SUB_LAYER_TYPES, CV_CONST_LEAF_LAYER_SCHEMA_PATH } from '@config/types/config-constants';
+import { TypeJsonArray, TypeJsonObject } from '@config/types/config-types';
+import { TypeStyleConfig, TypeLayerEntryType, TypeSourceWmsInitialConfig, Extent } from '@config/types/map-schema-types';
+import { AbstractBaseLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config';
+import { WmsLayerConfig } from '@config/types/classes/geoview-config/raster-config/wms-config';
+import { isvalidComparedToInternalSchema } from '@config/utils';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
+
+import { logger } from '@/core/utils/logger';
+import { DateMgt } from '@/core/utils/date-mgt';
+
+// ========================
+// #region CLASS DEFINITION
+/**
+ * The OGC WMS geoview sublayer class.
+ */
+
+export class WmsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
+ // =========================
+ // #region PROPERTIES
+ /** Source settings to apply to the GeoView image layer source at creation time. */
+ declare source: TypeSourceWmsInitialConfig;
+
+ /** Style to apply to the raster layer. */
+ style?: TypeStyleConfig;
+ // #endregion PROPERTIES
+
+ // ===============
+ // #region METHODS
+ /*
+ * Methods are listed in the following order: abstract, override, private, protected, public and static.
+ */
+ // ==========================
+ // #region OVERRIDE
+
+ /**
+ * The getter method that returns the schemaPath property. Each geoview sublayer type knows what section of the schema must be
+ * used to do its validation.
+ *
+ * @returns {string} The schemaPath associated to the sublayer.
+ * @protected @override
+ */
+ protected override getSchemaPath(): string {
+ return CV_CONST_LEAF_LAYER_SCHEMA_PATH.WMS;
+ }
+
+ /**
+ * A method that returns the entryType property. Each sublayer knows what entry type is associated to it.
+ *
+ * @returns {TypeLayerEntryType} The entryType associated to the sublayer.
+ * @protected @override
+ */
+ protected override getEntryType(): TypeLayerEntryType {
+ return CV_CONST_SUB_LAYER_TYPES.RASTER_IMAGE;
+ }
+
+ /**
+ * This method is used to fetch, parse and extract the relevant information from the metadata of the leaf node.
+ * The same method signature is used by layer group nodes and leaf nodes (layers).
+ * @override
+ */
+ override fetchLayerMetadata(): Promise {
+ // If an error has already been detected, then the layer is unusable.
+ if (this.getErrorDetectedFlag()) return Promise.resolve();
+
+ // WMS service metadata contains the layer's metadata.
+ const layerMetadata = WmsLayerConfig.getLayerMetadataEntry(
+ this.layerId,
+ this.getGeoviewLayerConfig().getServiceMetadata().Capability.Layer
+ );
+ if (layerMetadata) {
+ this.setLayerMetadata(layerMetadata);
+ // Parse the raw layer metadata and build the geoview configuration.
+ this.#parseLayerMetadata();
+
+ if (!isvalidComparedToInternalSchema(this.getSchemaPath(), this, true)) {
+ throw new GeoviewLayerConfigError(
+ `GeoView internal configuration ${this.getLayerPath()} is invalid compared to the internal schema specification.`
+ );
+ }
+
+ return Promise.resolve();
+ }
+
+ logger.logError(`Can't find layer's metadata for layerPath ${this.getLayerPath()}.`);
+ this.setErrorDetectedFlag();
+ return Promise.resolve();
+ }
+
+ /**
+ * Apply default values. The default values will be overwritten by the values in the metadata when they are analyzed.
+ * The resulting config will then be overwritten by the values provided in the user config.
+ * @override
+ */
+ override applyDefaultValues(): void {
+ super.applyDefaultValues();
+ this.source = {
+ crossOrigin: 'Anonymous',
+ serverType: 'mapserver',
+ projection: 3978,
+ featureInfo: {
+ queryable: false,
+ nameField: '',
+ outfields: [],
+ },
+ };
+ }
+ // #endregion OVERRIDE
+
+ // ===============
+ // #region PRIVATE
+ /**
+ * This method is used to parse the layer metadata and extract the source information and other properties.
+ * @protected
+ */
+ #parseLayerMetadata(): void {
+ const layerMetadata = this.getLayerMetadata();
+
+ if (layerMetadata?.Attribution?.Title) this.attributions.push(layerMetadata.Attribution.Title as string);
+
+ this.bounds = layerMetadata.EX_GeographicBoundingBox as Extent;
+
+ if (layerMetadata.queryable) this.source.featureInfo!.queryable = layerMetadata.queryable as boolean;
+
+ this.source.wmsStyle = layerMetadata.Style
+ ? ((layerMetadata.Style as TypeJsonArray).map((style) => {
+ return style.Name;
+ }) as string[])
+ : undefined;
+
+ this.#processTemporalDimension(layerMetadata.Dimension);
+ }
+
+ /** ***************************************************************************************************************************
+ * This method will create a Geoview temporal dimension if it existds in the service metadata
+ * @param {TypeJsonObject} wmsDimension The WMS time dimension object
+ * @protected
+ */
+ #processTemporalDimension(wmsDimension: TypeJsonObject): void {
+ if (wmsDimension) {
+ const temporalDimension: TypeJsonObject | undefined = (wmsDimension as TypeJsonArray).find((dimension) => dimension.name === 'time');
+ if (temporalDimension) this.temporalDimension = DateMgt.createDimensionFromOGC(temporalDimension);
+ }
+ }
+ // #endregion PRIVATE
+ // #endregion METHODS
+ // #endregion CLASS DEFINITION
+}
diff --git a/packages/geoview-core/src/api/config/types/config-constants.ts b/packages/geoview-core/src/api/config/types/config-constants.ts
index 923f23cc158..fdacfcfeb13 100644
--- a/packages/geoview-core/src/api/config/types/config-constants.ts
+++ b/packages/geoview-core/src/api/config/types/config-constants.ts
@@ -82,7 +82,7 @@ export const CV_GEOVIEW_SCHEMA_PATH: Record = {
VECTOR_TILES: '',
OGC_FEATURE: '',
WFS: '',
- WMS: '',
+ WMS: 'https://cgpv/schema#/definitions/WmsLayerConfig',
};
export const CV_MAP_CONFIG_SCHEMA_PATH = 'https://cgpv/schema#/definitions/MapFeatureConfig';
export const CV_LAYER_GROUP_SCHEMA_PATH = 'https://cgpv/schema#/definitions/GroupLayerEntryConfig';
diff --git a/packages/geoview-core/src/api/config/types/config-validation-schema.json b/packages/geoview-core/src/api/config/types/config-validation-schema.json
index 727358c76cc..72d7e36d061 100644
--- a/packages/geoview-core/src/api/config/types/config-validation-schema.json
+++ b/packages/geoview-core/src/api/config/types/config-validation-schema.json
@@ -315,8 +315,9 @@
"description": "Universal map settings",
"properties": {
"canRemoveSublayers": {
+ "description": "Whether or not sublayers can be removed from layer groups.",
"type": "boolean",
- "description": "Whether or not sublayers can be removed from layer groups. Default = true."
+ "default": true
}
}
},
@@ -398,16 +399,19 @@
"$ref": "#/definitions/TypeGeoviewLayerType"
},
"serviceDateFormat": {
- "description": "Date format used by the service endpoint (default 'DD/MM/YYYY HH:MM:SSZ').",
- "type": "string"
+ "description": "Date format used by the service endpoint.",
+ "type": "string",
+ "default": "DD/MM/YYYY HH:MM:SSZ"
},
"externalDateFormat": {
- "description": "Date format used by the getFeatureInfo to output date variable (default 'DD/MM/YYYY HH:MM:SSZ').",
- "type": "string"
+ "description": "Date format used by the getFeatureInfo to output date variable.",
+ "type": "string",
+ "default": "DD/MM/YYYY HH:MM:SSZ"
},
"isTimeAware": {
+ "description": "Flag to mention if layer will use its time dimension if provided. Used to remove a layer from time enabled functions like time slider.",
"type": "boolean",
- "description": "Flag to mention if layer will use its time dimension if provided (default: true). Used to remove a layer from time enabled functions like time slider."
+ "default": true
},
"listOfLayerEntryConfig": {
"description": "The layer entries to use from the GeoView layer.",
@@ -557,8 +561,9 @@
"default": 1
},
"projection": {
+ "description": "The projection code of the coordinates.",
"type": "number",
- "description": "The projection code of the coordinates. Default value is 4326."
+ "default": 4326
}
},
"required": ["id", "coordinate"]
@@ -815,6 +820,24 @@
}
]
},
+ "WmsLayerConfig": {
+ "description": "Structure used by the viewer to describe the configuration of a WMS layer.",
+ "type": "object",
+ "allOf": [
+ {
+ "description": "The parent class.",
+ "$ref": "#/definitions/AbstractGeoviewLayerConfig"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "geoviewLayerType": {
+ "enum": ["ogcWms"]
+ }
+ }
+ }
+ ]
+ },
"EntryConfigBaseClass": {
"description": "Base class from which we derive all the nodes (group and leaves) in the layer tree.",
"type": "object",
@@ -859,12 +882,12 @@
}
},
"minScale": {
- "description": "Minimum scale the layer can display (default: 0).",
+ "description": "Minimum scale the layer can display.",
"type": "number",
"default": 0
},
"maxScale": {
- "description": "Maximum scale the layer can display (default: 0).",
+ "description": "Maximum scale the layer can display.",
"type": "number",
"default": 0
},
@@ -987,7 +1010,7 @@
}
},
"entryType": {
- "enum": ["raster-tile"]
+ "enum": ["raster-image"]
},
"source": {
"$ref": "#/definitions/TypeSourceEsriDynamicInitialConfig"
@@ -1027,6 +1050,27 @@
}
]
},
+ "OgcWmsLayerEntryConfig": {
+ "description": "Class from which we derive all the WMS leaf nodes in the layer tree.",
+ "type": "object",
+ "allOf": [
+ {
+ "description": "The parent class.",
+ "$ref": "#/definitions/AbstractBaseLayerEntryConfig"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "source": {
+ "$ref": "#/definitions/TypeSourceWmsInitialConfig"
+ },
+ "style": {
+ "$ref": "#/definitions/TypeStyleConfig"
+ }
+ }
+ }
+ ]
+ },
"TypeStyleGeometry": {
"description": "Valid keys for the geometryType property.",
"enum": ["point", "linestring", "polygon"]
@@ -1040,7 +1084,8 @@
},
"crossOrigin": {
"description": "The crossOrigin attribute if needed to load the data.",
- "type": "string"
+ "type": "string",
+ "default": "Anonymous"
}
}
},
@@ -1064,47 +1109,38 @@
}
]
},
- "TypeSourceImageWmsInitialConfig": {
+ "TypeSourceWmsInitialConfig": {
"description": "Initial settings for tile image sources.",
- "additionalProperties": false,
"type": "object",
"allOf": [
{
"$ref": "#/definitions/TypeBaseSourceInitialConfig"
},
{
- "additionalProperties": false,
"type": "object",
"properties": {
"featureInfo": {
"$ref": "#/definitions/TypeFeatureInfoLayerConfig"
},
"serverType": {
- "$ref": "#/definitions/TypeOfServer",
- "description": "The type of the remote WMS server. The default value is mapserver."
+ "description": "The type of the remote WMS server.",
+ "$ref": "#/definitions/TypeOfServer"
},
- "style": {
- "oneOf": [
- {
- "type": "string",
- "description": "Single style to apply"
- },
- {
- "type": "array",
- "items": {
- "type": "string"
- },
- "description": "Array of style to choose."
- }
- ]
+ "wmsStyle": {
+ "description": "Array of style to choose from.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
}
]
},
"TypeOfServer": {
- "description": "The type of the remote WMS server. The default value is mapserver.",
- "enum": ["mapserver", "geoserver", "qgis"]
+ "description": "The type of the remote WMS server.",
+ "enum": ["mapserver", "geoserver", "qgis"],
+ "default": "mapserver"
},
"TypeSourceImageStaticInitialConfig": {
"description": "Initial settings for static image sources.",
@@ -1142,8 +1178,9 @@
"type": "object",
"properties": {
"maxRecordCount": {
- "description": "Maximum number of records to fetch (default: 0).",
- "type": "number"
+ "description": "Maximum number of records to fetch.",
+ "type": "number",
+ "default": 0
},
"layerFilter": {
"description": "Filter to apply on features of this layer.",
@@ -1156,9 +1193,9 @@
"$ref": "#/definitions/TypeVectorSourceFormats"
},
"strategy": {
+ "description": "The loading strategy to use. By default an all strategy is used, a one-off strategy which loads all features at once.",
"enum": ["all", "bbox"],
- "default": "all",
- "description": "The loading strategy to use. By default an all strategy is used, a one-off strategy which loads all features at once."
+ "default": "all"
}
}
}
@@ -1261,8 +1298,9 @@
"type": "object",
"properties": {
"maxRecordCount": {
- "description": "Maximum number of records to fetch (default: 0).",
- "type": "number"
+ "description": "Maximum number of records to fetch.",
+ "type": "number",
+ "default": 0
},
"layerFilter": {
"description": "Filter to apply on features of this layer.",
@@ -1284,7 +1322,7 @@
]
},
"TypeEsriFormatParameter": {
- "description": "The format of the exported image. The default format is png.",
+ "description": "The format of the exported image.",
"enum": ["png", "jpg", "gif", "svg"],
"default": "png"
},
@@ -1299,8 +1337,9 @@
"type": "object",
"properties": {
"maxRecordCount": {
- "description": "Maximum number of records to fetch (default: 0).",
- "type": "number"
+ "description": "Maximum number of records to fetch.",
+ "type": "number",
+ "default": 0
},
"layerFilter": {
"description": "Filter to apply on features of this layer.",
@@ -1320,7 +1359,7 @@
"type": "object",
"properties": {
"queryable": {
- "description": "Allow querying. Default = false.",
+ "description": "Allow querying.",
"type": "boolean",
"default": false
},
@@ -1350,11 +1389,21 @@
},
"type": {
"description": "The field type.",
- "type": "string"
+ "$ref": "#/definitions/TypeOutfieldsType"
},
"domain": {
- "description": "An array of values that constitute the domain.",
- "type": "array"
+ "description": "The domain of values.",
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/definitions/codedValueType"
+ },
+ {
+ "$ref": "#/definitions/rangeDomainType"
+ }
+ ]
}
},
"required": ["name", "alias", "type", "domain"]
@@ -1364,6 +1413,64 @@
"additionalProperties": false,
"enum": ["string", "number", "date", "url"]
},
+ "codedValueType": {
+ "description": "Coded value definition.",
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": ["codedValue"]
+ },
+ "name": {
+ "description": "The domain name.",
+ "type": "string"
+ },
+ "description": {
+ "description": "A description of the domain.",
+ "type": "string"
+ },
+ "codedValues": {
+ "description": "The list of coded values.",
+ "$ref": "#/definitions/codeValueEntryType"
+ }
+ }
+ },
+ "codeValueEntryType": {
+ "description": "The structure of a code value.",
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "code": {
+ "description": "The code property has an unknown type."
+ }
+ }
+ },
+ "rangeDomainType": {
+ "description": "The structure of a range domain.",
+ "additionalProperties": false,
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": ["range"]
+ },
+ "name": {
+ "description": "The domain name.",
+ "type": "string"
+ },
+ "range": {
+ "type":"array",
+ "minItems": 2,
+ "maxItems": 2,
+ "items": {
+ "description": "The code property has an unknown type."
+ }
+ }
+ },
+ "required": ["type", "name", "range"]
+ },
"TypeLayerEntryType": {
"description": "Layer entry data type.",
"enum": ["vector", "vector-tile", "raster-tile", "raster-image", "group"]
@@ -1420,8 +1527,7 @@
"type": "array",
"items": {
"type": "string"
- },
- "minItems": 1
+ }
},
"hasDefault": {
"description": "Flag used to indicate that the symbology has a default value.",
@@ -1444,14 +1550,15 @@
"type": "object",
"properties": {
"visible": {
- "description": "Flag used to show/hide features associated to the label (default: true).",
- "type": "boolean"
+ "description": "Flag used to show/hide features associated to the label.",
+ "type": "boolean",
+ "default": true
},
"label": {
"type": "string"
},
"values": {
- "description": "Simple type has a single value at index 0; uniqueValue type has many entries (up to 3 for ESRI) and classBreaks type has two entries (index 0 for min and index 1 for max).",
+ "description": "Simple type has an empty array; uniqueValue type has many entries (up to 3 for ESRI) and classBreaks type has two entries (index 0 for min and index 1 for max).",
"type": "array",
"items": {
"oneOf": [
@@ -1462,8 +1569,7 @@
"type": "number"
}
]
- },
- "minItems": 1
+ }
},
"settings": {
"$ref": "#/definitions/TypeKindOfVectorSettings"
@@ -1583,16 +1689,19 @@
"$ref": "#/definitions/TypeStrokeSymbolConfig"
},
"paternSize": {
- "description": "Distance between patern lines. Default = 8",
- "type": "number"
+ "description": "Distance between patern lines.",
+ "type": "number",
+ "default": 8
},
"paternWidth": {
- "description": "Patern line width.default = 1.",
- "type": "number"
+ "description": "Patern line width.",
+ "type": "number",
+ "default": 1
},
"fillStyle": {
- "description": "Kind of filling for vector features. Default = solid. ",
- "$ref": "#/definitions/TypeFillStyle"
+ "description": "Kind of filling for vector features.",
+ "$ref": "#/definitions/TypeFillStyle",
+ "default": "solid"
}
}
}
@@ -1693,7 +1802,8 @@
},
"crossOrigin": {
"description": "The crossOrigin attribute for loaded images. Note that you must provide a crossOrigin value if you want to access pixel data with the Canvas renderer.",
- "type": "string"
+ "type": "string",
+ "default": "Anonymous"
}
}
}
diff --git a/packages/geoview-core/src/api/config/types/map-schema-types.ts b/packages/geoview-core/src/api/config/types/map-schema-types.ts
index 667b1e27b91..41ac805c82f 100644
--- a/packages/geoview-core/src/api/config/types/map-schema-types.ts
+++ b/packages/geoview-core/src/api/config/types/map-schema-types.ts
@@ -185,7 +185,7 @@ export type TypeViewSettings = {
enableRotation?: boolean;
/**
* The initial rotation for the view in degree (positive rotation clockwise, 0 means North). Will be converted to radiant by
- * the viewer. Domaine = [0..360], default = 0.
+ * the viewer. Domain = [0..360], default = 0.
*/
rotation?: number;
/** The extent that constrains the view. Called with [minX, minY, maxX, maxY] extent coordinates.
@@ -194,12 +194,12 @@ export type TypeViewSettings = {
maxExtent?: Extent;
/**
* The minimum zoom level used to determine the resolution constraint. If not set, will use default from basemap.
- * Domaine = [0..50].
+ * Domain = [0..50].
*/
minZoom?: number;
/**
* The maximum zoom level used to determine the resolution constraint. If not set, will use default from basemap.
- * Domaine = [0..50].
+ * Domain = [0..50].
*/
maxZoom?: number;
/**
@@ -378,13 +378,13 @@ export interface TypeSourceTileInitialConfig extends TypeBaseSourceInitialConfig
}
/** Initial settings for WMS image sources. */
-export interface TypeSourceImageWmsInitialConfig extends TypeBaseSourceInitialConfig {
+export interface TypeSourceWmsInitialConfig extends TypeBaseSourceInitialConfig {
/** Definition of the feature information structure that will be used by the getFeatureInfo method. */
featureInfo?: TypeFeatureInfoLayerConfig;
/** The type of the remote WMS server. The default value is mapserver. */
serverType?: TypeOfServer;
/** Style to apply. Default = '' */
- style?: string | string[];
+ wmsStyle?: string[];
}
/** Type of server. */
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 fec3799827a..b6ef0bb7bac 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
@@ -117,7 +117,7 @@ export class WMS extends AbstractGeoViewRaster {
*
* @returns {Promise} A promise that the execution is completed.
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
protected override async fetchServiceMetadata(): Promise {
const metadataUrl = getLocalizedValue(this.metadataAccessPath, AppEventProcessor.getDisplayLanguage(this.mapId));
if (metadataUrl) {
@@ -195,7 +195,7 @@ export class WMS extends AbstractGeoViewRaster {
* @returns {Promise} A promise that the execution is completed.
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
async #getServiceMetadata(url: string): Promise {
try {
const response = await fetch(url);
@@ -217,7 +217,7 @@ export class WMS extends AbstractGeoViewRaster {
* @returns {Promise} A promise that the execution is completed.
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
async #fetchXmlServiceMetadata(metadataUrl: string): Promise {
try {
const parser = new WMSCapabilities();
@@ -261,7 +261,7 @@ export class WMS extends AbstractGeoViewRaster {
* @returns {number[]} An array containing the path to the layer or [] if not found.
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
#getMetadataLayerPath(layerName: string, layerProperty: TypeJsonObject, pathToTheParentLayer: number[] = []): number[] {
const newLayerPath = [...pathToTheParentLayer];
if (Array.isArray(layerProperty)) {
@@ -294,7 +294,7 @@ export class WMS extends AbstractGeoViewRaster {
* @param {TypeJsonObject} layerToAdd The layer property to add
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
#addLayerToMetadataInstance(
metadataLayerPathToAdd: number[],
metadataLayer: TypeJsonObject | undefined,
@@ -323,7 +323,7 @@ export class WMS extends AbstractGeoViewRaster {
* @returns {TypeLayerEntryConfig[]} The array of layer configurations.
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
#getLayersToQuery(): TypeLayerEntryConfig[] {
const arrayOfLayerIds: TypeLayerEntryConfig[] = [];
const gatherLayerIds = (listOfLayerEntryConfig = this.listOfLayerEntryConfig): void => {
@@ -345,7 +345,7 @@ export class WMS extends AbstractGeoViewRaster {
* @param {TypeJsonObject | undefined} layer The layer property from the metadata that will inherit the values
* @private
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
#processMetadataInheritance(parentLayer?: TypeJsonObject, layer: TypeJsonObject | undefined = this.metadata?.Capability?.Layer): void {
if (parentLayer && layer) {
// Table 7 — Inheritance of Layer properties specified in the standard with 'replace' behaviour.
@@ -438,11 +438,11 @@ export class WMS extends AbstractGeoViewRaster {
* @param {AbstractBaseLayerEntryConfig} layerConfig The layer configurstion associated to the dynamic group.
* @private
*/
- // TODO: Refactor - Layers Refactoring - Check here for layer metadata config vs layers
- // GV Layers Refactoring - Obsolete (in config? in layers?)
+ // GV Layers Refactoring - Obsolete (in config)
#createGroupLayer(layer: TypeJsonObject, layerConfig: AbstractBaseLayerEntryConfig): void {
// TODO: Refactor - createGroup is the same thing for all the layers type? group is a geoview structure.
// TO.DOCONT: Should it be handle upper in abstract class to loop in structure and launch the creation of a leaf?
+ // TODO: The answer is no. Even if the final structure is the same, the input structure is different for each geoview layer types.
const newListOfLayerEntryConfig: TypeLayerEntryConfig[] = [];
const arrayOfLayerMetadata = Array.isArray(layer.Layer) ? layer.Layer : ([layer.Layer] as TypeJsonArray);
@@ -483,7 +483,7 @@ export class WMS extends AbstractGeoViewRaster {
* @private
*/
// TODO: Refactor - Layers Refactoring - Check here for layer metadata config vs layers
- // GV Layers Refactoring - Obsolete (in config? in layers?)
+ // GV Layers Refactoring - Obsolete (in config)
#getLayerMetadataEntry(layerId: string, layer: TypeJsonObject | undefined = this.metadata?.Capability?.Layer): TypeJsonObject | null {
if (!layer) return null;
if ('Name' in layer && (layer.Name as string) === layerId) return layer;
@@ -657,7 +657,7 @@ export class WMS extends AbstractGeoViewRaster {
* @param {TypeJsonObject} wmsTimeDimension The WMS time dimension object
* @param {OgcWmsLayerEntryConfig} layerConfig The layer entry to configure
*/
- // GV Layers Refactoring - Obsolete (in config?)
+ // GV Layers Refactoring - Obsolete (in config)
protected processTemporalDimension(wmsTimeDimension: TypeJsonObject, layerConfig: OgcWmsLayerEntryConfig): void {
if (wmsTimeDimension !== undefined) {
this.setTemporalDimension(layerConfig.layerPath, DateMgt.createDimensionFromOGC(wmsTimeDimension));
diff --git a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts
index 5de9a4a6227..82235deecff 100644
--- a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts
+++ b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts
@@ -61,6 +61,8 @@ export function esriGetFieldType(
* @param {string} fieldName field name for which we want to get the domain.
* @returns {codedValueType | rangeDomainType | null} The domain of the field.
*/
+// TODO: ESRI domains are translated to GeoView domains in the configuration. Any GeoView layer that support geoview domains can
+// TODO.CONT: call a method getFieldDomain that use config.source.featureInfo.outfields to find a field domain.
export function esriGetFieldDomain(
layerConfig: EsriDynamicLayerEntryConfig | EsriFeatureLayerEntryConfig | EsriImageLayerEntryConfig,
fieldName: string