diff --git a/packages/geoview-core/public/configs/validator/08-geojson.json b/packages/geoview-core/public/configs/validator/08-geojson.json new file mode 100644 index 00000000000..1e00f6a5e4a --- /dev/null +++ b/packages/geoview-core/public/configs/validator/08-geojson.json @@ -0,0 +1,55 @@ +{ + "map": { + "interaction": "dynamic", + "viewSettings": { + "projection": 3978 + }, + "basemapOptions": { + "basemapId": "imagery", + "shaded": false, + "labeled": false + }, + "listOfGeoviewLayerConfig": [ + { + "geoviewLayerId": "geojsonLYR1", + "geoviewLayerName": { "en": "GeoJSON Sample" }, + "metadataAccessPath": { "en": "./datasets/geojson/metadata-new.meta" }, + "geoviewLayerType": "GeoJSON", + "listOfLayerEntryConfig": [ + { + "layerId": "polygons.json", + "layerName": { "en": "Polygons" }, + "layerFilter": "creationDate >= date \\'2020/02/15\\'" + }, + { + "layerId": "lines.json", + "layerName": { "en": "Lines" }, + "layerFilter": "creationDate >= date \\'2020-05-28T12:00:00-05:00\\'" + }, + { + "isLayerGroup": true, + "layerId": "point-feature-group", + "layerName": { "en": "point-feature-group" }, + "listOfLayerEntryConfig": [ + { + "layerId": "icon_points.json", + "layerName": { "en": "Icons" }, + "initialSettings": { "states": {"visible" : false} }, + "layerFilter": "creationDate >= date \\'2020-01-14T12:00:01-05:00\\'" + }, + { + "layerId": "points.json", + "layerName": { "en": "Points" }, + "layerFilter": "creationDate >= date \\'2019-02-15T22:00:00Z\\'" + } + ] + } + ] + } + ] + }, + "components": ["overview-map"], + "overviewMap": {"hideOnZoom": 7}, + "corePackages": [], + "theme": "geo.ca" +} \ No newline at end of file diff --git a/packages/geoview-core/public/datasets/geojson/metadata-new.meta b/packages/geoview-core/public/datasets/geojson/metadata-new.meta new file mode 100644 index 00000000000..645cb50792f --- /dev/null +++ b/packages/geoview-core/public/datasets/geojson/metadata-new.meta @@ -0,0 +1,474 @@ +{ + "schemaVersionUsed": "1,0", + "copyrightText": "© His Majesty the King in Right of Canada, as represented by the Minister of Natural Resources", + "listOfLayerEntryConfig": [ + { + "layerId": "polygons.json", + "layerName": "Polygons", + "geometryType": "polygon", + "source": { + "featureInfo": { + "queryable": true, + "nameField": "Province", + "outfields": [ + { + "name": "Province", + "alias": "Province", + "type": "string", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + }, + { + "name": "myImages", + "alias": "myImages", + "type": "string", + "domain": null + } + ] + } + }, + "style": { + "type": "uniqueValue", + "fields": ["Province"], + "hasDefault": true, + "info": [ + { + "label": "Quebec", + "visible": true, + "values": ["Quebec"], + "settings": { + "type": "filledPolygon", + "color": "rgba(0,128,128,0.5)", + "paternSize": 10, + "paternWidth": 2, + "fillStyle": "diagonalCross", + "stroke": { + "color": "rgba(128,128,0,1)", + "lineStyle": "dot" + } + } + }, + { + "label": "Alberta", + "visible": false, + "values": ["Alberta"], + "settings": { + "type": "filledPolygon", + "color": "rgba(255,0,255,0.5)", + "paternSize": 10, + "paternWidth": 2, + "fillStyle": "diagonalCross", + "stroke": { + "color": "rgba(128,0,128,1)", + "lineStyle": "dot" + } + } + }, + { + "label": "Other provinces", + "visible": true, + "values": ["Other provinces"], + "settings": { + "type": "filledPolygon", + "color": "rgba(0,0,255,0.5)", + "paternSize": 10, + "paternWidth": 2, + "fillStyle": "diagonalCross", + "stroke": { + "color": "rgba(128,0,0,1)", + "lineStyle": "dot" + } + } + } + ] + } + }, + { + "layerId": "multipolygons.geojson", + "layerName": "MultiPolygons", + "geometryType": "polygon", + "source": { + "featureInfo": { + "queryable": true, + "nameField": "Province", + "outfields": [ + { + "name": "Province", + "alias": "Province", + "type": "string", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "type": "uniqueValue", + "fields": ["Province"], + "hasDefault": true, + "info": [ + { + "label": "Multiple provinces", + "visible": true, + "values": ["Other provinces"], + "settings": { + "type": "filledPolygon", + "color": "rgba(0,0,255,0.5)", + "paternSize": 10, + "paternWidth": 2, + "fillStyle": "diagonalCross", + "stroke": { + "color": "rgba(128,0,0,1)", + "lineStyle": "dot" + } + } + } + ] + } + }, + { + "layerId": "lines.json", + "layerName": "Roads", + "geometryType": "linestring", + "source": { + "featureInfo": { + "queryable": true, + "nameField": "Road_Number", + "outfields": [ + { + "name": "Road_Number", + "alias": "Road Number", + "type": "number", + "domain": null + }, + { + "name": "Province", + "alias": "Province Number", + "type": "string", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "type": "simple", + "label": "LineString label", + "settings": { + "type": "lineString", + "stroke": { + "lineStyle": "shortDash-dot-dot", + "color": "rgba(128,0,0,1)" + } + } + } + }, + { + "layerId": "pointGroup1", + "layerName": "Point Group 1", + "bounds": [77, 47, 66, 55], + "isLayerGroup": true, + "listOfLayerEntryConfig": [ + { + "layerId": "icon_points.json", + "layerName": "Icons", + "geometryType": "point", + "source": { + "featureInfo": { + "queryable": true, + "nameField": "data", + "outfields": [ + { + "name": "data", + "alias": "data", + "type": "string", + "domain": null + }, + { + "name": "label", + "alias": "label", + "type": "string", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "styleType": "simple", + "label": "Icon point label", + "settings": { + "type": "iconSymbol", + "mimeType": "image/png", + "src": "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAcNJREFUKJFjYSATsCBz/v//z/Tp8+fwO/cf273/+FmckZHxn6iQwBMVJbldXJyc27Bq/P//v8jVm3cmtUxf7rX+3nN+ZEVJ+srhd+8/3KikIFfMyMj4Fa7x////zBeu3JjuXDM55Ou//xjOmnfxrsS+8t7Uze1FLAwMDClwjU+fv0irmLzYH5smGHjw/TdTz7yVIe/ef9wiJMi/gYWBgYHhxp2HXgeev2dFVtjhb8Pw7dtPhqbdp+FiCy894E+4cz+IgYEBovHDp88qyJpmxngwxIf5wfnImt+9f68Ed+rff3/ZkTVycnDA2dxcHMhSDH/+/WeHa+Tm5HzJwMCgCJMsW7SZgYmZkeHHj18M5RsPo2jk4eJ+DdcoKSZyQoeX0+LK5+8MDAwMDC9+/WWImbkeI4D85MUZJMWED8A1qirKVjUlBvgETVqugqEa5mQmRoacKO9zWurKPXCNfHx8389evha5Njd8bfPCLXIXPn1F0eQgLshQGut7XUREOJCRkfEfXCMDAwODsa7WmQOnT+t150VMevn6ne33bz+kGBgZ/3FzcTyUEBXZxcb4p0RbTek3TD1KWnUwNf3IwMAQj8u5yAAAupehfivnXOEAAAAASUVORK5CYII=", + "opacity": 0.5 + } + } + }, + { + "layerId": "points.json", + "layerName": "Points", + "geometryType": "point", + "source": { + "featureInfo": { + "queryable": false, + "nameField": "Red", + "outfields": [ + { + "name": "Red", + "alias": "Red", + "type": "number", + "domain": null + }, + { + "name": "Green", + "alias": "Green", + "type": "number", + "domain": null + }, + { + "name": "Blue", + "alias": "Blue", + "type": "number", + "domain": null + }, + { + "name": "Yellow", + "alias": "Yellow", + "type": "number", + "domain": null + }, + { + "name": "Orange", + "alias": "Orange", + "type": "number", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "Point": { + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" + } + } + } + }, + { + "layerId": "pointGroup2", + "layerName": "Point Group 2", + "isLayerGroup": true, + "listOfLayerEntryConfig": [ + { + "layerId": "points_1.json", + "layerName": "Points 1", + "geometryType": "point", + "source": { + "featureInfo": { + "queryable": false, + "nameField": "Red", + "outfields": [ + { + "name": "Red", + "alias": "Red", + "type": "number", + "domain": null + }, + { + "name": "Green", + "alias": "Green", + "type": "number", + "domain": null + }, + { + "name": "Blue", + "alias": "Blue", + "type": "number", + "domain": null + }, + { + "name": "Yellow", + "alias": "Yellow", + "type": "number", + "domain": null + }, + { + "name": "Orange", + "alias": "Orange", + "type": "number", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "Point": { + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" + } + } + } + }, + { + "layerId": "points_2.json", + "layerName": "Points 2", + "geometryType": "point", + "source": { + "featureInfo": { + "queryable": false, + "nameField": "Red", + "outfields": [ + { + "name": "Red", + "alias": "Red", + "type": "number", + "domain": null + }, + { + "name": "Green", + "alias": "Green", + "type": "number", + "domain": null + }, + { + "name": "Blue", + "alias": "Blue", + "type": "number", + "domain": null + }, + { + "name": "Yellow", + "alias": "Yellow", + "type": "number", + "domain": null + }, + { + "name": "Orange", + "alias": "Orange", + "type": "number", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "Point": { + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" + } + } + } + }, + { + "layerId": "points_3.json", + "layerName": "Points 3", + "geometryType": "point", + "source": { + "featureInfo": { + "queryable": false, + "nameField": "Red", + "outfields": [ + { + "name": "Red", + "alias": "Red", + "type": "number", + "domain": null + }, + { + "name": "Green", + "alias": "Green", + "type": "number", + "domain": null + }, + { + "name": "Blue", + "alias": "Blue", + "type": "number", + "domain": null + }, + { + "name": "Yellow", + "alias": "Yellow", + "type": "number", + "domain": null + }, + { + "name": "Orange", + "alias": "Orange", + "type": "number", + "domain": null + }, + { + "name": "creationDate", + "alias": "Creation Date", + "type": "date", + "domain": null + } + ] + } + }, + "style": { + "Point": { + "styleType": "simple", + "label": "Point label", + "settings": { + "type": "simpleSymbol", + "symbol": "star" + } + } + } + } + ] + } + ] + } + ] +} diff --git a/packages/geoview-core/public/templates/config-sandbox.html b/packages/geoview-core/public/templates/config-sandbox.html index b718390d7e6..9e1a912044f 100644 --- a/packages/geoview-core/public/templates/config-sandbox.html +++ b/packages/geoview-core/public/templates/config-sandbox.html @@ -154,6 +154,7 @@

Sandbox Configuration

+ @@ -680,7 +681,7 @@

Sanbox Map

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). */