ogcWms: 'https://geo.weather.gc.ca/geomet',
ogcWfs: 'https://ahocevar.com/geoserver/wfs?REQUEST=GetCapabilities&VERSION=2.0.0&SERVICE=WFS',
esriImage: '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',
+ GeoJSON: 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/geojson/metadata-new.meta',
};
// Get the GeoView Layer Type drop-down object used to select the type of layer.
@@ -869,7 +870,7 @@
Sanbox Map
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',
+ GeoJSON: 'https://canadian-geospatial-platform.github.io/geoview/public/datasets/geojson/metadata-new.meta',
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',
diff --git a/packages/geoview-core/src/api/config/config-api.ts b/packages/geoview-core/src/api/config/config-api.ts
index ff65573c774..01a1d6e4676 100644
--- a/packages/geoview-core/src/api/config/config-api.ts
+++ b/packages/geoview-core/src/api/config/config-api.ts
@@ -1,5 +1,4 @@
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';
@@ -54,13 +53,19 @@ export class ConfigApi {
* @returns {string | undefined} The GeoView layer type or undefined if it cannot be guessed.
*/
static guessLayerType(url: string): string | undefined {
- const upperUrl = url.toUpperCase();
+ if (!url) return undefined;
+
+ const urlItems = url.toUpperCase().split('?');
+ const [upperUrl] = urlItems;
+ const upperParams = urlItems[1] || '';
+ const upperParamArray = upperParams ? upperParams.split('&') : [];
const urlTokens = upperUrl.split('/');
const layerIdString = urlTokens[urlTokens.length - 1];
// GV: Important - Testing for NaN after parseInt is not a good way to check whether a string is a number, as parseInt('1a2', 10)
// GV: returns 1 instead of NaN. To be detected as NaN, the string passed to parseInt must not begin with a number.
// GV: Regex /^\d+$/ is used instead to check whether a string is a number.
const layerId = /^\d+$/.test(layerIdString) ? parseInt(layerIdString, 10) : Number.NaN;
+
if (upperUrl.endsWith('MAPSERVER') || upperUrl.endsWith('MAPSERVER/')) return CV_CONST_LAYER_TYPES.ESRI_DYNAMIC;
if (upperUrl.indexOf('FEATURESERVER') !== -1 || (upperUrl.indexOf('MAPSERVER') !== -1 && !Number.isNaN(layerId)))
@@ -68,20 +73,24 @@ export class ConfigApi {
if (upperUrl.indexOf('IMAGESERVER') !== -1) return CV_CONST_LAYER_TYPES.ESRI_IMAGE;
- if (urlTokens.indexOf('WFS') !== -1) return CV_CONST_LAYER_TYPES.WFS;
+ if (upperParamArray.indexOf('SERVICE=WFS') !== -1 || upperUrl.indexOf('WFS') !== -1) return CV_CONST_LAYER_TYPES.WFS;
- if (upperUrl.endsWith('.JSON') || upperUrl.endsWith('.GEOJSON')) return CV_CONST_LAYER_TYPES.GEOJSON;
+ if (upperUrl.endsWith('.META') || upperUrl.endsWith('.JSON') || upperUrl.endsWith('.GEOJSON')) return CV_CONST_LAYER_TYPES.GEOJSON;
if (upperUrl.endsWith('.GPKG')) return CV_CONST_LAYER_TYPES.GEOPACKAGE;
+ if (upperUrl.includes('VECTORTILESERVER')) return CV_CONST_LAYER_TYPES.VECTOR_TILES;
+
if (upperUrl.indexOf('{Z}/{X}/{Y}') !== -1 || upperUrl.indexOf('{Z}/{Y}/{X}') !== -1) return CV_CONST_LAYER_TYPES.XYZ_TILES;
if (ConfigApi.isValidUUID(url)) return CV_CONFIG_GEOCORE_TYPE;
- if (upperUrl.indexOf('WMS') !== -1) return CV_CONST_LAYER_TYPES.WMS;
+ if (upperParamArray.indexOf('SERVICE=WMS') !== -1 || upperUrl.indexOf('WMS') !== -1) return CV_CONST_LAYER_TYPES.WMS;
if (upperUrl.endsWith('.CSV')) return CV_CONST_LAYER_TYPES.CSV;
+ if (upperUrl.includes('COLLECTIONS')) return CV_CONST_LAYER_TYPES.OGC_FEATURE;
+
return undefined;
}
@@ -501,6 +510,7 @@ export class ConfigApi {
/**
* Create the layer tree from the service metadata. If an error is detected, throw an error.
+ * When listOfLayerId is [], then the entire metadata layer tree is returned.
*
* @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'.
@@ -545,20 +555,6 @@ export class ConfigApi {
];
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':
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 502be33ca03..614cccfdd3c 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
@@ -236,7 +236,7 @@ export abstract class AbstractGeoviewLayerConfig {
/**
* The getter method that returns the language used to create the geoview layer.
*
- * @returns {TypeDisplayLanguage} The GeoView layer schema associated to the config.
+ * @returns {TypeDisplayLanguage} The language associated to the config.
* @protected
*/
protected getLanguage(): TypeDisplayLanguage {
diff --git a/packages/geoview-core/src/api/config/types/classes/geoview-config/vector-config/geojson-config.ts b/packages/geoview-core/src/api/config/types/classes/geoview-config/vector-config/geojson-config.ts
new file mode 100644
index 00000000000..10ca706a0cc
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/geoview-config/vector-config/geojson-config.ts
@@ -0,0 +1,250 @@
+import { CV_CONST_LAYER_TYPES, CV_CONST_SUB_LAYER_TYPES, CV_GEOVIEW_SCHEMA_PATH } from '@config/types/config-constants';
+import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config';
+import { GeoJsonGroupLayerConfig } from '@config/types/classes/sub-layer-config/group-node/geojson-group-layer-config';
+import { toJsonObject, TypeJsonArray, TypeJsonObject } from '@config/types/config-types';
+import { TypeDisplayLanguage } from '@config/types/map-schema-types';
+import { GeoJsonLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/vector/geojson-layer-entry-config';
+import { EntryConfigBaseClass } from '@config/types/classes/sub-layer-config/entry-config-base-class';
+import { GeoviewLayerConfigError, GeoviewLayerInvalidParameterError } from '@config/types/classes/config-exceptions';
+
+import { layerEntryIsGroupLayer } from '@config/types/type-guards';
+import { mergeWith } from 'lodash';
+import { logger } from '@/core/utils/logger';
+import { createLocalizedString } from '@/core/utils/utilities';
+import { Cast } from '@/app';
+
+export type TypeGeoJsonLayerNode = GeoJsonGroupLayerConfig | GeoJsonLayerEntryConfig;
+
+// ========================
+// #region CLASS HEADER
+
+/**
+ * The GeoJson geoview layer class.
+ */
+export class GeoJsonLayerConfig extends AbstractGeoviewLayerConfig {
+ // ==================
+ // #region PROPERTIES
+
+ /**
+ * Type of GeoView layer.
+ */
+ override geoviewLayerType = CV_CONST_LAYER_TYPES.GEOJSON;
+
+ /** The layer entries to use from the GeoView layer. */
+ declare listOfLayerEntryConfig: EntryConfigBaseClass[] | TypeGeoJsonLayerNode[];
+ // #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('/');
+ const pathItemLength = metadataAccessPathItems.length;
+ const lastPathItem = metadataAccessPathItems[pathItemLength - 1];
+ if (lastPathItem.toLowerCase().endsWith('.json') || lastPathItem.toLowerCase().endsWith('.geojson')) {
+ // The metadataAccessPath ends with a layer index. 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 index and the path to the service metadata.
+ this.metadataAccessPath = metadataAccessPathItems.slice(0, -1).join('/');
+ if (this.listOfLayerEntryConfig.length) {
+ this.setErrorDetectedFlag();
+ logger.logError('When a GeoJson metadataAccessPath ends with a layer file name, the listOfLayerEntryConfig must be empty.');
+ }
+ this.listOfLayerEntryConfig = [
+ this.createLeafNode(toJsonObject({ layerId: lastPathItem, layerName: createLocalizedString(lastPathItem) }), language, this)!,
+ ];
+ }
+ }
+ // #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 GeoJsonLayerConfig */
+ return CV_GEOVIEW_SCHEMA_PATH.GEOJSON;
+ }
+
+ /**
+ * 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 GeoJsonLayerEntryConfig(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 GeoJsonGroupLayerConfig(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 {
+ try {
+ if (this.metadataAccessPath.toLowerCase().endsWith('.meta')) {
+ const fetchResponse = await fetch(this.metadataAccessPath);
+ if (fetchResponse.status === 404) throw new GeoviewLayerConfigError('The service metadata fetch returned a 404 status (Not Found)');
+ const layerMetadata = (await fetchResponse.json()) as TypeJsonObject;
+ const metadataAccessPathElements = this.metadataAccessPath.split('/');
+ this.metadataAccessPath = metadataAccessPathElements.slice(0, metadataAccessPathElements.length - 1).join('/');
+ if (layerMetadata) this.setServiceMetadata(layerMetadata);
+ else throw new GeoviewLayerConfigError('The metadata object returned is undefined');
+ } else {
+ await this.createLayerTree();
+ return;
+ }
+
+ this.listOfLayerEntryConfig = this.processListOfLayerEntryConfig(this.listOfLayerEntryConfig);
+ await this.fetchListOfLayerMetadata();
+
+ await this.createLayerTree();
+ } 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 GeoJson metadata for geoview layer ${this.geoviewLayerId}.\n`, error);
+ }
+ }
+
+ /**
+ * Create a layer entry node for a specific layerId using the service metadata. The node returned can only be a
+ * layer leaf or a layer group.
+ *
+ * @param {string} 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.
+ * @protected @override
+ */
+ protected override createLayerEntryNode(layerId: string, parentNode: EntryConfigBaseClass | undefined): EntryConfigBaseClass {
+ if (this.getServiceMetadata())
+ return this.createLeafNode(
+ toJsonObject({ layerId, layerName: createLocalizedString(layerId) }),
+ this.getLanguage(),
+ this,
+ parentNode
+ )!;
+ // If we cannot find the layerId in the layer definitions, throw an error.
+ const layerFound = this.findLayerMetadataEntry(layerId);
+ if (!layerFound) {
+ throw new GeoviewLayerInvalidParameterError('LayerIdNotFound', [layerId?.toString()]);
+ }
+
+ const layerConfig = mergeWith({}, layerFound, (destValue, sourceValue, key) => {
+ if (key === 'layerName') return createLocalizedString(sourceValue);
+ return undefined;
+ });
+
+ if (layerEntryIsGroupLayer(layerFound)) return this.createGroupNode(layerConfig, this.getLanguage(), this, parentNode);
+ return this.createLeafNode(layerConfig, this.getLanguage(), this, parentNode)!;
+ }
+
+ /**
+ * Create the layer tree using the service metadata.
+ *
+ * @returns {TypeJsonObject[]} The layer tree created from the metadata.
+ * @protected @override
+ */
+ protected override createLayerTreeFromServiceMetadata(): EntryConfigBaseClass[] {
+ let layerTree = this.getServiceMetadata()?.listOfLayerEntryConfig as TypeJsonArray;
+ if (!layerTree) return [];
+ if (layerTree.length > 1)
+ layerTree = Cast({
+ layerId: this.geoviewLayerId,
+ layerName: 'Layer Tree',
+ isLayerGroup: true,
+ listOfLayerEntryConfig: layerTree,
+ });
+
+ const layerConfig = mergeWith({}, layerTree, (destValue, sourceValue, key) => {
+ if (key === 'layerName') return createLocalizedString(sourceValue);
+ return undefined;
+ }) as TypeJsonObject;
+
+ if (layerEntryIsGroupLayer(layerConfig)) return [this.createGroupNode(layerConfig, this.getLanguage(), this)];
+ return [this.createLeafNode(layerConfig, this.getLanguage(), this)!];
+ }
+ // #endregion OVERRIDE
+
+ // ==============
+ // #region PUBLIC
+ /** ****************************************************************************************************************************
+ * This method search recursively the layerId in the layer entry of the service metadata.
+ *
+ * @param {string} layerId The layer identifier that must exists on the server.
+ *
+ * @returns {TypeJsonObject | null} The found layer from the capabilities or null if not found.
+ */
+ findLayerMetadataEntry(
+ layerId: string,
+ listOfLayerEntryConfig = this.getServiceMetadata()?.listOfLayerEntryConfig as TypeJsonArray
+ ): TypeJsonObject | null {
+ if (listOfLayerEntryConfig === undefined) return null;
+ return listOfLayerEntryConfig.reduce((layerFound, layerEntry) => {
+ if (layerFound) return layerFound;
+
+ if (layerEntry.layerId === layerId) {
+ return layerEntry;
+ }
+
+ if (layerEntry.isLayerGroup || layerEntry.entryType === CV_CONST_SUB_LAYER_TYPES.GROUP) {
+ return this.findLayerMetadataEntry(layerId, layerEntry.listOfLayerEntryConfig as TypeJsonArray);
+ }
+
+ return null;
+ }, null as TypeJsonObject | null);
+ }
+
+ // #endregion PUBLIC
+ // #endregion METHODS
+ // #endregion CLASS HEADER
+}
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 3adc14dceb3..3e571c22f16 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
@@ -8,6 +8,7 @@ import { EsriFeatureLayerConfig } from '@config/types/classes/geoview-config/vec
import { EsriImageLayerConfig } from '@config/types/classes/geoview-config/raster-config/esri-image-config';
import { WmsLayerConfig } from '@config/types/classes/geoview-config/raster-config/wms-config';
import { WfsLayerConfig } from '@config/types/classes/geoview-config/vector-config/wfs-config';
+import { GeoJsonLayerConfig } from '@config/types/classes/geoview-config/vector-config/geojson-config';
import {
CV_BASEMAP_ID,
CV_BASEMAP_LABEL,
@@ -452,10 +453,10 @@ export class MapFeatureConfig {
return new WmsLayerConfig(layerConfig, language);
case CV_CONST_LAYER_TYPES.WFS:
return new WfsLayerConfig(layerConfig, language);
+ case CV_CONST_LAYER_TYPES.GEOJSON:
+ return new GeoJsonLayerConfig(layerConfig, language);
// case CV_CONST_LAYER_TYPES.ESRI_IMAGE:
// return new EsriImageLayerConfig(layerConfig, language);
- // case CV_CONST_LAYER_TYPES.GEOJSON:
- // return new GeojsonLayerConfig(layerConfig, language);
// case CV_CONST_LAYER_TYPES.GEOPACKAGE:
// return new GeopackageLayerConfig(layerConfig, language);
// case CV_CONST_LAYER_TYPES.XYZ_TILES:
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts
index db59a9d9baa..886f37e3e01 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/entry-config-base-class.ts
@@ -122,28 +122,28 @@ export abstract class EntryConfigBaseClass {
// ================
// #region ABSTRACT
/**
- * @protected @abstract
* 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 @abstract
*/
protected abstract getSchemaPath(): string;
/**
- * @protected @abstract
* 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 @abstract
*/
protected abstract getEntryType(): TypeLayerEntryType;
/**
- * @abstract
* Fetch the layer metadata from the metadataAccessPath and store it in a private variable of the sublayer.
* The same method signature is used by layer group nodes and leaf nodes (layers).
*
* @returns {Promise} A Promise that will resolve when the execution will be completed.
+ * @abstract
*/
abstract fetchLayerMetadata(): Promise;
@@ -152,8 +152,18 @@ export abstract class EntryConfigBaseClass {
// =================
// #region PROTECTED
/**
+ * The getter method that returns the language used to create the sublayer.
+ *
+ * @returns {TypeDisplayLanguage} The language associated to the config.
* @protected
+ */
+ protected getLanguage(): TypeDisplayLanguage {
+ return this.#language;
+ }
+
+ /**
* Validate the node configuration using the schema associated to its layer type.
+ * @protected
*/
protected validateLayerConfig(layerConfig: TypeJsonObject): void {
if (!isvalidComparedToInputSchema(this.getSchemaPath(), layerConfig)) this.setErrorDetectedFlag();
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/geojson-group-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/geojson-group-layer-config.ts
new file mode 100644
index 00000000000..ea4206d552e
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/geojson-group-layer-config.ts
@@ -0,0 +1,101 @@
+import { GroupLayerEntryConfig } from '@config/types/classes/sub-layer-config/group-node/group-layer-entry-config';
+import { isvalidComparedToInternalSchema } from '@config/utils';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
+import { GeoJsonLayerConfig } from '@config/types/classes/geoview-config/vector-config/geojson-config';
+import { Cast } from '@config/types/config-types';
+import { Extent, TypeLayerInitialSettings } from '@config/types/map-schema-types';
+import { merge } from 'lodash';
+import { validateExtentWhenDefined } from '@/geo/utils/utilities';
+import { logger } from '@/core/utils/logger';
+
+// ========================
+// #region CLASS HEADER
+/**
+ * Base type used to define the common implementation of a GeoJson GeoView sublayer to display on the map.
+ */
+export class GeoJsonGroupLayerConfig extends GroupLayerEntryConfig {
+ // ===============
+ // #region METHODS
+ /*
+ * Methods are listed in the following order: abstract, override, private, protected, public and static.
+ */
+
+ // ================
+ // #region OVERRIDE
+ /**
+ * Shadow method used to do a cast operation on the parent method to return GeoJsonLayerConfig instead of
+ * AbstractGeoviewLayerConfig.
+ *
+ * @returns {GeoJsonLayerConfig} The Geoview layer configuration that owns this GeoJson layer entry config.
+ * @override
+ */
+ override getGeoviewLayerConfig(): GeoJsonLayerConfig {
+ return super.getGeoviewLayerConfig() as GeoJsonLayerConfig;
+ }
+
+ /** ***************************************************************************************************************************
+ * This method is used to fetch, parse and extract the relevant information from the metadata for 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;
+
+ const layerMetadata = this.getGeoviewLayerConfig().findLayerMetadataEntry(this.layerId);
+ if (layerMetadata) {
+ this.setLayerMetadata(layerMetadata);
+
+ // Parse the raw layer metadata and build the geoview configuration.
+ this.#parseLayerMetadata();
+ }
+
+ // Fetch the sub-layers metadata that compose the group.
+ 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 parse the layer metadata and extract the source information and other properties.
+ * @private
+ */
+ #parseLayerMetadata(): void {
+ const layerMetadata = this.getLayerMetadata();
+
+ if (layerMetadata?.attributions) this.attributions.push(layerMetadata.attributions as string);
+ this.layerName = layerMetadata.layerName as string;
+ this.minScale = (layerMetadata?.minScale || this.minScale) as number;
+ this.maxScale = (layerMetadata.maxScale || this.maxScale) as number;
+
+ this.initialSettings = Cast(merge(this.initialSettings, layerMetadata.initialSettings));
+
+ if (layerMetadata?.initialSettings?.extent) {
+ this.initialSettings.extent = validateExtentWhenDefined(layerMetadata.initialSettings.extent as Extent);
+ if (this?.initialSettings?.extent?.find?.((value, i) => value !== layerMetadata.initialSettings.extent[i]))
+ logger.logWarning(
+ `The extent specified in the metadata for the layer path “${this.getLayerPath()}” is considered invalid and has been corrected.`
+ );
+ }
+
+ if (layerMetadata?.bounds) {
+ this.bounds = validateExtentWhenDefined(layerMetadata.bounds as Extent);
+ if (this?.bounds?.find?.((value, i) => value !== layerMetadata.bounds[i]))
+ logger.logWarning(
+ `The bounds specified in the metadata for the layer path “${this.getLayerPath()}” is considered invalid and has been corrected.`
+ );
+ }
+ }
+
+ // #endregion PRIVATE
+ // #endregion METHODS
+ // #endregion CLASS HEADER
+}
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wfs-group-layer-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wfs-group-layer-config.ts
index 3931757af6b..52bebf05837 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wfs-group-layer-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/group-node/wfs-group-layer-config.ts
@@ -5,7 +5,7 @@ import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions
// ========================
// #region CLASS HEADER
/**
- * Base type used to define the common implementation of an ESRI GeoView sublayer to display on the map.
+ * Base type used to define the common implementation of a WFS GeoView sublayer to display on the map.
*/
export class WfsGroupLayerConfig extends GroupLayerEntryConfig {
// ===============
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
index f5dd468d5e4..44baa144d5f 100644
--- 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
@@ -57,7 +57,7 @@ export class WmsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
* Shadow method used to do a cast operation on the parent method to return WmsLayerConfig instead of
* AbstractGeoviewLayerConfig.
*
- * @returns {WmsLayerConfig} The Geoview layer configuration that owns this WFS layer entry config.
+ * @returns {WmsLayerConfig} The Geoview layer configuration that owns this WMS layer entry config.
* @override @async
*/
override getGeoviewLayerConfig(): WmsLayerConfig {
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/esri-feature-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/esri-feature-layer-entry-config.ts
index 49aa0557413..85eafd55bf6 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/esri-feature-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/esri-feature-layer-entry-config.ts
@@ -87,6 +87,7 @@ export class EsriFeatureLayerEntryConfig extends AbstractBaseEsriLayerEntryConfi
override applyDefaultValues(): void {
super.applyDefaultValues();
this.source = {
+ strategy: 'all',
maxRecordCount: 0,
format: 'EsriJSON',
projection: 3978,
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/geojson-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/geojson-layer-entry-config.ts
new file mode 100644
index 00000000000..a145831a263
--- /dev/null
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/geojson-layer-entry-config.ts
@@ -0,0 +1,174 @@
+import { CV_CONST_SUB_LAYER_TYPES, CV_CONST_LEAF_LAYER_SCHEMA_PATH } from '@config/types/config-constants';
+import { Cast } from '@config/types/config-types';
+import {
+ TypeStyleConfig,
+ TypeLayerEntryType,
+ TypeSourceGeoJsonInitialConfig,
+ TypeFeatureInfoLayerConfig,
+ TypeStyleGeometry,
+ TypeLayerInitialSettings,
+ Extent,
+} from '@config/types/map-schema-types';
+import { AbstractBaseLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/abstract-base-layer-entry-config';
+import { GeoJsonLayerConfig } from '@config/types/classes/geoview-config/vector-config/geojson-config';
+import { isvalidComparedToInternalSchema } from '@config/utils';
+import { GeoviewLayerConfigError } from '@config/types/classes/config-exceptions';
+
+import { merge } from 'lodash';
+import { logger } from '@/core/utils/logger';
+import { validateExtentWhenDefined } from '@/geo/utils/utilities';
+import { TimeDimension } from '@/core/utils/date-mgt';
+
+// ====================
+// #region CLASS HEADER
+/**
+ * The GeoJson geoview sublayer class.
+ */
+
+export class GeoJsonLayerEntryConfig extends AbstractBaseLayerEntryConfig {
+ // ==================
+ // #region PROPERTIES
+ /** Source settings to apply to the GeoView image layer source at creation time. */
+ declare source: TypeSourceGeoJsonInitialConfig;
+
+ /** 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.GEOJSON;
+ }
+
+ /**
+ * 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.VECTOR;
+ }
+
+ /**
+ * Shadow method used to do a cast operation on the parent method to return GeoJsonLayerConfig instead of
+ * AbstractGeoviewLayerConfig.
+ *
+ * @returns {GeoJsonLayerConfig} The Geoview layer configuration that owns this GeoJson layer entry config.
+ * @override
+ */
+ override getGeoviewLayerConfig(): GeoJsonLayerConfig {
+ return super.getGeoviewLayerConfig() as GeoJsonLayerConfig;
+ }
+
+ /**
+ * 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 @async
+ */
+ override fetchLayerMetadata(): Promise {
+ // If an error has already been detected, then the layer is unusable.
+ if (this.getErrorDetectedFlag()) return Promise.resolve();
+
+ if (Object.keys(this.getGeoviewLayerConfig().getServiceMetadata()).length === 0) {
+ this.setLayerMetadata({});
+ return Promise.resolve();
+ }
+
+ const layerMetadata = this.getGeoviewLayerConfig().findLayerMetadataEntry(this.layerId);
+ 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 = {
+ strategy: 'all',
+ maxRecordCount: 0,
+ crossOrigin: 'Anonymous',
+ 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.
+ * @private
+ */
+ #parseLayerMetadata(): void {
+ const layerMetadata = this.getLayerMetadata();
+
+ if (layerMetadata?.attributions) this.attributions.push(layerMetadata.attributions as string);
+ this.geometryType = (layerMetadata.geometryType || this.geometryType) as TypeStyleGeometry;
+ this.layerName = layerMetadata.layerName as string;
+ this.minScale = (layerMetadata?.minScale || this.minScale) as number;
+ this.maxScale = (layerMetadata.maxScale || this.maxScale) as number;
+
+ this.initialSettings = Cast(merge(this.initialSettings, layerMetadata.initialSettings));
+ this.source.featureInfo = Cast(merge(this.source.featureInfo, layerMetadata.source.featureInfo));
+ this.style = Cast(merge(this.style, layerMetadata.style));
+ this.temporalDimension = Cast(merge(this.temporalDimension, layerMetadata.temporalDimension));
+
+ if (layerMetadata?.initialSettings?.extent) {
+ this.initialSettings.extent = validateExtentWhenDefined(layerMetadata.initialSettings.extent as Extent);
+ if (this?.initialSettings?.extent?.find?.((value, i) => value !== layerMetadata.initialSettings.extent[i]))
+ logger.logWarning(
+ `The extent specified in the metadata for the layer path “${this.getLayerPath()}” is considered invalid and has been corrected.`
+ );
+ }
+
+ if (layerMetadata?.bounds) {
+ this.bounds = validateExtentWhenDefined(layerMetadata.bounds as Extent);
+ if (this?.bounds?.find?.((value, i) => value !== layerMetadata.bounds[i]))
+ logger.logWarning(
+ `The bounds specified in the metadata for the layer path “${this.getLayerPath()}” is considered invalid and has been corrected.`
+ );
+ }
+ }
+
+ // #endregion PRIVATE
+ // #endregion METHODS
+ // #endregion CLASS HEADER
+}
diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/wfs-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/wfs-layer-entry-config.ts
index 35ebcd95fd4..a6838385c53 100644
--- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/wfs-layer-entry-config.ts
+++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/vector/wfs-layer-entry-config.ts
@@ -17,14 +17,14 @@ import { logger } from '@/core/utils/logger';
import { validateExtentWhenDefined } from '@/geo/utils/utilities';
import { findPropertyNameByRegex, xmlToJson } from '@/core/utils/utilities';
-// ========================
+// ====================
// #region CLASS HEADER
/**
* The OGC WFS geoview sublayer class.
*/
export class WfsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
- // =========================
+ // ==================
// #region PROPERTIES
/** Source settings to apply to the GeoView image layer source at creation time. */
declare source: TypeSourceWfsInitialConfig;
@@ -38,7 +38,7 @@ export class WfsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
/*
* Methods are listed in the following order: abstract, override, private, protected, public and static.
*/
- // ==========================
+ // ================
// #region OVERRIDE
/**
@@ -225,8 +225,8 @@ export class WfsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
).split(' ');
const bounds = [Number(lowerCorner[0]), Number(lowerCorner[1]), Number(upperCorner[0]), Number(upperCorner[1])] as Extent;
- this.initialSettings!.extent = validateExtentWhenDefined(bounds)!;
- if (this.initialSettings!.extent.find((value, i) => value !== bounds[i]))
+ this.initialSettings!.extent = validateExtentWhenDefined(bounds);
+ if (this.initialSettings?.extent?.find?.((value, i) => value !== bounds[i]))
logger.logWarning(
`The extent specified in the metadata for the layer path “${this.getLayerPath()}” is considered invalid and has been corrected.`
);
@@ -269,7 +269,7 @@ export class WfsLayerEntryConfig extends AbstractBaseLayerEntryConfig {
// #endregion PRIVATE
- // ===============
+ // ==============
// #region STATIC
/** ***************************************************************************************************************************
* Extract the type of the specified field from the metadata. If the type can not be found, return 'string'.
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 347e6c19b0a..1adf5b45c56 100644
--- a/packages/geoview-core/src/api/config/types/config-constants.ts
+++ b/packages/geoview-core/src/api/config/types/config-constants.ts
@@ -61,9 +61,9 @@ export const CV_CONST_LEAF_LAYER_SCHEMA_PATH: Record = {
ESRI_FEATURE: 'https://cgpv/schema#/definitions/EsriFeatureLayerEntryConfig',
WMS: 'https://cgpv/schema#/definitions/WmsLayerEntryConfig',
WFS: 'https://cgpv/schema#/definitions/WfsLayerEntryConfig',
+ GEOJSON: 'https://cgpv/schema#/definitions/GeoJsonLayerEntryConfig',
IMAGE_STATIC: 'https://cgpv/schema#/definitions/ImageStaticLayerEntryConfig',
- GEOJSON: 'https://cgpv/schema#/definitions/VectorLayerEntryConfig',
GEOPACKAGE: 'https://cgpv/schema#/definitions/VectorLayerEntryConfig',
XYZ_TILES: 'https://cgpv/schema#/definitions/TileLayerEntryConfig',
VECTOR_TILES: 'Thttps://cgpv/schema#/definitions/TileLayerEntryConfig',
@@ -77,9 +77,9 @@ export const CV_GEOVIEW_SCHEMA_PATH: Record = {
ESRI_FEATURE: 'https://cgpv/schema#/definitions/EsriFeatureLayerConfig',
WMS: 'https://cgpv/schema#/definitions/WmsLayerConfig',
WFS: 'https://cgpv/schema#/definitions/WfsLayerConfig',
+ GEOJSON: 'https://cgpv/schema#/definitions/GeoJsonLayerConfig',
IMAGE_STATIC: '',
- GEOJSON: '',
GEOPACKAGE: '',
XYZ_TILES: '',
VECTOR_TILES: '',
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 9ba50d6c4f1..7f9ba66dc47 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
@@ -879,6 +879,24 @@
}
]
},
+ "GeoJsonLayerConfig": {
+ "description": "Structure used by the viewer to describe the configuration of a GeoJSON layer.",
+ "type": "object",
+ "allOf": [
+ {
+ "description": "The parent class.",
+ "$ref": "#/definitions/AbstractGeoviewLayerConfig"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "geoviewLayerType": {
+ "enum": ["GeoJSON"]
+ }
+ }
+ }
+ ]
+ },
"EntryConfigBaseClass": {
"description": "Base class from which we derive all the nodes (group and leaves) in the layer tree.",
"type": "object",
@@ -1115,6 +1133,27 @@
}
]
},
+ "GeoJsonLayerEntryConfig": {
+ "description": "Class from which we derive all the GeoJson leaf nodes in the layer tree.",
+ "type": "object",
+ "allOf": [
+ {
+ "description": "The parent class.",
+ "$ref": "#/definitions/AbstractBaseLayerEntryConfig"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "source": {
+ "$ref": "#/definitions/TypeBaseVectorSourceInitialConfig"
+ },
+ "style": {
+ "$ref": "#/definitions/TypeStyleConfig"
+ }
+ }
+ }
+ ]
+ },
"WmsLayerEntryConfig": {
"description": "Class from which we derive all the WMS leaf nodes in the layer tree.",
"type": "object",
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 a24387647e4..657085e69f0 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
@@ -348,6 +348,7 @@ export { EsriDynamicLayerEntryConfig } from '@config/types/classes/sub-layer-con
export { EsriFeatureLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/vector/esri-feature-layer-entry-config';
export { WmsLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/raster/wms-layer-entry-config';
export { WfsLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/vector/wfs-layer-entry-config';
+export { GeoJsonLayerEntryConfig } from '@config/types/classes/sub-layer-config/leaf/vector/geojson-layer-entry-config';
/** Valid keys for the geometryType property. */
export type TypeStyleGeometry = 'point' | 'linestring' | 'polygon';
@@ -453,6 +454,9 @@ export type TypeVectorSourceFormats = 'GeoJSON' | 'EsriJSON' | 'KML' | 'WFS' | '
/** Type from which we derive the source properties for all the ESRI feature leaf nodes in the layer tree. */
export type TypeSourceEsriFeatureInitialConfig = TypeBaseVectorSourceInitialConfig;
+/** Type from which we derive the source properties for all the GeoJson feature leaf nodes in the layer tree. */
+export type TypeSourceGeoJsonInitialConfig = TypeBaseVectorSourceInitialConfig;
+
/** Type from which we derive the source properties for all the ESRI dynamic leaf nodes in the layer tree. */
export interface TypeSourceEsriDynamicInitialConfig extends TypeBaseSourceInitialConfig {
/** Maximum number of records to fetch (default: 0). */