From c8dba827949ce2c03f96afa13e29c904bf5139ff Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Tue, 7 Jan 2025 16:10:32 -0500 Subject: [PATCH 01/10] fix(query): Re work the identify query with offset and geometry false for hover Closes #2565 --- .../public/configs/navigator/28-geocore.json | 18 +------- .../geo/layer/gv-layers/abstract-gv-layer.ts | 30 +++++++------ .../layer/gv-layers/raster/gv-esri-dynamic.ts | 42 +++++++++++++++---- .../layer/layer-sets/abstract-layer-set.ts | 6 ++- .../hover-feature-info-layer-set.ts | 4 +- 5 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index 139ba497d62..d8692889722 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -12,23 +12,7 @@ "listOfGeoviewLayerConfig": [ { "geoviewLayerType": "geoCore", - "geoviewLayerId": "21b821cf-0f1c-40ee-8925-eab12d357668" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "ccc75c12-5acc-4a6a-959f-ef6f621147b9" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "0fca08b5-e9d0-414b-a3c4-092ff9c5e326" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "03ccfb5c-a06e-43e3-80fd-09d4f8f69703" - }, - { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "6433173f-bca8-44e6-be8e-3e8a19d3c299" + "geoviewLayerId": "1dcd28aa-99da-4f62-b157-15631379b170" } ] }, diff --git a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts index 947c01b2398..b42db135a50 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts @@ -236,12 +236,13 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * Returns feature information for the layer specified. * @param {QueryType} queryType - The type of query to perform. * @param {TypeLocation} location - An optionsl pixel, coordinate or polygon that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean * @returns {Promise} The feature info table. */ async getFeatureInfo( queryType: QueryType, - layerPath: string, - location: TypeLocation = null + location: TypeLocation = null, + queryGeometry: boolean = true ): Promise { // TODO: Refactor - After layers refactoring, remove the layerPath parameter here (gotta keep it in the signature for now for the layers-set active switch) try { @@ -266,19 +267,19 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { promiseGetFeature = this.getAllFeatureInfo(); break; case 'at_pixel': - promiseGetFeature = this.getFeatureInfoAtPixel(location as Pixel); + promiseGetFeature = this.getFeatureInfoAtPixel(location as Pixel, queryGeometry); break; case 'at_coordinate': - promiseGetFeature = this.getFeatureInfoAtCoordinate(location as Coordinate); + promiseGetFeature = this.getFeatureInfoAtCoordinate(location as Coordinate, queryGeometry); break; case 'at_long_lat': - promiseGetFeature = this.getFeatureInfoAtLongLat(location as Coordinate); + promiseGetFeature = this.getFeatureInfoAtLongLat(location as Coordinate, queryGeometry); break; case 'using_a_bounding_box': - promiseGetFeature = this.getFeatureInfoUsingBBox(location as Coordinate[]); + promiseGetFeature = this.getFeatureInfoUsingBBox(location as Coordinate[], queryGeometry); break; case 'using_a_polygon': - promiseGetFeature = this.getFeatureInfoUsingPolygon(location as Coordinate[]); + promiseGetFeature = this.getFeatureInfoUsingPolygon(location as Coordinate[], queryGeometry); break; default: // Default is empty array @@ -315,10 +316,11 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { /** * Overridable function to return of feature information at a given pixel location. * @param {Coordinate} location - The pixel coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtPixel(location: Pixel): Promise { + protected getFeatureInfoAtPixel(location: Pixel, queryGeometry = true): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtPixel on layer path ${this.getLayerPath()}`); } @@ -326,10 +328,11 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { /** * Overridable function to return of feature information at a given coordinate. * @param {Coordinate} location - The coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtCoordinate(location: Coordinate): Promise { + protected getFeatureInfoAtCoordinate(location: Coordinate, queryGeometry = true): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtCoordinate on layer path ${this.getLayerPath()}`); } @@ -337,10 +340,11 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { /** * Overridable function to return of feature information at the provided long lat coordinate. * @param {Coordinate} lnglat - The coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtLongLat(location: Coordinate): Promise { + protected getFeatureInfoAtLongLat(location: Coordinate, queryGeometry = true): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtLongLat on layer path ${this.getLayerPath()}`); } @@ -348,10 +352,11 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { /** * Overridable function to return of feature information at the provided bounding box. * @param {Coordinate} location - The bounding box that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoUsingBBox(location: Coordinate[]): Promise { + protected getFeatureInfoUsingBBox(location: Coordinate[], queryGeometry = true): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoUsingBBox on layer path ${this.getLayerPath()}`); } @@ -359,10 +364,11 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { /** * Overridable function to return of feature information at the provided polygon. * @param {Coordinate} location - The polygon that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoUsingPolygon(location: Coordinate[]): Promise { + protected getFeatureInfoUsingPolygon(location: Coordinate[], queryGeometry = true): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoUsingPolygon on layer path ${this.getLayerPath()}`); } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index a2014201b40..8b32369a94f 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -29,6 +29,7 @@ import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers'; import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; import { TypeEsriImageLayerLegend } from './gv-esri-image'; +import { transform } from 'ol/proj'; type TypeFieldOfTheSameValue = { value: string | number | Date; nbOccurence: number }; type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryTree }[]; @@ -221,32 +222,41 @@ export class GVEsriDynamic extends AbstractGVRaster { /** * Overrides the return of feature information at a given pixel location. * @param {Coordinate} location - The pixel coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ - protected override getFeatureInfoAtPixel(location: Pixel): Promise { + protected override getFeatureInfoAtPixel(location: Pixel, queryGeometry = true): Promise { // Redirect to getFeatureInfoAtCoordinate - return this.getFeatureInfoAtCoordinate(this.getMapViewer().map.getCoordinateFromPixel(location)); + return this.getFeatureInfoAtCoordinate(this.getMapViewer().map.getCoordinateFromPixel(location), queryGeometry); } /** * Overrides the return of feature information at a given coordinate. * @param {Coordinate} location - The coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ - protected override getFeatureInfoAtCoordinate(location: Coordinate): Promise { + protected override getFeatureInfoAtCoordinate( + location: Coordinate, + queryGeometry = true + ): Promise { // Transform coordinate from map project to lntlat const projCoordinate = this.getMapViewer().convertCoordinateMapProjToLngLat(location); // Redirect to getFeatureInfoAtLongLat - return this.getFeatureInfoAtLongLat(projCoordinate); + return this.getFeatureInfoAtLongLat(projCoordinate, queryGeometry); } /** * Overrides the return of feature information at the provided long lat coordinate. * @param {Coordinate} lnglat - The coordinate that will be used by the query. + * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ - protected override async getFeatureInfoAtLongLat(lnglat: Coordinate): Promise { + protected override async getFeatureInfoAtLongLat( + lnglat: Coordinate, + queryGeometry = true + ): Promise { try { // If invisible if (!this.getVisible()) return []; @@ -273,14 +283,29 @@ export class GVEsriDynamic extends AbstractGVRaster { const layerDefs = this.getOLSource()?.getParams()?.layerDefs || ''; const size = mapViewer.map.getSize()!; + // function calculatePixelWidth(map, latitude) { + const view = this.getMapViewer().map.getView(); + const resolution = view.getResolution()!; // Map resolution in meters (LCC) + + // Conversion factor: meters per degree of longitude at given latitude + const metersPerDegreeLon = 111320 * Math.cos((lnglat[1] * Math.PI) / 180); + + // Convert 1 pixel width (resolution in meters) to degrees + const pixelWidthInDegrees = resolution / metersPerDegreeLon; + + const off = pixelWidthInDegrees; + // } + + console.log('off ' + off); identifyUrl = `${identifyUrl}identify?f=json&tolerance=${this.hitTolerance}` + `&mapExtent=${extent.xmin},${extent.ymin},${extent.xmax},${extent.ymax}` + `&imageDisplay=${size[0]},${size[1]},96` + `&layers=visible:${layerConfig.layerId}` + `&layerDefs=${layerDefs}` + - `&returnFieldName=true&sr=4326&returnGeometry=true` + - `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}`; + `&returnFieldName=true&sr=4326&returnGeometry=${queryGeometry}` + + `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}` + + `&maxAllowableOffset=${off}`; const response = await fetch(identifyUrl); const jsonResponse = await response.json(); @@ -289,6 +314,9 @@ export class GVEsriDynamic extends AbstractGVRaster { throw new Error(`Error code = ${jsonResponse.error.code} ${jsonResponse.error.message}` || ''); } + // If no features + if (!jsonResponse.results) return []; + const features = new EsriJSON().readFeatures( { features: jsonResponse.results }, { dataProjection: Projection.PROJECTION_NAMES.LNGLAT, featureProjection: mapViewer.getProjection().getCode() } diff --git a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts index 6f8b2142ef5..376cbb870cf 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/abstract-layer-set.ts @@ -393,16 +393,18 @@ export abstract class AbstractLayerSet { * @param {AbstractGVLayer} geoviewLayer - The geoview layer * @param {QueryType} queryType - The query type * @param {TypeLocation} location - The location for the query + * @param {boolean} queryGeometry - The query geometry boolean * @returns {Promise} A promise resolving to the query results */ protected static queryLayerFeatures( data: TypeFeatureInfoResultSetEntry | TypeAllFeatureInfoResultSetEntry | TypeHoverResultSetEntry, geoviewLayer: AbstractGVLayer, queryType: QueryType, - location: TypeLocation + location: TypeLocation, + queryGeometry: boolean = true ): Promise { // Get Feature Info - return geoviewLayer.getFeatureInfo(queryType, data.layerPath, location); + return geoviewLayer.getFeatureInfo(queryType, location, queryGeometry); } /** diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index 8005db6bd38..15edbaa3bb0 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -107,7 +107,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { // If layer was found if (layer && layer instanceof AbstractGVLayer) { // If state is not queryable - if (!AbstractLayerSet.isStateQueryable(layer)) return; + return; // if (!AbstractLayerSet.isStateQueryable(layer)) return; // Flag processing this.resultSet[layerPath].feature = undefined; @@ -117,7 +117,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { MapEventProcessor.setMapHoverFeatureInfo(this.getMapId(), this.resultSet[layerPath].feature); // Process query on results data - AbstractLayerSet.queryLayerFeatures(this.resultSet[layerPath], layer, queryType, pixelCoordinate) + AbstractLayerSet.queryLayerFeatures(this.resultSet[layerPath], layer, queryType, pixelCoordinate, false) .then((arrayOfRecords) => { if (arrayOfRecords === null) { this.resultSet[layerPath].queryStatus = 'error'; From 701561bc8edb7d22ca938c0ce1155a8b183849be Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 10 Jan 2025 07:23:29 -0500 Subject: [PATCH 02/10] manage oid check --- .../public/configs/navigator/28-geocore.json | 12 ++- packages/geoview-core/schema.json | 3 +- .../abstract-base-esri-layer-entry-config.ts | 11 +-- .../types/config-validation-schema.json | 3 +- .../src/api/config/types/map-schema-types.ts | 2 +- .../core/components/data-table/data-table.tsx | 16 ++-- .../data-table/json-export-button.tsx | 5 ++ .../layer-state.ts | 8 +- .../layer/geoview-layers/esri-layer-common.ts | 1 + .../vector/abstract-geoview-vector.ts | 25 +++++- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 77 +++++++++++++------ .../src/geo/layer/gv-layers/utils.ts | 27 +++++-- .../hover-feature-info-layer-set.ts | 11 ++- .../geoview-core/src/geo/utils/utilities.ts | 38 +++++++++ 14 files changed, 178 insertions(+), 61 deletions(-) diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index d8692889722..41d52873e48 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -11,8 +11,16 @@ }, "listOfGeoviewLayerConfig": [ { - "geoviewLayerType": "geoCore", - "geoviewLayerId": "1dcd28aa-99da-4f62-b157-15631379b170" + "geoviewLayerId": "historical-flood", + "geoviewLayerName": "Historical Flood Events (HFE)", + "externalDateFormat": "mm/dd/yyyy hh:mm:ss-05:00", + "metadataAccessPath": "https://gisp.dfo-mpo.gc.ca/arcgis/rest/services/FGP/Fieldnotes_Pacific_Science_Field_Operations_en/MapServer", + "geoviewLayerType": "esriDynamic", + "listOfLayerEntryConfig": [ + { + "layerId": "0" + } + ] } ] }, diff --git a/packages/geoview-core/schema.json b/packages/geoview-core/schema.json index 28bea530eb8..d83d86f3b5e 100644 --- a/packages/geoview-core/schema.json +++ b/packages/geoview-core/schema.json @@ -72,7 +72,8 @@ "string", "number", "date", - "url" + "url", + "oid" ] }, "TypeFeatureInfoNotQueryable": { diff --git a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts index 4c2088ffdd5..111769c0a7f 100644 --- a/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts +++ b/packages/geoview-core/src/api/config/types/classes/sub-layer-config/leaf/abstract-base-esri-layer-entry-config.ts @@ -165,16 +165,13 @@ export abstract class AbstractBaseEsriLayerEntryConfig extends AbstractBaseLayer * * @param {string} esriFieldType The ESRI field type. * - * @returns {'string' | 'date' | 'number'} The type of the field. + * @returns {'string' | 'date' | 'number' | 'oid'} The type of the field. * @static @private */ - static #convertEsriFieldType(esriFieldType: string): 'string' | 'date' | 'number' { + static #convertEsriFieldType(esriFieldType: string): 'string' | 'date' | 'number' | 'oid' { if (esriFieldType === 'esriFieldTypeDate') return 'date'; - if ( - ['esriFieldTypeDouble', 'esriFieldTypeInteger', 'esriFieldTypeSingle', 'esriFieldTypeSmallInteger', 'esriFieldTypeOID'].includes( - esriFieldType - ) - ) + if (esriFieldType === 'esriFieldTypeOID') return 'oid'; + if (['esriFieldTypeDouble', 'esriFieldTypeInteger', 'esriFieldTypeSingle', 'esriFieldTypeSmallInteger'].includes(esriFieldType)) return 'number'; return 'string'; } 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 39a79ae5c1f..080ade92b78 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 @@ -1707,7 +1707,8 @@ "string", "number", "date", - "url" + "url", + "oid" ] }, "codedValueType": { 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 03cc6cc5b26..ae092d24f01 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 @@ -500,7 +500,7 @@ export type TypeOutfields = { }; /** The types supported by the outfields object. */ -export type TypeOutfieldsType = 'string' | 'date' | 'number' | 'url'; +export type TypeOutfieldsType = 'string' | 'date' | 'number' | 'url' | 'oid'; export type codedValueType = { type: 'codedValue'; diff --git a/packages/geoview-core/src/core/components/data-table/data-table.tsx b/packages/geoview-core/src/core/components/data-table/data-table.tsx index 50041520ad9..f77012584cc 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-table.tsx @@ -299,13 +299,15 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): async (feature: TypeFeatureInfoEntry) => { let { extent } = feature; - // If there is no extent, the layer is ESRI Dynamic, get the feature extent using its OBJECTID - // GV: Some layers do not use OBJECTID, these are the other values seen so far. - // TODO: Update field info types to include esriFieldTypeOID to identify the ID field. - const idFields = ['OBJECTID', 'OBJECTID_1', 'FID', 'STATION_NUMBER']; - const idField = idFields.find((fieldName) => feature.fieldInfo[fieldName]?.value !== undefined); - if (!extent && idField !== undefined) - extent = await getExtentFromFeatures(layerPath, [feature.fieldInfo[idField]!.value as string], idField); + // Get oid field + const oidField = + feature && feature.fieldInfo + ? Object.keys(feature.fieldInfo).find((key) => feature.fieldInfo[key]!.dataType === 'oid') || undefined + : undefined; + + // If there is no extent, the layer is ESRI Dynamic, get the feature extent using its oid field + if (!extent && oidField !== undefined) + extent = await getExtentFromFeatures(layerPath, [feature.fieldInfo[oidField]!.value as string], oidField); if (extent) { // Project diff --git a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx index 9f983084103..3c672b4ab91 100644 --- a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx +++ b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx @@ -72,6 +72,11 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): try { // Create a new promise that will resolved when features have been updated with their geometries return new Promise((resolve, reject) => { + // Get oid field + const oidField = chunk[0].fieldInfo + ? Object.keys(chunk[0].fieldInfo).find((key) => chunk[0].fieldInfo[key]!.dataType === 'oid') || undefined + : undefined; + // Get the ids const objectids = chunk.map((record) => { return record.geometry?.get('OBJECTID') as number; diff --git a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts index e64addf4f2b..065eabcb4ac 100644 --- a/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts +++ b/packages/geoview-core/src/core/stores/store-interface-and-intial-values/layer-state.ts @@ -115,6 +115,12 @@ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILay // Check if EsriDynamic config if (layerConfig && layerEntryIsEsriDynamic(layerConfig)) { + // Get oid field + const oidField = + layerConfig.source.featureInfo && layerConfig.source.featureInfo.outfields + ? layerConfig.source.featureInfo.outfields.filter((field) => field.type === 'oid')[0].name + : 'OBJECTID'; + // Query for the specific object ids // TODO: Put the server original projection in the config metadata (add a new optional param in source for esri) // TO.DOCONT: When we get the projection we can get the projection in original server (will solve error trying to reproject https://maps-cartes.ec.gc.ca/arcgis/rest/services/CESI/MapServer/7 in 3857) @@ -123,7 +129,7 @@ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILay `${layerConfig.source?.dataAccessPath}/${layerConfig.layerId}`, geometryType, objectIDs, - 'OBJECTID', + oidField, true, MapEventProcessor.getMapState(get().mapId).currentProjection ); diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts b/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts index 057064a45c6..a1660e794c4 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/esri-layer-common.ts @@ -201,6 +201,7 @@ export function commonGetFieldType( if (!fieldDefinition) return 'string'; const esriFieldType = fieldDefinition.type as string; if (esriFieldType === 'esriFieldTypeDate') return 'date'; + if (esriFieldType === 'esriFieldTypeOID') return 'oid'; if ( ['esriFieldTypeDouble', 'esriFieldTypeInteger', 'esriFieldTypeSingle', 'esriFieldTypeSmallInteger', 'esriFieldTypeOID'].includes( esriFieldType diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts index 712af4be75b..b393d7dffc2 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts @@ -13,7 +13,7 @@ import { ProjectionLike } from 'ol/proj'; import { Geometry, Point } from 'ol/geom'; import { getUid } from 'ol/util'; -import { TypeOutfields } from '@config/types/map-schema-types'; +import { TypeFeatureInfoLayerConfig, TypeOutfields } from '@config/types/map-schema-types'; import { api } from '@/app'; import { AbstractGeoViewLayer, CONST_LAYER_TYPES } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; @@ -147,6 +147,9 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { // Convert the CSV to features features = AbstractGeoViewVector.convertCsv(this.mapId, xhr.responseText, layerConfig as VectorLayerEntryConfig); } else if (layerConfig.schemaTag === CONST_LAYER_TYPES.ESRI_FEATURE) { + // Get oid field + const oidField = AbstractGeoViewVector.#getEsriOidField(layerConfig); + // Fetch the features text array const esriFeaturesArray = await AbstractGeoViewVector.getEsriFeatures( layerConfig.layerPath, @@ -178,8 +181,11 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { is not a number, we assume it is provided as an ISO UTC string. If not, the result is unpredictable. */ if (features) { + // Get oid field + const oidField = AbstractGeoViewVector.#getEsriOidField(layerConfig); + features.forEach((feature) => { - const featureId = feature.get('OBJECTID') ? feature.get('OBJECTID') : getUid(feature); + const featureId = feature.get(oidField) ? feature.get(oidField) : getUid(feature); feature.setId(featureId); }); // If there's no feature info, build it from features @@ -242,6 +248,7 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { * @param {number} maxRecordCount - The max features per query from the service. * @param {number} featureLimit - The maximum number of features to fetch per query. * @param {number} queryLimit - The maximum number of queries to run at once. + * @param {string} oidField - The unique identifier field name. * @returns {Promise} An array of the response text for the features. * @private */ @@ -254,7 +261,8 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { featureCount: number, maxRecordCount?: number, featureLimit: number = 500, - queryLimit: number = 10 + queryLimit: number = 10, + oidField: string = 'OBJECTID' ): Promise { // Update url const baseUrl = url.replace('&where=1%3D1&returnCountOnly=true', `&outfields=*&geometryPrecision=1`); @@ -263,7 +271,7 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { // Create array of url's to call const urlArray: string[] = []; for (let i = 0; i < featureCount; i += featureFetchLimit) { - urlArray.push(`${baseUrl}&where=OBJECTID+<=+${i + featureFetchLimit}&resultOffset=${i}`); + urlArray.push(`${baseUrl}&where=${oidField}+<=+${i + featureFetchLimit}&resultOffset=${i}`); } const promises: Promise[] = []; @@ -497,4 +505,13 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { if (!layerConfig.source.featureInfo.nameField) layerConfig.source.featureInfo.nameField = layerConfig.source.featureInfo!.outfields[0].name; } + + static #getEsriOidField(layerConfig: AbstractBaseLayerEntryConfig): string { + // Get oid field + return layerConfig.source && + layerConfig.source.featureInfo && + (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields === undefined + ? (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields.filter((field) => field.type === 'oid')[0].name + : 'OBJECTID'; + } } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 8b32369a94f..9aecd6dfe46 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -8,7 +8,7 @@ import { Extent } from 'ol/extent'; import Feature from 'ol/Feature'; import Geometry from 'ol/geom/Geometry'; -import { validateExtent } from '@/geo/utils/utilities'; +import { getMetersPerPixel, validateExtent } from '@/geo/utils/utilities'; import { Projection } from '@/geo/utils/projection'; import { logger } from '@/core/utils/logger'; import { DateMgt } from '@/core/utils/date-mgt'; @@ -22,14 +22,14 @@ import { TypeLayerStyleConfig, TypeLayerStyleConfigInfo, } from '@/geo/map/map-schema-types'; -import { esriGetFieldType, esriGetFieldDomain } from '../utils'; +import { esriGetFieldType, esriGetFieldDomain, esriQueryRecordsByUrlObjectIds } from '../utils'; import { AbstractGVRaster } from './abstract-gv-raster'; import { TypeOutfieldsType } from '@/api/config/types/map-schema-types'; import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers'; import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; import { TypeEsriImageLayerLegend } from './gv-esri-image'; -import { transform } from 'ol/proj'; +import { TypeJsonObject } from '@/api/config/types/config-types'; type TypeFieldOfTheSameValue = { value: string | number | Date; nbOccurence: number }; type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryTree }[]; @@ -56,6 +56,10 @@ export class GVEsriDynamic extends AbstractGVRaster { public constructor(mapId: string, olSource: ImageArcGISRest, layerConfig: EsriDynamicLayerEntryConfig) { super(mapId, olSource, layerConfig); + // TODO: Investigate to see if we can call the export map for the whole service at once instead of making many call + // TODO.CONT: We can use the layers and layersDef parameters to set what should be visible. + // TODO.CONT: layers=show:layerId ; layerDefs={ "layerId": "layer def" } + // TODO.CONT: There is no allowableOffset on esri dynamic to speed up. We will need to see what can be done for layers in wrong projection // Create the image layer options. const imageLayerOptions: ImageOptions = { source: olSource, @@ -279,49 +283,72 @@ export class GVEsriDynamic extends AbstractGVRaster { const boundsLL = mapViewer.convertCoordinateMapProjToLngLat([mapExtent[0], mapExtent[1]]); const boundsUR = mapViewer.convertCoordinateMapProjToLngLat([mapExtent[2], mapExtent[3]]); const extent = { xmin: boundsLL[0], ymin: boundsLL[1], xmax: boundsUR[0], ymax: boundsUR[1] }; - const layerDefs = this.getOLSource()?.getParams()?.layerDefs || ''; const size = mapViewer.map.getSize()!; - // function calculatePixelWidth(map, latitude) { - const view = this.getMapViewer().map.getView(); - const resolution = view.getResolution()!; // Map resolution in meters (LCC) - - // Conversion factor: meters per degree of longitude at given latitude - const metersPerDegreeLon = 111320 * Math.cos((lnglat[1] * Math.PI) / 180); - - // Convert 1 pixel width (resolution in meters) to degrees - const pixelWidthInDegrees = resolution / metersPerDegreeLon; - - const off = pixelWidthInDegrees; - // } + // Get meters per pixel to set the maxAllowableOffset to simplify return geometry + const offset = getMetersPerPixel(mapViewer, lnglat[1]); + console.log(`off ${offset}`); - console.log('off ' + off); identifyUrl = `${identifyUrl}identify?f=json&tolerance=${this.hitTolerance}` + `&mapExtent=${extent.xmin},${extent.ymin},${extent.xmax},${extent.ymax}` + `&imageDisplay=${size[0]},${size[1]},96` + `&layers=visible:${layerConfig.layerId}` + `&layerDefs=${layerDefs}` + - `&returnFieldName=true&sr=4326&returnGeometry=${queryGeometry}` + `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}` + - `&maxAllowableOffset=${off}`; + `&returnGeometry=false`; - const response = await fetch(identifyUrl); - const jsonResponse = await response.json(); - if (jsonResponse.error) { + logger.logMarkerStart('off identify'); + const identifyResponse = await fetch(identifyUrl); + const identifyJsonResponse = await identifyResponse.json(); + if (identifyJsonResponse.error) { logger.logInfo('There is a problem with this query: ', identifyUrl); - throw new Error(`Error code = ${jsonResponse.error.code} ${jsonResponse.error.message}` || ''); + throw new Error(`Error code = ${identifyJsonResponse.error.code} ${identifyJsonResponse.error.message}` || ''); } + // If no features identified + if (identifyJsonResponse.results.length === 0) return []; + + // Extract OBJECTIDs + const oidField = layerConfig.source.featureInfo.outfields + ? layerConfig.source.featureInfo.outfields.filter((field) => field.type === 'oid')[0].name + : 'OBJECTID'; + const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => result.attributes[oidField]); + logger.logMarkerCheck('off identify'); + + logger.logMarkerStart('off query'); + const response1 = await esriQueryRecordsByUrlObjectIds( + layerConfig.source.dataAccessPath + layerConfig.layerId, + 'Polygon', + objectIds, + '*', + true, + mapViewer.getMapState().currentProjection, + offset, + false + ); + logger.logMarkerCheck('off query'); + + logger.logMarkerStart('off feature'); + const features1 = new EsriJSON().readFeatures({ features: response1 }) as Feature[]; + const arrayOfFeatureInfoEntries1 = await this.formatFeatureInfoResult(features1, layerConfig); + logger.logMarkerCheck('off feature'); + return arrayOfFeatureInfoEntries1; + console.log('off ' + response1); + // If no features if (!jsonResponse.results) return []; - + logger.logMarkerStart('off start'); + console.log('off ring' + jsonResponse.results[0].geometry.rings.length); const features = new EsriJSON().readFeatures( { features: jsonResponse.results }, - { dataProjection: Projection.PROJECTION_NAMES.LNGLAT, featureProjection: mapViewer.getProjection().getCode() } + { dataProjection: 'EPSG:4326', featureProjection: mapViewer.getProjection().getCode() } ) as Feature[]; const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); + logger.logMarkerCheck('off start'); + + console.log('off ' + arrayOfFeatureInfoEntries[0].geometry.values_.geometry.flatCoordinates.length); return arrayOfFeatureInfoEntries; } catch (error) { // Log diff --git a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts index 5afd261026b..d5505a7f857 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts @@ -15,6 +15,7 @@ import { EsriDynamicLayerEntryConfig } from '@/core/utils/config/validation-clas import { EsriFeatureLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/esri-feature-layer-entry-config'; import { EsriImageLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-image-layer-entry-config'; import { GeometryApi } from '../geometry/geometry'; +import { getMetersPerPixel } from '@/geo/utils/utilities'; /** * Returns the type of the specified field. @@ -38,11 +39,13 @@ export function esriGetFieldType( layerConfig: EsriDynamicLayerEntryConfig | EsriFeatureLayerEntryConfig | EsriImageLayerEntryConfig, fieldName: string ): TypeOutfieldsType { + logger.logError('TEST') const esriFieldDefinitions = layerConfig.getLayerMetadata()?.fields as TypeJsonArray; const fieldDefinition = esriFieldDefinitions.find((metadataEntry) => metadataEntry.name === fieldName); if (!fieldDefinition) return 'string'; const esriFieldType = fieldDefinition.type as string; if (esriFieldType === 'esriFieldTypeDate') return 'date'; + if (esriFieldType === 'esriFieldTypeOID') return 'oid'; if ( ['esriFieldTypeDouble', 'esriFieldTypeInteger', 'esriFieldTypeSingle', 'esriFieldTypeSmallInteger', 'esriFieldTypeOID'].includes( esriFieldType @@ -105,9 +108,14 @@ export function esriParseFeatureInfoEntries(records: TypeJsonObject[], geometryT * Asynchronously queries an Esri feature layer given the url and returns an array of `TypeFeatureInfoEntryPartial` records. * @param {string} url - An Esri url indicating a feature layer to query * @param {TypeStyleGeometry?} geometryType - The geometry type for the geometries in the layer being queried (used when geometries are returned) + * @param {boolean} parseFeatureInfoEntries - A boolean to indicate if we use the raw esri output or if we parse it * @returns {TypeFeatureInfoEntryPartial[] | null} An array of relared records of type TypeFeatureInfoEntryPartial, or an empty array. */ -export async function esriQueryRecordsByUrl(url: string, geometryType?: TypeStyleGeometry): Promise { +export async function esriQueryRecordsByUrl( + url: string, + geometryType?: TypeStyleGeometry, + parseFeatureInfoEntries = true +): Promise { // TODO: Refactor - Suggestion to rework this function and the one in EsriDynamic.getFeatureInfoAtLongLat(), making // TO.DO.CONT: the latter redirect to this one here and merge some logic between the 2 functions ideally making this // TO.DO.CONT: one here return a TypeFeatureInfoEntry[] with options to have returnGeometry=true or false and such. @@ -120,8 +128,8 @@ export async function esriQueryRecordsByUrl(url: string, geometryType?: TypeStyl throw new Error(`Error code = ${respJson.error.code} ${respJson.error.message}` || ''); } - // Return the array of TypeFeatureInfoEntryPartial - return esriParseFeatureInfoEntries(respJson.features, geometryType); + // Return the array of TypeFeatureInfoEntryPartial or the raw response features array + return parseFeatureInfoEntries ? esriParseFeatureInfoEntries(respJson.features, geometryType) : respJson.features; } catch (error) { // Log logger.logError('There is a problem with this query: ', url, error); @@ -137,6 +145,8 @@ export async function esriQueryRecordsByUrl(url: string, geometryType?: TypeStyl * @param {string} fields - The list of field names to include in the output * @param {boolean} geometry - True to return the geometries in the output * @param {number} outSR - The spatial reference of the output geometries from the query + * @param {number} maxOffset - The max allowable offset value to simplify geometry + * @param {boolean} parseFeatureInfoEntries - A boolean to indicate if we use the raw esri output or if we parse it * @returns {TypeFeatureInfoEntryPartial[] | null} An array of relared records of type TypeFeatureInfoEntryPartial, or an empty array. */ export function esriQueryRecordsByUrlObjectIds( @@ -145,14 +155,19 @@ export function esriQueryRecordsByUrlObjectIds( objectIds: number[], fields: string, geometry: boolean, - outSR?: number + outSR?: number, + maxOffset?: number, + parseFeatureInfoEntries = true ): Promise { + // Offset + const offset = maxOffset !== undefined ? `&maxAllowableOffset=${maxOffset}` : ''; + // Query const oids = objectIds.join(','); - const url = `${layerUrl}/query?where=&objectIds=${oids}&outFields=${fields}&returnGeometry=${geometry}&outSR=${outSR}&geometryPrecision=1&f=json`; + const url = `${layerUrl}/query?where=&objectIds=${oids}&outFields=${fields}&returnGeometry=${geometry}&outSR=${outSR}&geometryPrecision=1${offset}&f=json`; // Redirect - return esriQueryRecordsByUrl(url, geometryType); + return esriQueryRecordsByUrl(url, geometryType, parseFeatureInfoEntries); } /** diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index 15edbaa3bb0..3dfdd74927e 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -146,13 +146,12 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { // Log logger.logPromiseFailed('queryLayerFeatures in queryLayers in hoverFeatureInfoLayerSet', error); }); - } else { - this.resultSet[layerPath].feature = null; - this.resultSet[layerPath].queryStatus = 'error'; - - // Propagate to the store - MapEventProcessor.setMapHoverFeatureInfo(this.getMapId(), this.resultSet[layerPath].feature); } + this.resultSet[layerPath].feature = null; + this.resultSet[layerPath].queryStatus = 'error'; + + // Propagate to the store + MapEventProcessor.setMapHoverFeatureInfo(this.getMapId(), this.resultSet[layerPath].feature); }); } diff --git a/packages/geoview-core/src/geo/utils/utilities.ts b/packages/geoview-core/src/geo/utils/utilities.ts index 39dae427b5b..be49e25399c 100644 --- a/packages/geoview-core/src/geo/utils/utilities.ts +++ b/packages/geoview-core/src/geo/utils/utilities.ts @@ -22,6 +22,7 @@ import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { TypeLayerStyleConfig } from '@/geo/map/map-schema-types'; import { TypeBasemapLayer } from '../layer/basemap/basemap-types'; +import { MapViewer } from '@/app'; /** * Interface used for css style declarations @@ -308,6 +309,7 @@ export function convertTypeFeatureStyleToOpenLayersStyle(style?: TypeFeatureStyl return getDefaultDrawingStyle(style?.strokeColor, style?.strokeWidth, style?.fillColor); } +// #region EXTENT /** * Returns the union of 2 extents. * @param {Extent | undefined} extentA First extent @@ -432,6 +434,7 @@ export function validateExtentWhenDefined(extent: Extent | undefined, code: stri if (extent) return validateExtent(extent, code); return undefined; } +// #endregion EXTENT /** * Gets the area of a given geometry @@ -475,3 +478,38 @@ export function calculateDistance(coordinates: Coordinate[], inProj: string, out return { total: Math.round((getLength(geom) / 1000) * 100) / 100, sections }; } + +/** + * Get meters per pixel for different projections + * @param {MapViewer} map - The Geoview map viewer instance + * @param {number?} lat - The latitude, only needed for Web Mercator + * @returns {nubmber} Number representing meters per pixel + */ +export function getMetersPerPixel(map: MapViewer, lat?: number): number { + const view = map.getView(); + const projection = view.getProjection().getCode(); + const resolution = view.getResolution(); + + if (!resolution) return 0; + + // Web Mercator needs latitude correction because of severe distortion at high latitudes + // At latitude 60°N, the scale distortion factor is about 2:1 + if (projection === 'EPSG:3857') { + if (lat === undefined) { + // Get center of current view if latitude not provided + const center = view.getCenter(); + if (center) { + // Transform center point to get latitude + const [, latitude] = Projection.transform(center, projection, Projection.PROJECTION_NAMES.LNGLAT); + const latitudeCorrection = Math.cos((latitude * Math.PI) / 180); + return resolution * latitudeCorrection; + } + return resolution; + } + const latitudeCorrection = Math.cos((lat * Math.PI) / 180); + return resolution * latitudeCorrection; + } + + // LCC (and other meter-based projections) can use resolution directly + return resolution; +} From 8fa060a250aac0cdf5794e702d886f6340506f45 Mon Sep 17 00:00:00 2001 From: jolevesq Date: Mon, 13 Jan 2025 10:23:26 -0500 Subject: [PATCH 03/10] Fix oid data table query, uniqueValue no fields --- .../public/configs/navigator/28-geocore.json | 12 +-- .../public/configs/performance.json | 46 +++++++++ .../outliers/outlier-performance.html | 98 +++++++++++++++++++ .../public/templates/outliers/outliers.html | 1 + .../legend-event-processor.ts | 5 + .../core/components/data-table/data-table.tsx | 2 +- .../data-table/json-export-button.tsx | 8 +- .../layers/right-panel/layer-details.tsx | 20 ++-- .../vector/abstract-geoview-vector.ts | 7 +- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 62 ++++++------ .../src/geo/layer/gv-layers/utils.ts | 2 - .../hover-feature-info-layer-set.ts | 2 +- packages/geoview-core/src/geo/layer/layer.ts | 4 +- 13 files changed, 204 insertions(+), 65 deletions(-) create mode 100644 packages/geoview-core/public/configs/performance.json create mode 100644 packages/geoview-core/public/templates/outliers/outlier-performance.html diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index 41d52873e48..d8692889722 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -11,16 +11,8 @@ }, "listOfGeoviewLayerConfig": [ { - "geoviewLayerId": "historical-flood", - "geoviewLayerName": "Historical Flood Events (HFE)", - "externalDateFormat": "mm/dd/yyyy hh:mm:ss-05:00", - "metadataAccessPath": "https://gisp.dfo-mpo.gc.ca/arcgis/rest/services/FGP/Fieldnotes_Pacific_Science_Field_Operations_en/MapServer", - "geoviewLayerType": "esriDynamic", - "listOfLayerEntryConfig": [ - { - "layerId": "0" - } - ] + "geoviewLayerType": "geoCore", + "geoviewLayerId": "1dcd28aa-99da-4f62-b157-15631379b170" } ] }, diff --git a/packages/geoview-core/public/configs/performance.json b/packages/geoview-core/public/configs/performance.json new file mode 100644 index 00000000000..6dd39d58cf3 --- /dev/null +++ b/packages/geoview-core/public/configs/performance.json @@ -0,0 +1,46 @@ +{ + "map": { + "interaction": "dynamic", + "viewSettings": { + "projection": 3857 + }, + "basemapOptions": { + "basemapId": "transport", + "shaded": true, + "labeled": false + }, + "listOfGeoviewLayerConfig": [ + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "1dcd28aa-99da-4f62-b157-15631379b170" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "6c343726-1e92-451a-876a-76e17d398a1c" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "e2424b6c-db0c-4996-9bc0-2ca2e6714d71" + } + ] + }, + "components": [ + "overview-map" + ], + "overviewMap": { + "hideOnZoom": 7 + }, + "footerBar": { + "tabs": { + "core": [ + "legend", + "layers", + "details", + "geochart", + "data-table" + ] + } + }, + "corePackages": [], + "theme": "geo.ca" +} \ No newline at end of file diff --git a/packages/geoview-core/public/templates/outliers/outlier-performance.html b/packages/geoview-core/public/templates/outliers/outlier-performance.html new file mode 100644 index 00000000000..6d8f2089ad2 --- /dev/null +++ b/packages/geoview-core/public/templates/outliers/outlier-performance.html @@ -0,0 +1,98 @@ + + + + + + Outlier ESRI Layers - Canadian Geospatial Platform Viewer + + + + + + + + + + + + +
+ + + + + + + +
+

Outlier layers with performace issue

+
+ + + + + + + + + + +
+ Main
+ Outlier Layers
+

This page is used to showcase layers with few layer with bad performance with different original server projection

+
+ +
+

Max Record Count Layers

+ Top +
+ +

+    
+ + + + +
+ +
+ Outlier Layers: +
    +
  • + Advance Polling District (ESRI Feature) - maxRecordCount of 1000, with 6172 features - Issue #2199 +
  • +
  • + Canada Nature Fund for Aquatic Species at Risk (CNFASAR) - Heavy layer in 3857, plus no field define for uniqueValue symbology +
  • +
+
+
+ + + + + diff --git a/packages/geoview-core/public/templates/outliers/outliers.html b/packages/geoview-core/public/templates/outliers/outliers.html index e4bc6e5f729..ff52ea7885a 100644 --- a/packages/geoview-core/public/templates/outliers/outliers.html +++ b/packages/geoview-core/public/templates/outliers/outliers.html @@ -34,6 +34,7 @@

Performance Issue (Huge) Layers

Max Record Count Layers
GeoAI
Elections 2019
+ Many slow layers in different projection

diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts index e09dbe98e4b..498a7dce273 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/legend-event-processor.ts @@ -639,6 +639,11 @@ export class LegendEventProcessor extends AbstractEventProcessor { const visibleValues = new Set(styleUnique.filter((style) => style.visible).map((style) => style.values.join(';'))); const unvisibleValues = new Set(styleUnique.filter((style) => !style.visible).map((style) => style.values.join(';'))); + // GV: Some esri layer has uniqueValue renderer but there is no field define in their metadata (i.e. e2424b6c-db0c-4996-9bc0-2ca2e6714d71). + // TODO: The fields contain undefined, it should be empty. Check in new config api + // TODO: This is a workaround + if (uniqueValueStyle.fields[0] === undefined) uniqueValueStyle.fields.pop(); + // Filter features based on visibility return features.filter((feature) => { const fieldValues = uniqueValueStyle.fields.map((field) => feature.fieldInfo[field]!.value).join(';'); diff --git a/packages/geoview-core/src/core/components/data-table/data-table.tsx b/packages/geoview-core/src/core/components/data-table/data-table.tsx index f77012584cc..29c412cedbb 100644 --- a/packages/geoview-core/src/core/components/data-table/data-table.tsx +++ b/packages/geoview-core/src/core/components/data-table/data-table.tsx @@ -226,7 +226,7 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): header: value.alias, filterFn: 'contains', columnFilterModeOptions: ['contains', 'startsWith', 'endsWith', 'empty', 'notEmpty'], - ...(value.dataType === 'number' && { + ...((value.dataType === 'number' || value.dataType === 'oid') && { filterFn: 'between', columnFilterModeOptions: [ 'equals', diff --git a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx index 3c672b4ab91..b7dd34e4411 100644 --- a/packages/geoview-core/src/core/components/data-table/json-export-button.tsx +++ b/packages/geoview-core/src/core/components/data-table/json-export-button.tsx @@ -74,12 +74,12 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): return new Promise((resolve, reject) => { // Get oid field const oidField = chunk[0].fieldInfo - ? Object.keys(chunk[0].fieldInfo).find((key) => chunk[0].fieldInfo[key]!.dataType === 'oid') || undefined - : undefined; + ? Object.keys(chunk[0].fieldInfo).find((key) => chunk[0].fieldInfo[key]!.dataType === 'oid') || 'OBJECTID' + : 'OBJECTID'; // Get the ids const objectids = chunk.map((record) => { - return record.geometry?.get('OBJECTID') as number; + return record.geometry?.get(oidField) as number; }); // Query @@ -88,7 +88,7 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): // For each result results.forEach((result) => { // Filter - const recFound = chunk.filter((record) => record.geometry?.get('OBJECTID') === result.fieldInfo?.OBJECTID?.value); + const recFound = chunk.filter((record) => record.geometry?.get(oidField) === result.fieldInfo[oidField]?.value); // If found it if (recFound && recFound.length === 1) { diff --git a/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx b/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx index b576bc3824e..734c85b67ea 100644 --- a/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx +++ b/packages/geoview-core/src/core/components/layers/right-panel/layer-details.tsx @@ -123,17 +123,18 @@ export function LayerDetails(props: LayerDetailsProps): JSX.Element { }; function renderItemCheckbox(item: TypeLegendItem): JSX.Element | null { - // no checkbox for simple style layers - if ( - layerDetails.styleConfig?.LineString?.type === 'simple' || - layerDetails.styleConfig?.MultiLineString?.type === 'simple' || - layerDetails.styleConfig?.Point?.type === 'simple' || - layerDetails.styleConfig?.MultiPoint?.type === 'simple' || - layerDetails.styleConfig?.Polygon?.type === 'simple' || - layerDetails.styleConfig?.MultiPolygon?.type === 'simple' - ) { + // First check if styleConfig exists + if (!layerDetails.styleConfig) { return null; } + + // No checkbox for simple style layers + if (layerDetails.styleConfig[item.geometryType]?.type === 'simple') return null; + + // GV: Some esri layer has uniqueValue renderer but there is no field define in their metadata (i.e. e2424b6c-db0c-4996-9bc0-2ca2e6714d71). + // For these layers, we need to disable checkboxes + if (layerDetails.styleConfig[item.geometryType]?.fields[0] === undefined) return null; + if (!layerDetails.canToggle) { return ( @@ -177,6 +178,7 @@ export function LayerDetails(props: LayerDetailsProps): JSX.Element { key={`${item.name}/${layerDetails.items.indexOf(item)}`} alignItems="center" justifyItems="stretch" + sx={{ display: 'flex', flexWrap: 'nowrap' }} > {renderItemCheckbox(item)} diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts index b393d7dffc2..cd96402be50 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts @@ -155,6 +155,7 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { layerConfig.layerPath, url as string, JSON.parse(xhr.responseText).count, + oidField, this.getLayerMetadata(layerConfig.layerPath)?.maxRecordCount as number | undefined ); @@ -245,10 +246,10 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { * @param {string} layerPath - The layer path of the layer. * @param {string} url - The base url for the service. * @param {number} featureCount - The number of features in the layer. + * @param {string} oidField - The unique identifier field name. * @param {number} maxRecordCount - The max features per query from the service. * @param {number} featureLimit - The maximum number of features to fetch per query. * @param {number} queryLimit - The maximum number of queries to run at once. - * @param {string} oidField - The unique identifier field name. * @returns {Promise} An array of the response text for the features. * @private */ @@ -259,10 +260,10 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { layerPath: string, url: string, featureCount: number, + oidField: string, maxRecordCount?: number, featureLimit: number = 500, - queryLimit: number = 10, - oidField: string = 'OBJECTID' + queryLimit: number = 10 ): Promise { // Update url const baseUrl = url.replace('&where=1%3D1&returnCountOnly=true', `&outfields=*&geometryPrecision=1`); diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 9aecd6dfe46..7fd33893cc3 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -24,7 +24,7 @@ import { } from '@/geo/map/map-schema-types'; import { esriGetFieldType, esriGetFieldDomain, esriQueryRecordsByUrlObjectIds } from '../utils'; import { AbstractGVRaster } from './abstract-gv-raster'; -import { TypeOutfieldsType } from '@/api/config/types/map-schema-types'; +import { TypeOutfieldsType, TypeStyleGeometry } from '@/api/config/types/map-schema-types'; import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers'; import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; @@ -60,12 +60,22 @@ export class GVEsriDynamic extends AbstractGVRaster { // TODO.CONT: We can use the layers and layersDef parameters to set what should be visible. // TODO.CONT: layers=show:layerId ; layerDefs={ "layerId": "layer def" } // TODO.CONT: There is no allowableOffset on esri dynamic to speed up. We will need to see what can be done for layers in wrong projection + // TODO.CONT: We may try to use the service projection imageLayerOptions.source?.updateParams({ imageSR: 3978 }); and let OL project on the fly + // TODO.CONT: from some test, it reduce time by half // Create the image layer options. const imageLayerOptions: ImageOptions = { source: olSource, properties: { layerConfig }, }; + // TODO: For testing purpose on projection and performance + if (layerConfig.geoviewLayerConfig.geoviewLayerId === '6c343726-1e92-451a-876a-76e17d398a1c') { + imageLayerOptions.source?.updateParams({ imageSR: 3978 }); + } + if (layerConfig.geoviewLayerConfig.geoviewLayerId === 'e2424b6c-db0c-4996-9bc0-2ca2e6714d71') { + imageLayerOptions.source?.updateParams({ imageSR: 3857 }); + } + // Init the layer options with initial settings AbstractGVRaster.initOptionsWithInitialSettings(imageLayerOptions, layerConfig); @@ -286,10 +296,7 @@ export class GVEsriDynamic extends AbstractGVRaster { const layerDefs = this.getOLSource()?.getParams()?.layerDefs || ''; const size = mapViewer.map.getSize()!; - // Get meters per pixel to set the maxAllowableOffset to simplify return geometry - const offset = getMetersPerPixel(mapViewer, lnglat[1]); - console.log(`off ${offset}`); - + // Identify query to get oid features value, at this point we do not query geometry identifyUrl = `${identifyUrl}identify?f=json&tolerance=${this.hitTolerance}` + `&mapExtent=${extent.xmin},${extent.ymin},${extent.xmax},${extent.ymax}` + @@ -297,9 +304,8 @@ export class GVEsriDynamic extends AbstractGVRaster { `&layers=visible:${layerConfig.layerId}` + `&layerDefs=${layerDefs}` + `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}` + - `&returnGeometry=false`; + `&returnGeometry=false&sr=4326`; - logger.logMarkerStart('off identify'); const identifyResponse = await fetch(identifyUrl); const identifyJsonResponse = await identifyResponse.json(); if (identifyJsonResponse.error) { @@ -315,40 +321,30 @@ export class GVEsriDynamic extends AbstractGVRaster { ? layerConfig.source.featureInfo.outfields.filter((field) => field.type === 'oid')[0].name : 'OBJECTID'; const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => result.attributes[oidField]); - logger.logMarkerCheck('off identify'); - logger.logMarkerStart('off query'); - const response1 = await esriQueryRecordsByUrlObjectIds( + // Get meters per pixel to set the maxAllowableOffset to simplify return geometry + const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; + + // TODO: We need to separate the query attribute from geometry. We can use the attributes returned by identify to show details panel + // TODO.CONT: or create 2 distinc query one for attributes and one for geometry. This way we can display the anel faster and wait later for geometry + // TODO.CONT: We need to see if we can fetch in async mode without freezing the ui. If not we will need a web worker for the fetch. + // TODO.CONT: If we go with web worker, we need a reusable approach so we can use with all our queries + // Get features + const response = await esriQueryRecordsByUrlObjectIds( layerConfig.source.dataAccessPath + layerConfig.layerId, - 'Polygon', + (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', '') as TypeStyleGeometry, objectIds, '*', - true, + queryGeometry, mapViewer.getMapState().currentProjection, - offset, + maxAllowableOffset, false ); - logger.logMarkerCheck('off query'); - - logger.logMarkerStart('off feature'); - const features1 = new EsriJSON().readFeatures({ features: response1 }) as Feature[]; - const arrayOfFeatureInfoEntries1 = await this.formatFeatureInfoResult(features1, layerConfig); - logger.logMarkerCheck('off feature'); - return arrayOfFeatureInfoEntries1; - console.log('off ' + response1); - - // If no features - if (!jsonResponse.results) return []; - logger.logMarkerStart('off start'); - console.log('off ring' + jsonResponse.results[0].geometry.rings.length); - const features = new EsriJSON().readFeatures( - { features: jsonResponse.results }, - { dataProjection: 'EPSG:4326', featureProjection: mapViewer.getProjection().getCode() } - ) as Feature[]; - const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); - logger.logMarkerCheck('off start'); - console.log('off ' + arrayOfFeatureInfoEntries[0].geometry.values_.geometry.flatCoordinates.length); + // TODO: This is also time consuming, the creation of the feature can take several seconds, check web worker + // Transform the features in an OL feature + const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; + const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); return arrayOfFeatureInfoEntries; } catch (error) { // Log diff --git a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts index d5505a7f857..f461015fd3d 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts @@ -15,7 +15,6 @@ import { EsriDynamicLayerEntryConfig } from '@/core/utils/config/validation-clas import { EsriFeatureLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/esri-feature-layer-entry-config'; import { EsriImageLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-image-layer-entry-config'; import { GeometryApi } from '../geometry/geometry'; -import { getMetersPerPixel } from '@/geo/utils/utilities'; /** * Returns the type of the specified field. @@ -39,7 +38,6 @@ export function esriGetFieldType( layerConfig: EsriDynamicLayerEntryConfig | EsriFeatureLayerEntryConfig | EsriImageLayerEntryConfig, fieldName: string ): TypeOutfieldsType { - logger.logError('TEST') const esriFieldDefinitions = layerConfig.getLayerMetadata()?.fields as TypeJsonArray; const fieldDefinition = esriFieldDefinitions.find((metadataEntry) => metadataEntry.name === fieldName); if (!fieldDefinition) return 'string'; diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index 3dfdd74927e..9662e2051a4 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -107,7 +107,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { // If layer was found if (layer && layer instanceof AbstractGVLayer) { // If state is not queryable - return; // if (!AbstractLayerSet.isStateQueryable(layer)) return; + if (!AbstractLayerSet.isStateQueryable(layer)) return; // Flag processing this.resultSet[layerPath].feature = undefined; diff --git a/packages/geoview-core/src/geo/layer/layer.ts b/packages/geoview-core/src/geo/layer/layer.ts index 674e9d499bc..c5c6d3ff133 100644 --- a/packages/geoview-core/src/geo/layer/layer.ts +++ b/packages/geoview-core/src/geo/layer/layer.ts @@ -886,9 +886,9 @@ export class LayerApi { // Create the right GV Layer based on the OLLayer and config type let gvLayer; - if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriDynamicLayerEntryConfig) + if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriDynamicLayerEntryConfig) { gvLayer = new GVEsriDynamic(mapId, olSource, layerConfig); - else if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriImageLayerEntryConfig) + } else if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriImageLayerEntryConfig) gvLayer = new GVEsriImage(mapId, olSource, layerConfig); else if (olSource instanceof Static && layerConfig instanceof ImageStaticLayerEntryConfig) gvLayer = new GVImageStatic(mapId, olSource, layerConfig); From c7888ced22dbbe58c7cb846bbd1bccb1948d9ac3 Mon Sep 17 00:00:00 2001 From: jolevesq Date: Mon, 13 Jan 2025 11:23:39 -0500 Subject: [PATCH 04/10] fix reviewable --- .../public/configs/navigator/28-geocore.json | 16 ++++++++++++++++ .../templates/outliers/outlier-performance.html | 7 +++++-- .../vector/abstract-geoview-vector.ts | 15 +++++++++++---- packages/geoview-core/src/geo/layer/layer.ts | 4 ++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/geoview-core/public/configs/navigator/28-geocore.json b/packages/geoview-core/public/configs/navigator/28-geocore.json index d8692889722..1979e869cc2 100644 --- a/packages/geoview-core/public/configs/navigator/28-geocore.json +++ b/packages/geoview-core/public/configs/navigator/28-geocore.json @@ -13,6 +13,22 @@ { "geoviewLayerType": "geoCore", "geoviewLayerId": "1dcd28aa-99da-4f62-b157-15631379b170" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "ccc75c12-5acc-4a6a-959f-ef6f621147b9" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "0fca08b5-e9d0-414b-a3c4-092ff9c5e326" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "03ccfb5c-a06e-43e3-80fd-09d4f8f69703" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "6433173f-bca8-44e6-be8e-3e8a19d3c299" } ] }, diff --git a/packages/geoview-core/public/templates/outliers/outlier-performance.html b/packages/geoview-core/public/templates/outliers/outlier-performance.html index 6d8f2089ad2..be6944481dd 100644 --- a/packages/geoview-core/public/templates/outliers/outlier-performance.html +++ b/packages/geoview-core/public/templates/outliers/outlier-performance.html @@ -68,10 +68,13 @@

Max Record Count Layers

Outlier Layers:
  • - Advance Polling District (ESRI Feature) - maxRecordCount of 1000, with 6172 features - Issue #2199 + Fieldnotes 2020-2021: Pacific Science Field Operations, 14 sub layer, identify of northern polygon crash the layer.
  • - Canada Nature Fund for Aquatic Species at Risk (CNFASAR) - Heavy layer in 3857, plus no field define for uniqueValue symbology + Protected and conserved area - Heavy layer in 3879. +
  • +
  • + Canada Nature Fund for Aquatic Species at Risk (CNFASAR) - Heavy layer in 3857, plus no field define for uniqueValue symbology.
diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts index cd96402be50..513b3c2ac96 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts @@ -507,12 +507,19 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { layerConfig.source.featureInfo.nameField = layerConfig.source.featureInfo!.outfields[0].name; } + /** + * Gets the Object ID field name from the layer configuration + * @param {AbstractBaseLayerEntryConfig} layerConfig - The layer configuration object + * @returns {string} The name of the OID field if found, otherwise returns 'OBJECTID' as default + * @description Extracts the Object ID field name from the layer configuration. An OID (Object ID) is a + * standardized identifier used to uniquely identify features in a layer. If no OID field is specified + * in the configuration, it defaults to 'OBJECTID'. + * @private + */ static #getEsriOidField(layerConfig: AbstractBaseLayerEntryConfig): string { // Get oid field - return layerConfig.source && - layerConfig.source.featureInfo && - (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields === undefined - ? (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields.filter((field) => field.type === 'oid')[0].name + return layerConfig.source?.featureInfo && (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields !== undefined + ? (layerConfig.source.featureInfo as TypeFeatureInfoLayerConfig).outfields.filter((field) => field.type === 'oid')[0]?.name : 'OBJECTID'; } } diff --git a/packages/geoview-core/src/geo/layer/layer.ts b/packages/geoview-core/src/geo/layer/layer.ts index c5c6d3ff133..674e9d499bc 100644 --- a/packages/geoview-core/src/geo/layer/layer.ts +++ b/packages/geoview-core/src/geo/layer/layer.ts @@ -886,9 +886,9 @@ export class LayerApi { // Create the right GV Layer based on the OLLayer and config type let gvLayer; - if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriDynamicLayerEntryConfig) { + if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriDynamicLayerEntryConfig) gvLayer = new GVEsriDynamic(mapId, olSource, layerConfig); - } else if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriImageLayerEntryConfig) + else if (olSource instanceof ImageArcGISRest && layerConfig instanceof EsriImageLayerEntryConfig) gvLayer = new GVEsriImage(mapId, olSource, layerConfig); else if (olSource instanceof Static && layerConfig instanceof ImageStaticLayerEntryConfig) gvLayer = new GVImageStatic(mapId, olSource, layerConfig); From b309698c14a93d02b99b49eb7c3a3312cc48f061 Mon Sep 17 00:00:00 2001 From: jolevesq Date: Mon, 13 Jan 2025 16:29:35 -0500 Subject: [PATCH 05/10] Add fetch web worker --- .../src/core/workers/abstract-worker-pool.ts | 34 +++++++++ .../src/core/workers/abstract-worker.ts | 4 +- .../core/workers/fetch-esri-worker-pool.ts | 51 ++++++++++++++ .../core/workers/fetch-esri-worker-script.ts | 69 +++++++++++++++++++ .../src/core/workers/fetch-esri-worker.ts | 42 +++++++++++ .../src/core/workers/fetch-worker-type.ts | 10 +++ .../src/core/workers/json-export-script.ts | 10 +-- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 33 ++++++++- .../hover-feature-info-layer-set.ts | 2 +- 9 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 packages/geoview-core/src/core/workers/abstract-worker-pool.ts create mode 100644 packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts create mode 100644 packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts create mode 100644 packages/geoview-core/src/core/workers/fetch-esri-worker.ts create mode 100644 packages/geoview-core/src/core/workers/fetch-worker-type.ts diff --git a/packages/geoview-core/src/core/workers/abstract-worker-pool.ts b/packages/geoview-core/src/core/workers/abstract-worker-pool.ts new file mode 100644 index 00000000000..fe866c33093 --- /dev/null +++ b/packages/geoview-core/src/core/workers/abstract-worker-pool.ts @@ -0,0 +1,34 @@ +import { AbstractWorker } from './abstract-worker'; + +export abstract class AbstractWorkerPool { + protected workers: AbstractWorker[] = []; + + protected busyWorkers = new Set>(); + + protected WorkerClass: new () => AbstractWorker; + + protected name: string; + + constructor(name: string, workerClass: new () => AbstractWorker, numWorkers = navigator.hardwareConcurrency || 4) { + this.name = name; + this.WorkerClass = workerClass; + this.initializeWorkers(numWorkers); + } + + protected initializeWorkers(numWorkers: number): void { + for (let i = 0; i < numWorkers; i++) { + const worker = new this.WorkerClass(); + this.workers.push(worker); + } + } + + public abstract init(): Promise; + + public abstract process(params: unknown): Promise; + + public terminate(): void { + this.workers.forEach((worker) => worker.terminate()); + this.workers = []; + this.busyWorkers.clear(); + } +} diff --git a/packages/geoview-core/src/core/workers/abstract-worker.ts b/packages/geoview-core/src/core/workers/abstract-worker.ts index 26369a7ec48..7a9312abc08 100644 --- a/packages/geoview-core/src/core/workers/abstract-worker.ts +++ b/packages/geoview-core/src/core/workers/abstract-worker.ts @@ -83,14 +83,14 @@ export abstract class AbstractWorker { * @param args - Arguments to pass to the worker for initialization. * @returns A promise that resolves when the worker is initialized. */ - protected abstract init(...args: unknown[]): Promise; + public abstract init(...args: unknown[]): Promise; /** * Process the worker. This method should be implemented by subclasses. * @param args - Arguments to pass to the worker for process. * @returns A promise that resolves when the worker is processed. */ - protected abstract process(...args: unknown[]): Promise; + public abstract process(...args: unknown[]): Promise; /** * Terminates the worker. diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts new file mode 100644 index 00000000000..f5cd3dc8888 --- /dev/null +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts @@ -0,0 +1,51 @@ +import { AbstractWorkerPool } from './abstract-worker-pool'; +import { FetchEsriWorker, FetchEsriWorkerType } from './fetch-esri-worker'; +import { QueryParams } from './fetch-esri-worker-script'; +import { createWorkerLogger } from './helper/logger-worker'; + +import { TypeJsonObject } from '@/api/config/types/config-types'; + +export class FetchEsriWorkerPool extends AbstractWorkerPool { + #logger = createWorkerLogger('FetchEsriWorkerPool'); + + constructor(numWorkers = navigator.hardwareConcurrency || 4) { + super('FetchEsriWorkerPool', FetchEsriWorker, numWorkers); + this.#logger.logInfo('Worker pool created', `Number of workers: ${numWorkers}`); + } + + public async init(): Promise { + try { + this.#logger.logTrace('Initializing worker pool'); + await Promise.all(this.workers.map((worker) => worker.init())); + this.#logger.logTrace('Worker pool initialized'); + } catch (error) { + this.#logger.logError('Worker pool initialization failed', error); + throw error; + } + } + + public async process(params: QueryParams): Promise { + const availableWorker = this.workers.find((w) => !this.busyWorkers.has(w)); + if (!availableWorker) { + throw new Error('No available workers'); + } + + const result = await availableWorker.process(params); + return result as TypeJsonObject; + } + + // /** + // * Process an ESRI query and transform features using a worker from the pool + // */ + // public async processQuery(params: QueryParams): Promise { + // try { + // this.#logger.logTrace('Starting query process', params.url); + // const result = await this.process(params); + // this.#logger.logTrace('Query process completed'); + // return result as TypeJsonObject; + // } catch (error) { + // this.#logger.logError('Query process failed', error); + // throw error; + // } + // } +} diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts new file mode 100644 index 00000000000..c90fa5f13af --- /dev/null +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts @@ -0,0 +1,69 @@ +import { expose } from 'comlink'; + +import { createWorkerLogger } from './helper/logger-worker'; + +import { TypeJsonObject } from '@/api/config/types/config-types'; +import { TypeStyleGeometry } from '@/api/config/types/map-schema-types'; + +export interface QueryParams { + url: string; + geometryType: TypeStyleGeometry; + objectIds: number[]; + queryGeometry: boolean; + projection: number; + maxAllowableOffset: number; +} + +const logger = createWorkerLogger('FetchEsriWorker'); + +// Move the ESRI query function directly into the worker to avoid circular dependencies +async function queryEsriFeatures(params: QueryParams): Promise { + const response = await fetch(`${params.url}/query`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + f: 'json', + geometryType: params.geometryType, + objectIds: params.objectIds.join(','), + outFields: '*', + returnGeometry: params.queryGeometry.toString(), + outSR: params.projection.toString(), + maxAllowableOffset: params.maxAllowableOffset.toString(), + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); +} + +const worker = { + // eslint-disable-next-line require-await + async init(): Promise { + try { + logger.logTrace('init worker', 'FetchEsriWorker initialized'); + } catch { + logger.logError('init worker', 'FetchEsriWorker failed to initialize'); + } + }, + + async process(params: QueryParams): Promise { + try { + logger.logTrace('process worker - Starting query processing', params.url); + const response = await queryEsriFeatures(params); + logger.logDebug('process worker - Query completed'); + return response; + } catch (error) { + logger.logError('process worker - Query processing failed', error); + throw error; + } + }, +}; + +// Expose the worker methods to be accessible from the main thread +expose(worker); +export default {} as typeof Worker & { new (): Worker }; diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts new file mode 100644 index 00000000000..3e9b6630fa7 --- /dev/null +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts @@ -0,0 +1,42 @@ +import { AbstractWorker } from './abstract-worker'; +import { QueryParams } from './fetch-esri-worker-script'; +import { TypeJsonObject } from '@/api/config/types/config-types'; + +export interface FetchEsriWorkerType { + /** + * Initializes the worker - empty for now. + */ + init: () => Promise; + + /** + * Processes an ESRI query JSON export. + * @param {QueryParams} queryParams - The query parameters for the fetch. + * @returns {TypeJsonObject} A promise that resolves to the response fetch as JSON string. + */ + process: (queryParams: QueryParams) => Promise; +} + +export class FetchEsriWorker extends AbstractWorker { + constructor() { + super('FetchEsriWorker', new Worker(new URL('./fetch-esri-worker-script.ts', import.meta.url))); + } + + /** + * Initializes the worker - empty for now. + * @returns A promise that resolves when initialization is complete. + */ + public async init(): Promise { + const result = await this.proxy.init(); + return result; + } + + /** + * Processes a JSON fetch for an esri query. + * @param {QueryParams} queryParams - The query parameters for the fetch. + * @returns A promise that resolves to the processed JSON string. + */ + public async process(queryParams: QueryParams): Promise { + const result = await this.proxy.process(queryParams); + return result; + } +} diff --git a/packages/geoview-core/src/core/workers/fetch-worker-type.ts b/packages/geoview-core/src/core/workers/fetch-worker-type.ts new file mode 100644 index 00000000000..0fa206e849c --- /dev/null +++ b/packages/geoview-core/src/core/workers/fetch-worker-type.ts @@ -0,0 +1,10 @@ +// import { TypeStyleGeometry } from '@/api/config/types/map-schema-types'; + +// export interface QueryParams { +// url: string; +// geometryType: TypeStyleGeometry; +// objectIds: number[]; +// queryGeometry: boolean; +// projection: number; +// maxAllowableOffset: number; +// } diff --git a/packages/geoview-core/src/core/workers/json-export-script.ts b/packages/geoview-core/src/core/workers/json-export-script.ts index 2b82b8952bd..0d869aa9eef 100644 --- a/packages/geoview-core/src/core/workers/json-export-script.ts +++ b/packages/geoview-core/src/core/workers/json-export-script.ts @@ -137,9 +137,9 @@ const worker = { try { sourceCRS = projectionInfo.sourceCRS; targetCRS = projectionInfo.targetCRS; - logger.logTrace('init', `Worker initialized with sourceCRS: ${sourceCRS}, targetCRS: ${targetCRS}`); + logger.logTrace('init worker', `Worker initialized with sourceCRS: ${sourceCRS}, targetCRS: ${targetCRS}`); } catch (error) { - logger.logError('init', error); + logger.logError('init worker', error); } }, @@ -151,7 +151,7 @@ const worker = { */ process(chunk: TypeWorkerExportChunk[], isFirst: boolean): string { try { - logger.logTrace('process', `Processing chunk of ${chunk.length} items`); + logger.logTrace('process worker', `Processing chunk of ${chunk.length} items`); let result = ''; if (isFirst) { result += '{"type":"FeatureCollection","features":['; @@ -171,10 +171,10 @@ const worker = { result += processedChunk.join(','); - logger.logTrace('process', `Finished processing`); + logger.logTrace('process worker', `Finished processing`); return result; } catch (error) { - logger.logError('process', error); + logger.logError('process worker', error); return ''; } }, diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 7fd33893cc3..14cf2984226 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -30,6 +30,8 @@ import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers' import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; import { TypeEsriImageLayerLegend } from './gv-esri-image'; import { TypeJsonObject } from '@/api/config/types/config-types'; +import { FetchEsriWorkerPool } from '@/core/workers/fetch-esri-worker-pool'; +import { QueryParams } from '@/core/workers/fetch-esri-worker-script'; type TypeFieldOfTheSameValue = { value: string | number | Date; nbOccurence: number }; type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryTree }[]; @@ -41,6 +43,8 @@ type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryT * @class GVEsriDynamic */ export class GVEsriDynamic extends AbstractGVRaster { + #fetchWorkerPool: FetchEsriWorkerPool; + // The default hit tolerance the query should be using static override DEFAULT_HIT_TOLERANCE: number = 7; @@ -56,6 +60,10 @@ export class GVEsriDynamic extends AbstractGVRaster { public constructor(mapId: string, olSource: ImageArcGISRest, layerConfig: EsriDynamicLayerEntryConfig) { super(mapId, olSource, layerConfig); + // Setup the worker pool + this.#fetchWorkerPool = new FetchEsriWorkerPool(); + this.#fetchWorkerPool.init().then(() => logger.logTraceCore('Worker pool for fetch ESRI initialized')); + // TODO: Investigate to see if we can call the export map for the whole service at once instead of making many call // TODO.CONT: We can use the layers and layersDef parameters to set what should be visible. // TODO.CONT: layers=show:layerId ; layerDefs={ "layerId": "layer def" } @@ -261,6 +269,26 @@ export class GVEsriDynamic extends AbstractGVRaster { return this.getFeatureInfoAtLongLat(projCoordinate, queryGeometry); } + async yourQueryMethod(layerConfig: any, objectIds: number[], queryGeometry: boolean): Promise { + try { + const params: QueryParams = { + url: layerConfig.source.dataAccessPath + layerConfig.layerId, + geometryType: (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', ''), + objectIds, + queryGeometry, + projection: 3978, + maxAllowableOffset: 7000, + }; + + const response = await this.#fetchWorkerPool.process(params); + const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; + return await this.formatFeatureInfoResult(features, layerConfig); + } catch (error) { + console.error('Query processing failed:', error); + throw error; + } + } + /** * Overrides the return of feature information at the provided long lat coordinate. * @param {Coordinate} lnglat - The coordinate that will be used by the query. @@ -325,6 +353,8 @@ export class GVEsriDynamic extends AbstractGVRaster { // Get meters per pixel to set the maxAllowableOffset to simplify return geometry const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; + this.yourQueryMethod(layerConfig, objectIds, true).then((features) => console.log('Features worker', features)); + // TODO: We need to separate the query attribute from geometry. We can use the attributes returned by identify to show details panel // TODO.CONT: or create 2 distinc query one for attributes and one for geometry. This way we can display the anel faster and wait later for geometry // TODO.CONT: We need to see if we can fetch in async mode without freezing the ui. If not we will need a web worker for the fetch. @@ -335,13 +365,14 @@ export class GVEsriDynamic extends AbstractGVRaster { (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', '') as TypeStyleGeometry, objectIds, '*', - queryGeometry, + false, mapViewer.getMapState().currentProjection, maxAllowableOffset, false ); // TODO: This is also time consuming, the creation of the feature can take several seconds, check web worker + // TODO.CONT: Because web worker can only use sereialize date and not object with function it may be difficult for this... // Transform the features in an OL feature const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index 9662e2051a4..3dfdd74927e 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -107,7 +107,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { // If layer was found if (layer && layer instanceof AbstractGVLayer) { // If state is not queryable - if (!AbstractLayerSet.isStateQueryable(layer)) return; + return; // if (!AbstractLayerSet.isStateQueryable(layer)) return; // Flag processing this.resultSet[layerPath].feature = undefined; From 3d1eb4addc6c4f88746177aab1d426f99223d3ab Mon Sep 17 00:00:00 2001 From: jolevesq Date: Tue, 14 Jan 2025 15:35:39 -0500 Subject: [PATCH 06/10] progress on worker and comments --- .../src/core/workers/abstract-worker-pool.ts | 92 +++++---- .../core/workers/fetch-esri-worker-pool.ts | 115 ++++++----- .../core/workers/fetch-esri-worker-script.ts | 180 +++++++++++------- .../src/core/workers/fetch-esri-worker.ts | 107 +++++++---- .../src/core/workers/fetch-worker-type.ts | 10 - .../vector/abstract-geoview-vector.ts | 2 + .../layer/gv-layers/raster/gv-esri-dynamic.ts | 82 +++++--- .../src/geo/layer/gv-layers/utils.ts | 4 +- .../layer-sets/feature-info-layer-set.ts | 7 + .../geo/utils/renderer/geoview-renderer.ts | 17 ++ 10 files changed, 377 insertions(+), 239 deletions(-) delete mode 100644 packages/geoview-core/src/core/workers/fetch-worker-type.ts diff --git a/packages/geoview-core/src/core/workers/abstract-worker-pool.ts b/packages/geoview-core/src/core/workers/abstract-worker-pool.ts index fe866c33093..fa32159c924 100644 --- a/packages/geoview-core/src/core/workers/abstract-worker-pool.ts +++ b/packages/geoview-core/src/core/workers/abstract-worker-pool.ts @@ -1,34 +1,58 @@ -import { AbstractWorker } from './abstract-worker'; - -export abstract class AbstractWorkerPool { - protected workers: AbstractWorker[] = []; - - protected busyWorkers = new Set>(); - - protected WorkerClass: new () => AbstractWorker; - - protected name: string; - - constructor(name: string, workerClass: new () => AbstractWorker, numWorkers = navigator.hardwareConcurrency || 4) { - this.name = name; - this.WorkerClass = workerClass; - this.initializeWorkers(numWorkers); - } - - protected initializeWorkers(numWorkers: number): void { - for (let i = 0; i < numWorkers; i++) { - const worker = new this.WorkerClass(); - this.workers.push(worker); - } - } - - public abstract init(): Promise; - - public abstract process(params: unknown): Promise; - - public terminate(): void { - this.workers.forEach((worker) => worker.terminate()); - this.workers = []; - this.busyWorkers.clear(); - } -} +import { AbstractWorker } from './abstract-worker'; + +/** + * Abstract base class for managing a pool of workers. + * Provides common functionality for worker pool management. + * @template T - The type of worker being managed + */ +export abstract class AbstractWorkerPool { + /** Array of worker instances in the pool */ + protected workers: AbstractWorker[] = []; + + /** Set of currently busy workers */ + protected busyWorkers = new Set>(); + + /** Constructor function for creating new worker instances */ + protected WorkerClass: new () => AbstractWorker; + + /** Name identifier for the worker pool */ + protected name: string; + + /** + * Creates an instance of AbstractWorkerPool. + * @param {string} name - Name identifier for the worker pool + * @param {new () => AbstractWorker} workerClass - Constructor for creating worker instances + * @param {number} numWorkers - Number of workers to initialize in the pool + */ + constructor(name: string, workerClass: new () => AbstractWorker, numWorkers = navigator.hardwareConcurrency || 4) { + this.name = name; + this.WorkerClass = workerClass; + this.initializeWorkers(numWorkers); + } + + /** + * Initializes the specified number of workers in the pool. + * @param {number} numWorkers - Number of workers to create + * @returns {void} + */ + protected initializeWorkers(numWorkers: number): void { + for (let i = 0; i < numWorkers; i++) { + const worker = new this.WorkerClass(); + this.workers.push(worker); + } + } + + /** + * Gets an available worker from the pool. + * @returns {AbstractWorker | undefined} + */ + protected getAvailableWorker(): AbstractWorker | undefined { + return this.workers.find((w) => !this.busyWorkers.has(w)); + } + + public terminate(): void { + this.workers.forEach((worker) => worker.terminate()); + this.workers = []; + this.busyWorkers.clear(); + } +} diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts index f5cd3dc8888..adc4ce91a31 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-pool.ts @@ -1,51 +1,64 @@ -import { AbstractWorkerPool } from './abstract-worker-pool'; -import { FetchEsriWorker, FetchEsriWorkerType } from './fetch-esri-worker'; -import { QueryParams } from './fetch-esri-worker-script'; -import { createWorkerLogger } from './helper/logger-worker'; - -import { TypeJsonObject } from '@/api/config/types/config-types'; - -export class FetchEsriWorkerPool extends AbstractWorkerPool { - #logger = createWorkerLogger('FetchEsriWorkerPool'); - - constructor(numWorkers = navigator.hardwareConcurrency || 4) { - super('FetchEsriWorkerPool', FetchEsriWorker, numWorkers); - this.#logger.logInfo('Worker pool created', `Number of workers: ${numWorkers}`); - } - - public async init(): Promise { - try { - this.#logger.logTrace('Initializing worker pool'); - await Promise.all(this.workers.map((worker) => worker.init())); - this.#logger.logTrace('Worker pool initialized'); - } catch (error) { - this.#logger.logError('Worker pool initialization failed', error); - throw error; - } - } - - public async process(params: QueryParams): Promise { - const availableWorker = this.workers.find((w) => !this.busyWorkers.has(w)); - if (!availableWorker) { - throw new Error('No available workers'); - } - - const result = await availableWorker.process(params); - return result as TypeJsonObject; - } - - // /** - // * Process an ESRI query and transform features using a worker from the pool - // */ - // public async processQuery(params: QueryParams): Promise { - // try { - // this.#logger.logTrace('Starting query process', params.url); - // const result = await this.process(params); - // this.#logger.logTrace('Query process completed'); - // return result as TypeJsonObject; - // } catch (error) { - // this.#logger.logError('Query process failed', error); - // throw error; - // } - // } -} +import { AbstractWorkerPool } from './abstract-worker-pool'; +import { FetchEsriWorker, FetchEsriWorkerType } from './fetch-esri-worker'; +import { QueryParams } from './fetch-esri-worker-script'; +import { createWorkerLogger } from './helper/logger-worker'; + +import { TypeJsonObject } from '@/api/config/types/config-types'; + +/** + * Worker pool for managing ESRI fetch operations. + * Extends AbstractWorkerPool to handle concurrent ESRI service requests. + * + * @class FetchEsriWorkerPool + * @extends {AbstractWorkerPool} + */ +export class FetchEsriWorkerPool extends AbstractWorkerPool { + // Logger instance for the fetch ESRI worker pool + #logger = createWorkerLogger('FetchEsriWorkerPool'); + + /** + * Creates an instance of FetchEsriWorkerPool. + * @param {number} [numWorkers=navigator.hardwareConcurrency || 4] - Number of workers to create in the pool + */ + constructor(numWorkers = navigator.hardwareConcurrency || 4) { + super('FetchEsriWorkerPool', FetchEsriWorker, numWorkers); + this.#logger.logInfo('Worker pool created', `Number of workers: ${numWorkers}`); + } + + /** + * Initializes all workers in the pool. + * @async + * @returns {Promise} + * @throws {Error} When worker initialization fails + */ + public async init(): Promise { + try { + await Promise.all(this.workers.map((worker) => worker.init())); + this.#logger.logTrace('Worker pool initialized'); + } catch (error) { + this.#logger.logError('Worker pool initialization failed', error); + throw error; + } + } + + /** + * Processes an ESRI query using an available worker from the pool. + * @param {QueryParams} params - Parameters for the ESRI query + * @returns {Promise} The query results + * @throws {Error} When no workers are available or query processing fails + */ + public async process(params: QueryParams): Promise { + const availableWorker = this.workers.find((w) => !this.busyWorkers.has(w)); + if (!availableWorker) { + throw new Error('No available workers'); + } + + try { + this.busyWorkers.add(availableWorker); + const result = await availableWorker.process(params); + return result as TypeJsonObject; + } finally { + this.busyWorkers.delete(availableWorker); + } + } +} diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts index c90fa5f13af..9d1208d054f 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts @@ -1,69 +1,111 @@ -import { expose } from 'comlink'; - -import { createWorkerLogger } from './helper/logger-worker'; - -import { TypeJsonObject } from '@/api/config/types/config-types'; -import { TypeStyleGeometry } from '@/api/config/types/map-schema-types'; - -export interface QueryParams { - url: string; - geometryType: TypeStyleGeometry; - objectIds: number[]; - queryGeometry: boolean; - projection: number; - maxAllowableOffset: number; -} - -const logger = createWorkerLogger('FetchEsriWorker'); - -// Move the ESRI query function directly into the worker to avoid circular dependencies -async function queryEsriFeatures(params: QueryParams): Promise { - const response = await fetch(`${params.url}/query`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - f: 'json', - geometryType: params.geometryType, - objectIds: params.objectIds.join(','), - outFields: '*', - returnGeometry: params.queryGeometry.toString(), - outSR: params.projection.toString(), - maxAllowableOffset: params.maxAllowableOffset.toString(), - }), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - return response.json(); -} - -const worker = { - // eslint-disable-next-line require-await - async init(): Promise { - try { - logger.logTrace('init worker', 'FetchEsriWorker initialized'); - } catch { - logger.logError('init worker', 'FetchEsriWorker failed to initialize'); - } - }, - - async process(params: QueryParams): Promise { - try { - logger.logTrace('process worker - Starting query processing', params.url); - const response = await queryEsriFeatures(params); - logger.logDebug('process worker - Query completed'); - return response; - } catch (error) { - logger.logError('process worker - Query processing failed', error); - throw error; - } - }, -}; - -// Expose the worker methods to be accessible from the main thread -expose(worker); -export default {} as typeof Worker & { new (): Worker }; +import { expose } from 'comlink'; + +import { createWorkerLogger } from './helper/logger-worker'; +import { TypeJsonObject } from '@/api/config/types/config-types'; + +/** + * This worker script is designed to be used with the FetchEsriWorker class. + * It handles the transformation of fetch of features from ArcGIS server. + * + * The main operations are: + * 1. Initialization: Set up the worker, empty for now. + * 2. Processing: Fetch the server and return the JSON. + */ + +/** + * Interface for ESRI query parameters + * @interface QueryParams + * @property {string} url - The URL of the ESRI service endpoint + * @property {string} geometryType - The type of geometry being queried + * @property {number[]} objectIds - Array of object IDs to query + * @property {boolean} queryGeometry - Whether to include geometry in the query + * @property {number} projection - The spatial reference ID for the output + * @property {number} maxAllowableOffset - The maximum allowable offset for geometry simplification + */ +export interface QueryParams { + url: string; + geometryType: string; + objectIds: number[]; + queryGeometry: boolean; + projection: number; + maxAllowableOffset: number; +} + +// Initialize the worker logger +const logger = createWorkerLogger('FetchEsriWorker'); + +/** + * Queries features from an ESRI service + * @async + * @param {QueryParams} params - The parameters for the ESRI query + * @returns {Promise} A promise that resolves to the query results + * @throws {Error} When the HTTP request fails + */ +async function queryEsriFeatures(params: QueryParams): Promise { + // Move the ESRI query function directly into the worker to avoid circular dependencies + const urlParam = `?objectIds=${params.objectIds}&outFields=*&returnGeometry=${params.queryGeometry}&outSR=${params.projection}&geometryPrecision=1&maxAllowableOffset=${params.maxAllowableOffset}&f=json`; + + const identifyResponse = await fetch(`${params.url}/query${urlParam}`); + const identifyJsonResponse = await identifyResponse.json(); + + // const response = await fetch(`${params.url}/query`, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/x-www-form-urlencoded', + // }, + // body: new URLSearchParams({ + // f: 'json', + // geometryType: params.geometryType, + // objectIds: params.objectIds.join(','), + // outFields: '*', + // returnGeometry: params.queryGeometry.toString(), + // outSR: params.projection.toString(), + // maxAllowableOffset: params.maxAllowableOffset.toString(), + // }), + // }); + + // if (!response.ok) { + // throw new Error(`HTTP error! status: ${response.status}`); + // } + + return identifyJsonResponse; +} + +/** + * The main worker object containing methods for initialization and processing. + */ +const worker = { + /** + * Initializes the worker. + */ + init(): void { + try { + logger.logTrace('FetchEsriWorker initialized'); + } catch { + logger.logError('FetchEsriWorker failed to initialize'); + } + }, + + /** + * Processes an ESRI query request + * @async + * @param {QueryParams} params - The parameters for the ESRI query + * @returns {Promise} A promise that resolves to the query results + * @throws {Error} When the query processing fails + */ + async process(params: QueryParams): Promise { + try { + logger.logTrace('Starting query processing', JSON.stringify(params)); + const response = await queryEsriFeatures(params); + logger.logDebug('Query completed'); + return response; + } catch (error) { + logger.logError('Query processing failed', error); + throw error; + } + }, +}; + +// Expose the worker methods to be accessible from the main thread +expose(worker); +export default {} as typeof Worker & { new (): Worker }; diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts index 3e9b6630fa7..e6db1f3a642 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts @@ -1,42 +1,65 @@ -import { AbstractWorker } from './abstract-worker'; -import { QueryParams } from './fetch-esri-worker-script'; -import { TypeJsonObject } from '@/api/config/types/config-types'; - -export interface FetchEsriWorkerType { - /** - * Initializes the worker - empty for now. - */ - init: () => Promise; - - /** - * Processes an ESRI query JSON export. - * @param {QueryParams} queryParams - The query parameters for the fetch. - * @returns {TypeJsonObject} A promise that resolves to the response fetch as JSON string. - */ - process: (queryParams: QueryParams) => Promise; -} - -export class FetchEsriWorker extends AbstractWorker { - constructor() { - super('FetchEsriWorker', new Worker(new URL('./fetch-esri-worker-script.ts', import.meta.url))); - } - - /** - * Initializes the worker - empty for now. - * @returns A promise that resolves when initialization is complete. - */ - public async init(): Promise { - const result = await this.proxy.init(); - return result; - } - - /** - * Processes a JSON fetch for an esri query. - * @param {QueryParams} queryParams - The query parameters for the fetch. - * @returns A promise that resolves to the processed JSON string. - */ - public async process(queryParams: QueryParams): Promise { - const result = await this.proxy.process(queryParams); - return result; - } -} +import { AbstractWorker } from './abstract-worker'; +import { QueryParams } from './fetch-esri-worker-script'; +import { TypeJsonObject } from '@/api/config/types/config-types'; + +/** + * How to create a new worker: + * + * 1. Define an interface for your worker's exposed methods (init, process and other is needed) + * 2. Create a new class extending AbstractWorker (e.g. export class MyWorker extends AbstractWorker) + * 3. Create the actual worker script (my-worker-script.ts): + * 4. Use your new worker in the main application: + * const myWorker = new MyWorker(); + * const result1 = await myWorker.init('test'); + * const result2 = await myWorker.process(42, true); + */ + +/** + * Interface defining the methods exposed by the fetch ESRI worker. + */ +export interface FetchEsriWorkerType { + /** + * Initializes the worker - empty for now. + */ + init: () => Promise; + + /** + * Processes an ESRI query JSON export. + * @param {QueryParams} queryParams - The query parameters for the fetch. + * @returns {TypeJsonObject} A promise that resolves to the response fetch as JSON string. + */ + process: (queryParams: QueryParams) => Promise; +} + +/** + * Class representing a fetch ESRI worker. + * Extends AbstractWorker to handle fetch operations on ESRI ArcGIS server in a separate thread. + */ +export class FetchEsriWorker extends AbstractWorker { + /** + * Creates an instance of FetchEsriWorker. + * Initializes the worker with the 'fetch-esri' script. + */ + constructor() { + super('FetchEsriWorker', new Worker(new URL('./fetch-esri-worker-script.ts', import.meta.url))); + } + + /** + * Initializes the worker - empty for now. + * @returns A promise that resolves when initialization is complete. + */ + public async init(): Promise { + const result = await this.proxy.init(); + return result; + } + + /** + * Processes a JSON fetch for an esri query. + * @param {QueryParams} queryParams - The query parameters for the fetch. + * @returns A promise that resolves to the processed JSON string. + */ + public async process(queryParams: QueryParams): Promise { + const result = await this.proxy.process(queryParams); + return result; + } +} diff --git a/packages/geoview-core/src/core/workers/fetch-worker-type.ts b/packages/geoview-core/src/core/workers/fetch-worker-type.ts deleted file mode 100644 index 0fa206e849c..00000000000 --- a/packages/geoview-core/src/core/workers/fetch-worker-type.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { TypeStyleGeometry } from '@/api/config/types/map-schema-types'; - -// export interface QueryParams { -// url: string; -// geometryType: TypeStyleGeometry; -// objectIds: number[]; -// queryGeometry: boolean; -// projection: number; -// maxAllowableOffset: number; -// } diff --git a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts index 513b3c2ac96..3cb5acedf62 100644 --- a/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts +++ b/packages/geoview-core/src/geo/layer/geoview-layers/vector/abstract-geoview-vector.ts @@ -266,6 +266,8 @@ export abstract class AbstractGeoViewVector extends AbstractGeoViewLayer { queryLimit: number = 10 ): Promise { // Update url + // TODO: Performance - Check if we should add &maxAllowableOffset=10 to the url. It creates small sliver but download size if 18mb compare to 50mb for outlier-election-2019 + // TODO.CONT: Download time is 90 seconds compare to 130 seconds. It may worth the loss of precision... const baseUrl = url.replace('&where=1%3D1&returnCountOnly=true', `&outfields=*&geometryPrecision=1`); const featureFetchLimit = maxRecordCount && maxRecordCount < featureLimit ? maxRecordCount : featureLimit; diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 14cf2984226..d057cb4d57f 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -22,9 +22,9 @@ import { TypeLayerStyleConfig, TypeLayerStyleConfigInfo, } from '@/geo/map/map-schema-types'; -import { esriGetFieldType, esriGetFieldDomain, esriQueryRecordsByUrlObjectIds } from '../utils'; +import { esriGetFieldType, esriGetFieldDomain } from '../utils'; import { AbstractGVRaster } from './abstract-gv-raster'; -import { TypeOutfieldsType, TypeStyleGeometry } from '@/api/config/types/map-schema-types'; +import { TypeOutfieldsType } from '@/api/config/types/map-schema-types'; import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers'; import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; @@ -60,11 +60,16 @@ export class GVEsriDynamic extends AbstractGVRaster { public constructor(mapId: string, olSource: ImageArcGISRest, layerConfig: EsriDynamicLayerEntryConfig) { super(mapId, olSource, layerConfig); + // TODO: Performance - Do we need worker pool or one worker per layer is enough. If a worker is already working we should terminate it + // TODO.CONT: and use the abort controller to cancel the fetch and start a new one. So every esriDynamic layer has it's own worker. // Setup the worker pool this.#fetchWorkerPool = new FetchEsriWorkerPool(); - this.#fetchWorkerPool.init().then(() => logger.logTraceCore('Worker pool for fetch ESRI initialized')); + this.#fetchWorkerPool + .init() + .then(() => logger.logTraceCore('Worker pool for fetch ESRI initialized')) + .catch((err) => logger.logError('Worker pool error', err)); - // TODO: Investigate to see if we can call the export map for the whole service at once instead of making many call + // TODO: Performance - Investigate to see if we can call the export map for the whole service at once instead of making many call // TODO.CONT: We can use the layers and layersDef parameters to set what should be visible. // TODO.CONT: layers=show:layerId ; layerDefs={ "layerId": "layer def" } // TODO.CONT: There is no allowableOffset on esri dynamic to speed up. We will need to see what can be done for layers in wrong projection @@ -76,7 +81,7 @@ export class GVEsriDynamic extends AbstractGVRaster { properties: { layerConfig }, }; - // TODO: For testing purpose on projection and performance + // TODO: Performance - For testing purpose on projection and performance if (layerConfig.geoviewLayerConfig.geoviewLayerId === '6c343726-1e92-451a-876a-76e17d398a1c') { imageLayerOptions.source?.updateParams({ imageSR: 3978 }); } @@ -269,7 +274,13 @@ export class GVEsriDynamic extends AbstractGVRaster { return this.getFeatureInfoAtLongLat(projCoordinate, queryGeometry); } - async yourQueryMethod(layerConfig: any, objectIds: number[], queryGeometry: boolean): Promise { + async getFeatureInfoGeometryWorker( + layerConfig: EsriDynamicLayerEntryConfig, + objectIds: number[], + queryGeometry: boolean, + projection: number, + maxAllowableOffset: number + ): Promise { try { const params: QueryParams = { url: layerConfig.source.dataAccessPath + layerConfig.layerId, @@ -277,14 +288,15 @@ export class GVEsriDynamic extends AbstractGVRaster { objectIds, queryGeometry, projection: 3978, - maxAllowableOffset: 7000, + maxAllowableOffset, }; const response = await this.#fetchWorkerPool.process(params); + logger.logDebug('worker', response); const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; return await this.formatFeatureInfoResult(features, layerConfig); } catch (error) { - console.error('Query processing failed:', error); + logger.logError('Query processing failed:', error); throw error; } } @@ -306,7 +318,7 @@ export class GVEsriDynamic extends AbstractGVRaster { // Get the layer config in a loaded phase const layerConfig = this.getLayerConfig(); - // If not queryable + // If not queryable or there no url access path to query return [] if (!layerConfig.source.featureInfo?.queryable) return []; let identifyUrl = layerConfig.source.dataAccessPath; @@ -315,7 +327,7 @@ export class GVEsriDynamic extends AbstractGVRaster { identifyUrl = identifyUrl.endsWith('/') ? identifyUrl : `${identifyUrl}/`; // GV: We cannot directly use the view extent and reproject. If we do so some layers (issue #2413) identify will return empty resultset - // GV-CONT: This happen with max extent as initial extent and 3978 projection. If we use only the LL and UP corners for the repojection it works + // GV.CONT: This happen with max extent as initial extent and 3978 projection. If we use only the LL and UP corners for the repojection it works const mapViewer = this.getMapViewer(); const mapExtent = mapViewer.getView().calculateExtent(); const boundsLL = mapViewer.convertCoordinateMapProjToLngLat([mapExtent[0], mapExtent[1]]); @@ -324,7 +336,7 @@ export class GVEsriDynamic extends AbstractGVRaster { const layerDefs = this.getOLSource()?.getParams()?.layerDefs || ''; const size = mapViewer.map.getSize()!; - // Identify query to get oid features value, at this point we do not query geometry + // Identify query to get oid features value and attributes, at this point we do not query geometry identifyUrl = `${identifyUrl}identify?f=json&tolerance=${this.hitTolerance}` + `&mapExtent=${extent.xmin},${extent.ymin},${extent.xmax},${extent.ymax}` + @@ -332,7 +344,7 @@ export class GVEsriDynamic extends AbstractGVRaster { `&layers=visible:${layerConfig.layerId}` + `&layerDefs=${layerDefs}` + `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}` + - `&returnGeometry=false&sr=4326`; + `&returnGeometry=false&sr=4326&&returnFieldName=true`; const identifyResponse = await fetch(identifyUrl); const identifyJsonResponse = await identifyResponse.json(); @@ -341,7 +353,7 @@ export class GVEsriDynamic extends AbstractGVRaster { throw new Error(`Error code = ${identifyJsonResponse.error.code} ${identifyJsonResponse.error.message}` || ''); } - // If no features identified + // If no features identified return [] if (identifyJsonResponse.results.length === 0) return []; // Extract OBJECTIDs @@ -353,29 +365,37 @@ export class GVEsriDynamic extends AbstractGVRaster { // Get meters per pixel to set the maxAllowableOffset to simplify return geometry const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; - this.yourQueryMethod(layerConfig, objectIds, true).then((features) => console.log('Features worker', features)); - - // TODO: We need to separate the query attribute from geometry. We can use the attributes returned by identify to show details panel - // TODO.CONT: or create 2 distinc query one for attributes and one for geometry. This way we can display the anel faster and wait later for geometry + // TODO: Performance - We need to separate the query attribute from geometry. We can use the attributes returned by identify to show details panel + // TODO.CONT: or create 2 distinc query one for attributes and one for geometry. This way we can display the panel faster and wait later for geometry // TODO.CONT: We need to see if we can fetch in async mode without freezing the ui. If not we will need a web worker for the fetch. // TODO.CONT: If we go with web worker, we need a reusable approach so we can use with all our queries // Get features - const response = await esriQueryRecordsByUrlObjectIds( - layerConfig.source.dataAccessPath + layerConfig.layerId, - (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', '') as TypeStyleGeometry, - objectIds, - '*', - false, - mapViewer.getMapState().currentProjection, - maxAllowableOffset, - false - ); - - // TODO: This is also time consuming, the creation of the feature can take several seconds, check web worker + // const response = await esriQueryRecordsByUrlObjectIds( + // layerConfig.source.dataAccessPath + layerConfig.layerId, + // (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', '') as TypeStyleGeometry, + // objectIds, + // '*', + // false, + // mapViewer.getMapState().currentProjection, + // maxAllowableOffset, + // false + // ); + + // TODO: Performance - This is also time consuming, the creation of the feature can take several seconds, check web worker // TODO.CONT: Because web worker can only use sereialize date and not object with function it may be difficult for this... - // Transform the features in an OL feature - const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; + // TODO.CONT: For the moment, the feature is created without a geometry. This should be added by web worker + // TODO.CONT: Splitting the query will help avoid layer details error when geometry is big anf let ui not frezze. The Web worker + // TODO.CONT: geometry assignement must not be in an async function. + // Transform the features in an OL feature - at this point, there is no geometry associated with the feature + const features = new EsriJSON().readFeatures({ features: identifyJsonResponse.results }) as Feature[]; const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); + + this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) + .then((featuresJSON) => { + logger.logDebug('Features worker', featuresJSON); + }) + .catch((err) => logger.logError('Features worker', err)); + return arrayOfFeatureInfoEntries; } catch (error) { // Log diff --git a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts index f461015fd3d..755da694f64 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts @@ -114,7 +114,7 @@ export async function esriQueryRecordsByUrl( geometryType?: TypeStyleGeometry, parseFeatureInfoEntries = true ): Promise { - // TODO: Refactor - Suggestion to rework this function and the one in EsriDynamic.getFeatureInfoAtLongLat(), making + // TODO: Performance - Refactor - Suggestion to rework this function and the one in EsriDynamic.getFeatureInfoAtLongLat(), making // TO.DO.CONT: the latter redirect to this one here and merge some logic between the 2 functions ideally making this // TO.DO.CONT: one here return a TypeFeatureInfoEntry[] with options to have returnGeometry=true or false and such. // Query the data @@ -162,7 +162,7 @@ export function esriQueryRecordsByUrlObjectIds( // Query const oids = objectIds.join(','); - const url = `${layerUrl}/query?where=&objectIds=${oids}&outFields=${fields}&returnGeometry=${geometry}&outSR=${outSR}&geometryPrecision=1${offset}&f=json`; + const url = `${layerUrl}/query?&objectIds=${oids}&outFields=${fields}&returnGeometry=${geometry}&outSR=${outSR}&geometryPrecision=1${offset}&f=json`; // Redirect return esriQueryRecordsByUrl(url, geometryType, parseFeatureInfoEntries); diff --git a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts index a17eadb3272..e23f28db064 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/feature-info-layer-set.ts @@ -110,6 +110,13 @@ export class FeatureInfoLayerSet extends AbstractLayerSet { // GV Each query should be distinct as far as the resultSet goes! The 'reinitialization' below isn't sufficient. // GV As it is (and was like this before events refactor), the this.resultSet is mutating between async calls. + // TODO: Use the AbortController and kill the active query if there is one in progress. The query layer here call the getFeatureInfoAtLongLat + // TODO.CONT: in gv-esri-dynamic. It is for this particular format we need check because identify are slow and many can be sent at the same time + // Create an AbortController instance + // const controller = new AbortController(); + // const signal = controller.signal; + // controller.abort(); // Cancels the fetch request + // Prepare to hold all promises of features in the loop below const allPromises: Promise[] = []; diff --git a/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts b/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts index eacb599a997..2dcfa64a28c 100644 --- a/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts +++ b/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts @@ -1580,6 +1580,23 @@ export async function getFeatureCanvas( if (style[geometryType]) { const styleSettings = style[geometryType]!; const { type } = styleSettings; + + // TODO: Performance - Wrap the style processing in a Promise to prevent blocking, Use requestAnimationFrame to process style during next frame + // Wrap the style processing in a Promise to prevent blocking + // return new Promise((resolve) => { + // // Use requestAnimationFrame to process style during next frame + // requestAnimationFrame(() => { + // const processedStyle = processStyle[type][geometryType].call( + // '', + // styleSettings, + // feature as Feature, + // filterEquation, + // legendFilterIsOff + // ); + // resolve(processedStyle); + // }); + // }); + const featureStyle = processStyle[type][geometryType](styleSettings, feature, filterEquation, legendFilterIsOff); if (featureStyle) { if (geometryType === 'Point') { From 8250065bb822433059db0212f39b3dba5ddb3318 Mon Sep 17 00:00:00 2001 From: jolevesq Date: Thu, 16 Jan 2025 16:05:00 -0500 Subject: [PATCH 07/10] update geometry from worker for point --- .../core/components/details/feature-info.tsx | 2 +- .../add-new-layer/add-new-layer.tsx | 74 +++++++++---------- .../core/workers/fetch-esri-worker-script.ts | 22 +----- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 46 ++++++++++-- .../src/geo/map/feature-highlight.ts | 2 +- .../geo/utils/renderer/geoview-renderer.ts | 2 +- 6 files changed, 77 insertions(+), 71 deletions(-) diff --git a/packages/geoview-core/src/core/components/details/feature-info.tsx b/packages/geoview-core/src/core/components/details/feature-info.tsx index 5a76c3147b2..d2eee3b6890 100644 --- a/packages/geoview-core/src/core/components/details/feature-info.tsx +++ b/packages/geoview-core/src/core/components/details/feature-info.tsx @@ -80,7 +80,7 @@ const FeatureHeader = memo(function FeatureHeader({ iconSrc, name, hasGeometry, }); export function FeatureInfo({ feature }: FeatureInfoProps): JSX.Element | null { - logger.logTraceRender('components/details/feature-info'); + logger.logTraceRender('components/details/feature-info', feature); // Hooks const theme = useTheme(); diff --git a/packages/geoview-core/src/core/components/layers/left-panel/add-new-layer/add-new-layer.tsx b/packages/geoview-core/src/core/components/layers/left-panel/add-new-layer/add-new-layer.tsx index 2a223d3ab57..9f3ce83b411 100644 --- a/packages/geoview-core/src/core/components/layers/left-panel/add-new-layer/add-new-layer.tsx +++ b/packages/geoview-core/src/core/components/layers/left-panel/add-new-layer/add-new-layer.tsx @@ -34,12 +34,10 @@ import { OgcFeatureLayerEntryConfig } from '@/core/utils/config/validation-class import { CsvLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/csv-layer-entry-config'; import { GeoJSONLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/geojson-layer-entry-config'; import { EsriFeatureLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/esri-feature-layer-entry-config'; -import { GeoPackageLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-validation-classes/geopackage-layer-config-entry'; import { XYZTilesLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/xyz-layer-entry-config'; import { EsriDynamicLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-dynamic-layer-entry-config'; import { EsriImageLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-image-layer-entry-config'; import { OgcWmsLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/ogc-wms-layer-entry-config'; -import { GeoPackage, TypeGeoPackageLayerConfig } from '@/geo/layer/geoview-layers/vector/geopackage'; import { GeoCore } from '@/geo/layer/other/geocore'; import { GeoViewLayerAddedResult, LayerApi } from '@/geo/layer/layer'; import { @@ -66,7 +64,8 @@ export function AddNewLayer(): JSX.Element { const { t } = useTranslation(); const theme = useTheme(); - const { CSV, ESRI_DYNAMIC, ESRI_FEATURE, ESRI_IMAGE, GEOJSON, GEOPACKAGE, WMS, WFS, OGC_FEATURE, XYZ_TILES } = CONST_LAYER_TYPES; + // TODO: refactor - add the Geopacakges when refactor is done GEOPACKAGE + const { CSV, ESRI_DYNAMIC, ESRI_FEATURE, ESRI_IMAGE, GEOJSON, WMS, WFS, OGC_FEATURE, XYZ_TILES } = CONST_LAYER_TYPES; const { GEOCORE } = CONST_LAYER_ENTRY_TYPES; const [geoviewLayerInstance, setGeoviewLayerInstance] = useState(); @@ -104,7 +103,6 @@ export function AddNewLayer(): JSX.Element { [ESRI_FEATURE, 'ESRI Feature Service'], [ESRI_IMAGE, 'ESRI Image Service'], [GEOJSON, 'GeoJSON'], - [GEOPACKAGE, 'GeoPackage'], [WMS, 'OGC Web Map Service (WMS)'], [WFS, 'OGC Web Feature Service (WFS)'], [OGC_FEATURE, 'OGC API Features'], @@ -712,43 +710,44 @@ export function AddNewLayer(): JSX.Element { return true; }; + // TODO: refactor - add the Geopacakges when refactor is done. We keep code for reference /** * Using the layerURL state object, check whether URL is a valid GeoPackage. * * @returns {boolean} True if layer passes validation */ - const geoPackageValidation = (): boolean => { - try { - // We assume a single GeoPackage file is present - setHasMetadata(false); - const geoPackageGeoviewLayerConfig = { - geoviewLayerType: GEOPACKAGE, - listOfLayerEntryConfig: [] as GeoPackageLayerEntryConfig[], - } as TypeGeoPackageLayerConfig; - const geopackageGeoviewLayerInstance = new GeoPackage(mapId, geoPackageGeoviewLayerConfig); - // Synchronize the geoviewLayerId. - geoPackageGeoviewLayerConfig.geoviewLayerId = geopackageGeoviewLayerInstance.geoviewLayerId; - setGeoviewLayerInstance(geopackageGeoviewLayerInstance); - const layers = [ - new GeoPackageLayerEntryConfig({ - geoviewLayerConfig: geoPackageGeoviewLayerConfig, - layerId: geoPackageGeoviewLayerConfig.geoviewLayerId, - layerName: '', - source: { - dataAccessPath: layerURL, - }, - } as GeoPackageLayerEntryConfig), - ]; - setLayerName(layers[0].layerName!); - setLayerEntries([layers[0]]); - } catch (error) { - emitErrorServer('GeoPackage'); - // Log error - logger.logError(error); - return false; - } - return true; - }; + // const geoPackageValidation = (): boolean => { + // try { + // // We assume a single GeoPackage file is present + // setHasMetadata(false); + // const geoPackageGeoviewLayerConfig = { + // geoviewLayerType: GEOPACKAGE, + // listOfLayerEntryConfig: [] as GeoPackageLayerEntryConfig[], + // } as TypeGeoPackageLayerConfig; + // const geopackageGeoviewLayerInstance = new GeoPackage(mapId, geoPackageGeoviewLayerConfig); + // // Synchronize the geoviewLayerId. + // geoPackageGeoviewLayerConfig.geoviewLayerId = geopackageGeoviewLayerInstance.geoviewLayerId; + // setGeoviewLayerInstance(geopackageGeoviewLayerInstance); + // const layers = [ + // new GeoPackageLayerEntryConfig({ + // geoviewLayerConfig: geoPackageGeoviewLayerConfig, + // layerId: geoPackageGeoviewLayerConfig.geoviewLayerId, + // layerName: '', + // source: { + // dataAccessPath: layerURL, + // }, + // } as GeoPackageLayerEntryConfig), + // ]; + // setLayerName(layers[0].layerName!); + // setLayerEntries([layers[0]]); + // } catch (error) { + // emitErrorServer('GeoPackage'); + // // Log error + // logger.logError(error); + // return false; + // } + // return true; + // }; /** * Attempt to determine the layer type based on the URL format @@ -769,8 +768,6 @@ export function AddNewLayer(): JSX.Element { setLayerType(WFS); } else if (displayURL.toUpperCase().endsWith('.JSON') || displayURL.toUpperCase().endsWith('.GEOJSON')) { setLayerType(GEOJSON); - } else if (displayURL.toUpperCase().endsWith('.GPKG')) { - setLayerType(GEOPACKAGE); } else if (displayURL.toUpperCase().indexOf('{Z}/{X}/{Y}') !== -1 || displayURL.toUpperCase().indexOf('{Z}/{Y}/{X}') !== -1) { setLayerType(XYZ_TILES); } else if (displayURL.indexOf('/') === -1 && displayURL.replaceAll('-', '').length === 32) { @@ -818,7 +815,6 @@ export function AddNewLayer(): JSX.Element { else if (layerType === ESRI_FEATURE) promise = esriValidation(ESRI_FEATURE); else if (layerType === ESRI_IMAGE) promise = esriImageValidation(); else if (layerType === GEOJSON) promise = geoJSONValidation(); - else if (layerType === GEOPACKAGE) promise = Promise.resolve(geoPackageValidation()); else if (layerType === GEOCORE) promise = geocoreValidation(); else if (layerType === CSV) promise = csvValidation(); diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts index 9d1208d054f..a69bd165048 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts @@ -48,26 +48,6 @@ async function queryEsriFeatures(params: QueryParams): Promise { const identifyResponse = await fetch(`${params.url}/query${urlParam}`); const identifyJsonResponse = await identifyResponse.json(); - // const response = await fetch(`${params.url}/query`, { - // method: 'POST', - // headers: { - // 'Content-Type': 'application/x-www-form-urlencoded', - // }, - // body: new URLSearchParams({ - // f: 'json', - // geometryType: params.geometryType, - // objectIds: params.objectIds.join(','), - // outFields: '*', - // returnGeometry: params.queryGeometry.toString(), - // outSR: params.projection.toString(), - // maxAllowableOffset: params.maxAllowableOffset.toString(), - // }), - // }); - - // if (!response.ok) { - // throw new Error(`HTTP error! status: ${response.status}`); - // } - return identifyJsonResponse; } @@ -97,7 +77,7 @@ const worker = { try { logger.logTrace('Starting query processing', JSON.stringify(params)); const response = await queryEsriFeatures(params); - logger.logDebug('Query completed'); + logger.logTrace('Query completed'); return response; } catch (error) { logger.logError('Query processing failed', error); diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index d057cb4d57f..00b7ddc0915 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -32,6 +32,8 @@ import { TypeEsriImageLayerLegend } from './gv-esri-image'; import { TypeJsonObject } from '@/api/config/types/config-types'; import { FetchEsriWorkerPool } from '@/core/workers/fetch-esri-worker-pool'; import { QueryParams } from '@/core/workers/fetch-esri-worker-script'; +import { Circle, Point } from 'ol/geom'; +import { delay } from '@/core/utils/utilities'; type TypeFieldOfTheSameValue = { value: string | number | Date; nbOccurence: number }; type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryTree }[]; @@ -274,27 +276,33 @@ export class GVEsriDynamic extends AbstractGVRaster { return this.getFeatureInfoAtLongLat(projCoordinate, queryGeometry); } + /** + * Query the features geometry with a web worker + * @param {EsriDynamicLayerEntryConfig} layerConfig - The layer config + * @param {number[]} objectIds - Array of object IDs to query + * @param {boolean} queryGeometry - Whether to include geometry in the query + * @param {number} projection - The spatial reference ID for the output + * @param {number} maxAllowableOffset - The maximum allowable offset for geometry simplification + * @returns {TypeJsonObject} A promise of esri response for query. + */ async getFeatureInfoGeometryWorker( layerConfig: EsriDynamicLayerEntryConfig, objectIds: number[], queryGeometry: boolean, projection: number, maxAllowableOffset: number - ): Promise { + ): Promise { try { const params: QueryParams = { url: layerConfig.source.dataAccessPath + layerConfig.layerId, geometryType: (layerConfig.getLayerMetadata()!.geometryType as string).replace('esriGeometry', ''), objectIds, queryGeometry, - projection: 3978, + projection, maxAllowableOffset, }; - const response = await this.#fetchWorkerPool.process(params); - logger.logDebug('worker', response); - const features = new EsriJSON().readFeatures({ features: response }) as Feature[]; - return await this.formatFeatureInfoResult(features, layerConfig); + return await this.#fetchWorkerPool.process(params); } catch (error) { logger.logError('Query processing failed:', error); throw error; @@ -344,7 +352,7 @@ export class GVEsriDynamic extends AbstractGVRaster { `&layers=visible:${layerConfig.layerId}` + `&layerDefs=${layerDefs}` + `&geometryType=esriGeometryPoint&geometry=${lnglat[0]},${lnglat[1]}` + - `&returnGeometry=false&sr=4326&&returnFieldName=true`; + `&returnGeometry=false&sr=4326&returnFieldName=true`; const identifyResponse = await fetch(identifyUrl); const identifyJsonResponse = await identifyResponse.json(); @@ -360,7 +368,7 @@ export class GVEsriDynamic extends AbstractGVRaster { const oidField = layerConfig.source.featureInfo.outfields ? layerConfig.source.featureInfo.outfields.filter((field) => field.type === 'oid')[0].name : 'OBJECTID'; - const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => result.attributes[oidField]); + const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => result.attributes[oidField].replace(',', '')); // Get meters per pixel to set the maxAllowableOffset to simplify return geometry const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; @@ -392,6 +400,28 @@ export class GVEsriDynamic extends AbstractGVRaster { this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) .then((featuresJSON) => { + featuresJSON.features.forEach((feat, index) => { + const geom = new EsriJSON().readFeature(feat, { + dataProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, + featureProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, + }) as Feature; + + if ( + arrayOfFeatureInfoEntries[index] && + arrayOfFeatureInfoEntries[index].geometry && + arrayOfFeatureInfoEntries[index].geometry instanceof Feature + ) { + arrayOfFeatureInfoEntries[index].extent = geom.getGeometry()?.getExtent(); + arrayOfFeatureInfoEntries[index].geometry.setGeometry(geom.getGeometry()); + } + }); + + // arrayOfFeatureInfoEntries!.forEach((featureInfoEntry, i) => { + // const feature = features[i]; + // featureInfoEntry.geometry = feature.getGeometry(); + + // arrayOfFeatureInfoEntries[0].geometry.setGeometry(featuresJSON.features[0]) + // }); logger.logDebug('Features worker', featuresJSON); }) .catch((err) => logger.logError('Features worker', err)); diff --git a/packages/geoview-core/src/geo/map/feature-highlight.ts b/packages/geoview-core/src/geo/map/feature-highlight.ts index 827b9d2215d..298d0e1d5cd 100644 --- a/packages/geoview-core/src/geo/map/feature-highlight.ts +++ b/packages/geoview-core/src/geo/map/feature-highlight.ts @@ -185,7 +185,7 @@ export class FeatureHighlight { } } else if (feature.extent) { const { height, width } = feature.featureIcon; - const radius = Math.min(height, width) / 2 - 2 < 7 ? 7 : Math.min(height, width) / 2 - 2; + const radius = 7; // Math.min(height, width) / 2 - 2 < 7 ? 7 : Math.min(height, width) / 2 - 2; const center = getCenter(feature.extent); const newPoint = new Point(center); const newFeature = new Feature(newPoint); diff --git a/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts b/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts index 2dcfa64a28c..ba47e10f51d 100644 --- a/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts +++ b/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts @@ -1581,7 +1581,7 @@ export async function getFeatureCanvas( const styleSettings = style[geometryType]!; const { type } = styleSettings; - // TODO: Performance - Wrap the style processing in a Promise to prevent blocking, Use requestAnimationFrame to process style during next frame + // TODO: Performance #2688 - Wrap the style processing in a Promise to prevent blocking, Use requestAnimationFrame to process style during next frame // Wrap the style processing in a Promise to prevent blocking // return new Promise((resolve) => { // // Use requestAnimationFrame to process style during next frame From 91e92e6f5283f31bf92a9f58be341effa6da15ef Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 17 Jan 2025 07:53:05 -0500 Subject: [PATCH 08/10] pre rebase --- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 16 +++++++--------- .../layer-sets/hover-feature-info-layer-set.ts | 2 +- .../src/geo/map/feature-highlight.ts | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 00b7ddc0915..f1e7b9b7987 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -32,8 +32,6 @@ import { TypeEsriImageLayerLegend } from './gv-esri-image'; import { TypeJsonObject } from '@/api/config/types/config-types'; import { FetchEsriWorkerPool } from '@/core/workers/fetch-esri-worker-pool'; import { QueryParams } from '@/core/workers/fetch-esri-worker-script'; -import { Circle, Point } from 'ol/geom'; -import { delay } from '@/core/utils/utilities'; type TypeFieldOfTheSameValue = { value: string | number | Date; nbOccurence: number }; type TypeQueryTree = { fieldValue: string | number | Date; nextField: TypeQueryTree }[]; @@ -368,7 +366,7 @@ export class GVEsriDynamic extends AbstractGVRaster { const oidField = layerConfig.source.featureInfo.outfields ? layerConfig.source.featureInfo.outfields.filter((field) => field.type === 'oid')[0].name : 'OBJECTID'; - const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => result.attributes[oidField].replace(',', '')); + const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => String(result.attributes[oidField]).replace(',', '')); // Get meters per pixel to set the maxAllowableOffset to simplify return geometry const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; @@ -400,19 +398,19 @@ export class GVEsriDynamic extends AbstractGVRaster { this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) .then((featuresJSON) => { - featuresJSON.features.forEach((feat, index) => { + (featuresJSON.features as unknown as TypeFeatureInfoEntry[]).forEach((feat: TypeFeatureInfoEntry, index: number) => { const geom = new EsriJSON().readFeature(feat, { dataProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, featureProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, }) as Feature; if ( - arrayOfFeatureInfoEntries[index] && - arrayOfFeatureInfoEntries[index].geometry && - arrayOfFeatureInfoEntries[index].geometry instanceof Feature + arrayOfFeatureInfoEntries![index] && + arrayOfFeatureInfoEntries![index].geometry && + arrayOfFeatureInfoEntries![index].geometry instanceof Feature ) { - arrayOfFeatureInfoEntries[index].extent = geom.getGeometry()?.getExtent(); - arrayOfFeatureInfoEntries[index].geometry.setGeometry(geom.getGeometry()); + arrayOfFeatureInfoEntries![index].extent = geom.getGeometry()?.getExtent(); + arrayOfFeatureInfoEntries![index].geometry.setGeometry(geom.getGeometry()); } }); diff --git a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts index 3dfdd74927e..9662e2051a4 100644 --- a/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts +++ b/packages/geoview-core/src/geo/layer/layer-sets/hover-feature-info-layer-set.ts @@ -107,7 +107,7 @@ export class HoverFeatureInfoLayerSet extends AbstractLayerSet { // If layer was found if (layer && layer instanceof AbstractGVLayer) { // If state is not queryable - return; // if (!AbstractLayerSet.isStateQueryable(layer)) return; + if (!AbstractLayerSet.isStateQueryable(layer)) return; // Flag processing this.resultSet[layerPath].feature = undefined; diff --git a/packages/geoview-core/src/geo/map/feature-highlight.ts b/packages/geoview-core/src/geo/map/feature-highlight.ts index 298d0e1d5cd..827b9d2215d 100644 --- a/packages/geoview-core/src/geo/map/feature-highlight.ts +++ b/packages/geoview-core/src/geo/map/feature-highlight.ts @@ -185,7 +185,7 @@ export class FeatureHighlight { } } else if (feature.extent) { const { height, width } = feature.featureIcon; - const radius = 7; // Math.min(height, width) / 2 - 2 < 7 ? 7 : Math.min(height, width) / 2 - 2; + const radius = Math.min(height, width) / 2 - 2 < 7 ? 7 : Math.min(height, width) / 2 - 2; const center = getCenter(feature.extent); const newPoint = new Point(center); const newFeature = new Feature(newPoint); From 80f184eed69f913746eae1da081a27f197198472 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 17 Jan 2025 12:49:56 -0500 Subject: [PATCH 09/10] finish details --- .../public/configs/performance.json | 4 + .../core/components/details/feature-info.tsx | 4 +- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 73 ++++++++++++------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/geoview-core/public/configs/performance.json b/packages/geoview-core/public/configs/performance.json index 6dd39d58cf3..d5ba9db4fa4 100644 --- a/packages/geoview-core/public/configs/performance.json +++ b/packages/geoview-core/public/configs/performance.json @@ -21,6 +21,10 @@ { "geoviewLayerType": "geoCore", "geoviewLayerId": "e2424b6c-db0c-4996-9bc0-2ca2e6714d71" + }, + { + "geoviewLayerType": "geoCore", + "geoviewLayerId": "c5c249c4-dea6-40a6-8fae-188a42030908" } ] }, diff --git a/packages/geoview-core/src/core/components/details/feature-info.tsx b/packages/geoview-core/src/core/components/details/feature-info.tsx index d2eee3b6890..c952f8af1b4 100644 --- a/packages/geoview-core/src/core/components/details/feature-info.tsx +++ b/packages/geoview-core/src/core/components/details/feature-info.tsx @@ -69,7 +69,7 @@ const FeatureHeader = memo(function FeatureHeader({ iconSrc, name, hasGeometry, - + @@ -186,7 +186,7 @@ export function FeatureInfo({ feature }: FeatureInfoProps): JSX.Element | null { []; const arrayOfFeatureInfoEntries = await this.formatFeatureInfoResult(features, layerConfig); - this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) - .then((featuresJSON) => { - (featuresJSON.features as unknown as TypeFeatureInfoEntry[]).forEach((feat: TypeFeatureInfoEntry, index: number) => { - const geom = new EsriJSON().readFeature(feat, { - dataProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, - featureProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, - }) as Feature; - - if ( - arrayOfFeatureInfoEntries![index] && - arrayOfFeatureInfoEntries![index].geometry && - arrayOfFeatureInfoEntries![index].geometry instanceof Feature - ) { - arrayOfFeatureInfoEntries![index].extent = geom.getGeometry()?.getExtent(); - arrayOfFeatureInfoEntries![index].geometry.setGeometry(geom.getGeometry()); - } - }); - - // arrayOfFeatureInfoEntries!.forEach((featureInfoEntry, i) => { - // const feature = features[i]; - // featureInfoEntry.geometry = feature.getGeometry(); - - // arrayOfFeatureInfoEntries[0].geometry.setGeometry(featuresJSON.features[0]) - // }); - logger.logDebug('Features worker', featuresJSON); - }) - .catch((err) => logger.logError('Features worker', err)); + // If geometry is needed, use web worker to query and assing geometry later + if (queryGeometry) + // TODO: Performance - We may need to use chunk and process 50 geom at a time. When we query 500 features (points) we have CORS issue with + // TODO.CONT: the esri query (was working with identify). But identify was failing on huge geometry... + this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) + .then((featuresJSON) => { + (featuresJSON.features as TypeJsonObject[]).forEach((feat: TypeJsonObject, index: number) => { + // TODO: Performance - There is still a problem when we create the feature with new EsriJSON().readFeature. It goes trought a loop and take minutes on the deflate function + // TODO.CONT: 1dcd28aa-99da-4f62-b157-15631379b170, ocean biology layer has huge amount of verticies and when zoomed in we require more + // TODO.CONT: more definition so the feature creation take more time. Investigate if we can create the geometry instead + // TODO.CONT: Investigate using this approach in esri-feature.ts + // const geom = new EsriJSON().readFeature(feat, { + // dataProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, + // featureProjection: `EPSG:${mapViewer.getMapState().currentProjection}`, + // }) as Feature; + + // TODO: Performance - Relying on style to get geometry is not good. We shold extract it from metadata and keep it in dedicated attribute + const geomType = Object.keys(layerConfig?.layerStyle || []); + + // Get coordinates in right format and create geometry + const coordinates = (feat.geometry?.points || + feat.geometry?.paths || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + feat.geometry?.rings || [feat.geometry?.x, feat.geometry?.y]) as any; // MultiPoint or Line or Polygon or Point schema + const newGeom: Geometry | null = + geomType.length > 0 + ? (GeometryApi.createGeometryFromType(geomType[0] as TypeStyleGeometry, coordinates) as unknown as Geometry) + : null; + + // TODO: Perfromance - We will need a trigger to refresh the higight and detaiils panel (for zoom button) when extent and + // TODO.CONT: is applied. Sometime the delay is too big so we need to change tab or layer in layer list to trigger the refresh + // We assume order of arrayOfFeatureInfoEntries is the same as featuresJSON.features as they are process in same order + const entry = arrayOfFeatureInfoEntries![index]; + if (newGeom !== null && entry.geometry && entry.geometry instanceof Feature) { + entry.extent = newGeom.getExtent(); + entry.geometry.setGeometry(newGeom); + } + }); + }) + .catch((err) => logger.logError('Features worker', err)); return arrayOfFeatureInfoEntries; } catch (error) { From 452aa5325810c92eafe30d8f1f777d342c508939 Mon Sep 17 00:00:00 2001 From: Johann Levesque Date: Fri, 17 Jan 2025 15:53:03 -0500 Subject: [PATCH 10/10] first review comment --- .../core/workers/fetch-esri-worker-script.ts | 5 ++-- .../src/core/workers/fetch-esri-worker.ts | 10 ++++---- .../geo/layer/gv-layers/abstract-gv-layer.ts | 10 ++++---- .../layer/gv-layers/raster/gv-esri-dynamic.ts | 23 ++++++++++++------ .../geoview-core/src/geo/utils/utilities.ts | 24 ++++--------------- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts index a69bd165048..b229e5db2dd 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker-script.ts @@ -68,15 +68,14 @@ const worker = { /** * Processes an ESRI query request - * @async * @param {QueryParams} params - The parameters for the ESRI query * @returns {Promise} A promise that resolves to the query results * @throws {Error} When the query processing fails */ - async process(params: QueryParams): Promise { + process(params: QueryParams): Promise { try { logger.logTrace('Starting query processing', JSON.stringify(params)); - const response = await queryEsriFeatures(params); + const response = queryEsriFeatures(params); logger.logTrace('Query completed'); return response; } catch (error) { diff --git a/packages/geoview-core/src/core/workers/fetch-esri-worker.ts b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts index e6db1f3a642..309a07c1c75 100644 --- a/packages/geoview-core/src/core/workers/fetch-esri-worker.ts +++ b/packages/geoview-core/src/core/workers/fetch-esri-worker.ts @@ -48,9 +48,8 @@ export class FetchEsriWorker extends AbstractWorker { * Initializes the worker - empty for now. * @returns A promise that resolves when initialization is complete. */ - public async init(): Promise { - const result = await this.proxy.init(); - return result; + public init(): Promise { + return this.proxy.init(); } /** @@ -58,8 +57,7 @@ export class FetchEsriWorker extends AbstractWorker { * @param {QueryParams} queryParams - The query parameters for the fetch. * @returns A promise that resolves to the processed JSON string. */ - public async process(queryParams: QueryParams): Promise { - const result = await this.proxy.process(queryParams); - return result; + public process(queryParams: QueryParams): Promise { + return this.proxy.process(queryParams); } } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts index b42db135a50..a70e300f316 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/abstract-gv-layer.ts @@ -320,7 +320,7 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtPixel(location: Pixel, queryGeometry = true): Promise { + protected getFeatureInfoAtPixel(location: Pixel, queryGeometry: boolean): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtPixel on layer path ${this.getLayerPath()}`); } @@ -332,7 +332,7 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtCoordinate(location: Coordinate, queryGeometry = true): Promise { + protected getFeatureInfoAtCoordinate(location: Coordinate, queryGeometry: boolean): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtCoordinate on layer path ${this.getLayerPath()}`); } @@ -344,7 +344,7 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoAtLongLat(location: Coordinate, queryGeometry = true): Promise { + protected getFeatureInfoAtLongLat(location: Coordinate, queryGeometry: boolean): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoAtLongLat on layer path ${this.getLayerPath()}`); } @@ -356,7 +356,7 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoUsingBBox(location: Coordinate[], queryGeometry = true): Promise { + protected getFeatureInfoUsingBBox(location: Coordinate[], queryGeometry: boolean): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoUsingBBox on layer path ${this.getLayerPath()}`); } @@ -368,7 +368,7 @@ export abstract class AbstractGVLayer extends AbstractBaseLayer { * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected getFeatureInfoUsingPolygon(location: Coordinate[], queryGeometry = true): Promise { + protected getFeatureInfoUsingPolygon(location: Coordinate[], queryGeometry: boolean): Promise { // Crash on purpose throw new Error(`Not implemented exception for getFeatureInfoUsingPolygon on layer path ${this.getLayerPath()}`); } diff --git a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts index 36cad6b2f6a..05e9df9180d 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/raster/gv-esri-dynamic.ts @@ -24,7 +24,7 @@ import { } from '@/geo/map/map-schema-types'; import { esriGetFieldType, esriGetFieldDomain } from '../utils'; import { AbstractGVRaster } from './abstract-gv-raster'; -import { TypeOutfieldsType, TypeStyleGeometry } from '@/api/config/types/map-schema-types'; +import { TypeOutfieldsType, TypeStyleGeometry, TypeValidMapProjectionCodes } from '@/api/config/types/map-schema-types'; import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { CONST_LAYER_TYPES } from '../../geoview-layers/abstract-geoview-layers'; import { TypeLegend } from '@/core/stores/store-interface-and-intial-values/layer-state'; @@ -256,7 +256,10 @@ export class GVEsriDynamic extends AbstractGVRaster { * @param {boolean} queryGeometry - The query geometry boolean. * @returns {Promise} A promise of an array of TypeFeatureInfoEntry[]. */ - protected override getFeatureInfoAtPixel(location: Pixel, queryGeometry = true): Promise { + protected override getFeatureInfoAtPixel( + location: Pixel, + queryGeometry: boolean = true + ): Promise { // Redirect to getFeatureInfoAtCoordinate return this.getFeatureInfoAtCoordinate(this.getMapViewer().map.getCoordinateFromPixel(location), queryGeometry); } @@ -269,7 +272,7 @@ export class GVEsriDynamic extends AbstractGVRaster { */ protected override getFeatureInfoAtCoordinate( location: Coordinate, - queryGeometry = true + queryGeometry: boolean = true ): Promise { // Transform coordinate from map project to lntlat const projCoordinate = this.getMapViewer().convertCoordinateMapProjToLngLat(location); @@ -287,7 +290,7 @@ export class GVEsriDynamic extends AbstractGVRaster { * @param {number} maxAllowableOffset - The maximum allowable offset for geometry simplification * @returns {TypeJsonObject} A promise of esri response for query. */ - async getFeatureInfoGeometryWorker( + async fetchFeatureInfoGeometryWithWorker( layerConfig: EsriDynamicLayerEntryConfig, objectIds: number[], queryGeometry: boolean, @@ -319,7 +322,7 @@ export class GVEsriDynamic extends AbstractGVRaster { */ protected override async getFeatureInfoAtLongLat( lnglat: Coordinate, - queryGeometry = true + queryGeometry: boolean = true ): Promise { try { // If invisible @@ -373,7 +376,13 @@ export class GVEsriDynamic extends AbstractGVRaster { const objectIds = identifyJsonResponse.results.map((result: TypeJsonObject) => String(result.attributes[oidField]).replace(',', '')); // Get meters per pixel to set the maxAllowableOffset to simplify return geometry - const maxAllowableOffset = queryGeometry ? getMetersPerPixel(mapViewer, lnglat[1]) : 0; + const maxAllowableOffset = queryGeometry + ? getMetersPerPixel( + mapViewer.getMapState().currentProjection as TypeValidMapProjectionCodes, + mapViewer.getView().getResolution() || 7000, + lnglat[1] + ) + : 0; // TODO: Performance - We need to separate the query attribute from geometry. We can use the attributes returned by identify to show details panel // TODO.CONT: or create 2 distinc query one for attributes and one for geometry. This way we can display the panel faster and wait later for geometry @@ -404,7 +413,7 @@ export class GVEsriDynamic extends AbstractGVRaster { if (queryGeometry) // TODO: Performance - We may need to use chunk and process 50 geom at a time. When we query 500 features (points) we have CORS issue with // TODO.CONT: the esri query (was working with identify). But identify was failing on huge geometry... - this.getFeatureInfoGeometryWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) + this.fetchFeatureInfoGeometryWithWorker(layerConfig, objectIds, true, mapViewer.getMapState().currentProjection, maxAllowableOffset) .then((featuresJSON) => { (featuresJSON.features as TypeJsonObject[]).forEach((feat: TypeJsonObject, index: number) => { // TODO: Performance - There is still a problem when we create the feature with new EsriJSON().readFeature. It goes trought a loop and take minutes on the deflate function diff --git a/packages/geoview-core/src/geo/utils/utilities.ts b/packages/geoview-core/src/geo/utils/utilities.ts index be49e25399c..0482e2926d6 100644 --- a/packages/geoview-core/src/geo/utils/utilities.ts +++ b/packages/geoview-core/src/geo/utils/utilities.ts @@ -22,7 +22,7 @@ import { getLegendStyles } from '@/geo/utils/renderer/geoview-renderer'; import { TypeLayerStyleConfig } from '@/geo/map/map-schema-types'; import { TypeBasemapLayer } from '../layer/basemap/basemap-types'; -import { MapViewer } from '@/app'; +import { TypeValidMapProjectionCodes } from '@/api/config/types/map-schema-types'; /** * Interface used for css style declarations @@ -481,31 +481,17 @@ export function calculateDistance(coordinates: Coordinate[], inProj: string, out /** * Get meters per pixel for different projections - * @param {MapViewer} map - The Geoview map viewer instance + * @param {TypeValidMapProjectionCodes} projection - The projection of the map + * @param {number} resolution - The resolution of the map * @param {number?} lat - The latitude, only needed for Web Mercator * @returns {nubmber} Number representing meters per pixel */ -export function getMetersPerPixel(map: MapViewer, lat?: number): number { - const view = map.getView(); - const projection = view.getProjection().getCode(); - const resolution = view.getResolution(); - +export function getMetersPerPixel(projection: TypeValidMapProjectionCodes, resolution: number, lat?: number): number { if (!resolution) return 0; // Web Mercator needs latitude correction because of severe distortion at high latitudes // At latitude 60°N, the scale distortion factor is about 2:1 - if (projection === 'EPSG:3857') { - if (lat === undefined) { - // Get center of current view if latitude not provided - const center = view.getCenter(); - if (center) { - // Transform center point to get latitude - const [, latitude] = Projection.transform(center, projection, Projection.PROJECTION_NAMES.LNGLAT); - const latitudeCorrection = Math.cos((latitude * Math.PI) / 180); - return resolution * latitudeCorrection; - } - return resolution; - } + if (projection === 3857 && lat !== undefined) { const latitudeCorrection = Math.cos((lat * Math.PI) / 180); return resolution * latitudeCorrection; }