From a03c90294d3419ca5460928b01bc280ca1d2a96b Mon Sep 17 00:00:00 2001 From: Alex <94073946+Alex-NRCan@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:02:33 -0500 Subject: [PATCH] fix(data-table) DataTable now attempts to export the geometries for the EsriDynamic layers (#2579) * DataTable now attempts to export the geometries for the EsriDynamic layers * Added support for MultiPolygon --- .../feature-info-event-processor.ts | 3 +- .../core/components/data-table/data-table.tsx | 1 - .../data-table/json-export-button.tsx | 195 +++++++++++++----- .../layer-state.ts | 42 +++- .../src/geo/layer/geometry/geometry.ts | 98 ++++++++- .../layer/geoview-layers/esri-layer-common.ts | 87 ++------ .../src/geo/layer/gv-layers/utils.ts | 48 ++++- .../src/geo/map/feature-highlight.ts | 2 +- .../src/geo/map/map-schema-types.ts | 4 +- .../geo/utils/renderer/geoview-renderer.ts | 6 + 10 files changed, 344 insertions(+), 142 deletions(-) diff --git a/packages/geoview-core/src/api/event-processors/event-processor-children/feature-info-event-processor.ts b/packages/geoview-core/src/api/event-processors/event-processor-children/feature-info-event-processor.ts index 422fa53bbdf..c77e32514f8 100644 --- a/packages/geoview-core/src/api/event-processors/event-processor-children/feature-info-event-processor.ts +++ b/packages/geoview-core/src/api/event-processors/event-processor-children/feature-info-event-processor.ts @@ -87,7 +87,8 @@ export class FeatureInfoEventProcessor extends AbstractEventProcessor { if (resultSet[layerPath]) { resultSet[layerPath].features = []; this.propagateFeatureInfoToStore(mapId, 'click', resultSet[layerPath]).catch((err) => - logger.logError('Not able to reset resultSet', err, layerPath) + // Log + logger.logPromiseFailed('Not able to reset resultSet', err, layerPath) ); } 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 868d79cd040..2c5cecec445 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 @@ -298,7 +298,6 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps): async (feature: TypeFeatureInfoEntry) => { let { extent } = feature; - // TODO This will require updating after the query optimization // If there is no extent, the layer is ESRI Dynamic, get the feature extent using its OBJECTID if (!extent) extent = await getExtentFromFeatures(layerPath, [feature.fieldInfo.OBJECTID!.value as string]); 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 f0fdf4700b9..1353b058ba8 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 @@ -1,13 +1,15 @@ import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import _ from 'lodash'; -import { Geometry, Point, Polygon, LineString, MultiPoint } from 'ol/geom'; +import { Geometry, Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon } from 'ol/geom'; import { MenuItem } from '@/ui'; +import { logger } from '@/core/utils/logger'; import { useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state'; -import { TypeFeatureInfoEntry } from '@/geo/map/map-schema-types'; import { TypeJsonObject } from '@/core/types/global-types'; import { useLayerStoreActions } from '@/core/stores/store-interface-and-intial-values/layer-state'; +import { TypeFeatureInfoEntry } from '@/geo/map/map-schema-types'; interface JSONExportButtonProps { rows: unknown[]; @@ -27,7 +29,7 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): // get store value - projection config to transfer lat long and layer const { transformPoints } = useMapStoreActions(); - const { getLayer } = useLayerStoreActions(); + const { getLayer, queryLayerEsriDynamic } = useLayerStoreActions(); /** * Creates a geometry json @@ -39,17 +41,39 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): let builtGeometry = {}; if (geometry instanceof Polygon) { + // coordinates are in the form of Coordinate[][] builtGeometry = { type: 'Polygon', coordinates: geometry.getCoordinates().map((coords) => { return coords.map((coord) => transformPoints([coord], 4326)[0]); }), }; + } else if (geometry instanceof MultiPolygon) { + // coordinates are in the form of Coordinate[][][] + builtGeometry = { + type: 'MultiPolygon', + coordinates: geometry.getCoordinates().map((coords1) => { + return coords1.map((coords2) => { + return coords2.map((coord) => transformPoints([coord], 4326)[0]); + }); + }), + }; } else if (geometry instanceof LineString) { + // coordinates are in the form of Coordinate[] builtGeometry = { type: 'LineString', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; + } else if (geometry instanceof MultiLineString) { + // coordinates are in the form of Coordinate[][] + builtGeometry = { + type: 'MultiLineString', + coordinates: geometry.getCoordinates().map((coords) => { + return coords.map((coord) => transformPoints([coord], 4326)[0]); + }), + }; } else if (geometry instanceof Point) { + // coordinates are in the form of Coordinate builtGeometry = { type: 'Point', coordinates: transformPoints([geometry.getCoordinates()], 4326)[0] }; } else if (geometry instanceof MultiPoint) { + // coordinates are in the form of Coordinate[] builtGeometry = { type: 'MultiPoint', coordinates: geometry.getCoordinates().map((coord) => transformPoints([coord], 4326)[0]) }; } @@ -58,53 +82,115 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): [transformPoints] ); + /** + * Builds the JSON features section of the file + * @returns {string} Json file content as string + */ + const getJsonFeatures = useCallback( + (theFeatures: TypeFeatureInfoEntry[]): TypeJsonObject[] => { + // Create GeoJSON feature + return theFeatures.map((feature) => { + const { geometry, fieldInfo } = feature; + + // Format the feature info to extract only value and remove the geoviewID field + const formattedInfo: Record[] = []; + Object.keys(fieldInfo).forEach((key) => { + if (key !== 'geoviewID') { + const tmpObj: Record = {}; + tmpObj[key] = fieldInfo[key]!.value; + formattedInfo.push(tmpObj); + } + }); + + return { + type: 'Feature', + geometry: buildGeometry(geometry?.getGeometry() as Geometry), + properties: formattedInfo, + } as unknown as TypeJsonObject; + }); + }, + [buildGeometry] + ); + /** * Builds the JSON file * @returns {string} Json file content as string */ - const getJson = useCallback((): string => { - // Filter features from filtered rows - const rowsID = rows.map((row) => { - if ( - typeof row === 'object' && - row !== null && - 'geoviewID' in row && - typeof row.geoviewID === 'object' && - row.geoviewID !== null && - 'value' in row.geoviewID - ) { - return row.geoviewID.value; - } - return ''; - }); - - const filteredFeatures = features.filter((feature) => rowsID.includes(feature.fieldInfo.geoviewID!.value)); - - // create GeoJSON feature - const geoData = filteredFeatures.map((feature) => { - const { geometry, fieldInfo } = feature; - - // Format the feature info to extract only value and remove the geoviewID field - const formattedInfo: Record[] = []; - Object.keys(fieldInfo).forEach((key) => { - if (key !== 'geoviewID') { - const tmpObj: Record = {}; - tmpObj[key] = fieldInfo[key]!.value; - formattedInfo.push(tmpObj); + const getJson = useCallback( + async (fetchGeometriesDuringProcess: boolean): Promise => { + // Filter features from filtered rows + const rowsID = rows.map((row) => { + if ( + typeof row === 'object' && + row !== null && + 'geoviewID' in row && + typeof row.geoviewID === 'object' && + row.geoviewID !== null && + 'value' in row.geoviewID + ) { + return row.geoviewID.value; } + return ''; }); - // TODO: fix issue with geometry not available for esriDynamic: https://github.com/Canadian-Geospatial-Platform/geoview/issues/2545 - return { - type: 'Feature', - geometry: buildGeometry(geometry?.getGeometry() as Geometry), - properties: formattedInfo, - }; - }); + const filteredFeatures = features.filter((feature) => rowsID.includes(feature.fieldInfo.geoviewID!.value)); + + // If must fetch the geometries during the process + if (fetchGeometriesDuringProcess) { + try { + // Split the array in arrays of 100 features maximum + const sublists = _.chunk(filteredFeatures, 100); + + // For each sub list + const promises = sublists.map((sublist) => { + // Create a new promise that will resolved when features have been updated with their geometries + return new Promise((resolve, reject) => { + // Get the ids + const objectids = sublist.map((record) => { + return record.geometry?.get('OBJECTID') as number; + }); + + // Query + queryLayerEsriDynamic(layerPath, objectids) + .then((results) => { + // For each result + results.forEach((result) => { + // Filter + const recFound = filteredFeatures.filter( + (record) => record.geometry?.get('OBJECTID') === result.fieldInfo?.OBJECTID?.value + ); + + // If found it + if (recFound && recFound.length === 1) { + // Officially attribute the geometry to that particular record + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (recFound[0].geometry as any).setGeometry(result.geometry); + } + }); + + // Only now, resolve the promise + resolve(); + }) + .catch(reject); + }); + }); + + // Once all promises complete + await Promise.all(promises); + } catch (err) { + // Handle error + logger.logError('Failed to query the features to get their geometries. The output will not have the geometries.', err); + } + } - // Stringify with some indentation - return JSON.stringify({ type: 'FeatureCollection', features: geoData }, null, 2); - }, [buildGeometry, features, rows]); + // Get the Json Features + const geoData = getJsonFeatures(filteredFeatures); + + // Stringify with some indentation + return JSON.stringify({ type: 'FeatureCollection', features: geoData }, null, 2); + }, + [layerPath, features, rows, getJsonFeatures, queryLayerEsriDynamic] + ); /** * Exports the blob to a file @@ -127,12 +213,25 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps): * Exports data table in csv format. */ const handleExportData = useCallback((): void => { - const jsonString = getJson(); - const blob = new Blob([jsonString], { - type: 'text/json', - }); - - exportBlob(blob, `table-${getLayer(layerPath)?.layerName.replaceAll(' ', '-')}.json`); + const layer = getLayer(layerPath); + const layerIsEsriDynamic = layer?.type === 'esriDynamic'; + + // Get the Json content for the layer + getJson(layerIsEsriDynamic) + .then((jsonString: string | undefined) => { + // If defined + if (jsonString) { + const blob = new Blob([jsonString], { + type: 'text/json', + }); + + exportBlob(blob, `table-${layer?.layerName.replaceAll(' ', '-')}.json`); + } + }) + .catch((err) => { + // Log + logger.logPromiseFailed('Not able to export', err); + }); }, [exportBlob, getJson, getLayer, layerPath]); return {t('dataTable.jsonExportBtn')}; 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 213ca15921f..0786c1122f2 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 @@ -8,11 +8,19 @@ import { Extent } from 'ol/extent'; import { useGeoViewStore } from '@/core/stores/stores-managers'; import { TypeLayersViewDisplayState, TypeLegendItem, TypeLegendLayer } from '@/core/components/layers/types'; import { TypeGetStore, TypeSetStore } from '@/core/stores/geoview-store'; -import { TypeResultSet, TypeResultSetEntry, TypeStyleConfig } from '@/geo/map/map-schema-types'; +import { + layerEntryIsEsriDynamic, + TypeFeatureInfoEntryPartial, + TypeResultSet, + TypeResultSetEntry, + TypeStyleConfig, +} from '@/geo/map/map-schema-types'; import { OL_ZOOM_DURATION, OL_ZOOM_PADDING } from '@/core/utils/constant'; +import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config'; import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor'; import { TypeGeoviewLayerType, TypeVectorLayerStyles } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor'; +import { esriQueryRecordsByUrlObjectIds } from '@/geo/layer/gv-layers/utils'; // #region INTERFACES & TYPES @@ -30,6 +38,7 @@ export interface ILayerState { actions: { deleteLayer: (layerPath: string) => void; getExtentFromFeatures: (layerPath: string, featureIds: string[]) => Promise; + queryLayerEsriDynamic: (layerPath: string, objectIDs: number[]) => Promise; getLayer: (layerPath: string) => TypeLegendLayer | undefined; getLayerBounds: (layerPath: string) => number[] | undefined; getLayerDeleteInProgress: () => boolean; @@ -85,6 +94,37 @@ export function initializeLayerState(set: TypeSetStore, get: TypeGetStore): ILay return LegendEventProcessor.getExtentFromFeatures(get().mapId, layerPath, featureIds); }, + /** + * Queries the EsriDynamic layer at the given layer path for a specific set of object ids + * @param {string} layerPath - The layer path of the layer to query + * @param {number[]} objectIDs - The object ids to filter the query on + * @returns A Promise of results of type TypeFeatureInfoEntryPartial + */ + queryLayerEsriDynamic: (layerPath: string, objectIDs: number[]): Promise => { + // Get the layer config + const layerConfig = MapEventProcessor.getMapViewerLayerAPI(get().mapId).getLayerEntryConfig( + layerPath + ) as AbstractBaseLayerEntryConfig; + + // Get the geometry type + const [geometryType] = layerConfig.getTypeGeometries(); + + // Check if EsriDynamic config + if (layerConfig && layerEntryIsEsriDynamic(layerConfig)) { + // Query for the specific object ids + return esriQueryRecordsByUrlObjectIds( + `${layerConfig.source?.dataAccessPath}${layerConfig.layerId}`, + geometryType, + objectIDs, + 'OBJECTID', + true + ); + } + + // Not an EsriDynamic layer + return Promise.reject(new Error('Not an EsriDynamic layer')); + }, + /** * Gets legend layer for given layer path. * @param {string} layerPath - The layer path to get info for. diff --git a/packages/geoview-core/src/geo/layer/geometry/geometry.ts b/packages/geoview-core/src/geo/layer/geometry/geometry.ts index 876d61941bb..17315c0f869 100644 --- a/packages/geoview-core/src/geo/layer/geometry/geometry.ts +++ b/packages/geoview-core/src/geo/layer/geometry/geometry.ts @@ -1,7 +1,7 @@ import VectorLayer from 'ol/layer/Vector'; import Feature from 'ol/Feature'; import VectorSource, { Options as VectorSourceOptions } from 'ol/source/Vector'; -import { Geometry as OLGeometry, Circle, LineString, Point, Polygon } from 'ol/geom'; +import { Geometry as OLGeometry, Circle, LineString, MultiLineString, Point, Polygon, MultiPolygon } from 'ol/geom'; import { Coordinate } from 'ol/coordinate'; import { Fill, Stroke, Style, Icon } from 'ol/style'; import { Options as VectorLayerOptions } from 'ol/layer/BaseVector'; @@ -655,32 +655,112 @@ export class GeometryApi { /** * Creates a Geometry given a geometry type and coordinates expected in any logical format. - * @param geometryType - The geometry type to create - * @param coordinates - The coordinates to use to create the geometry + * @param {TypeStyleGeometry} geometryType - The geometry type to create + * @param {Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]} coordinates - The coordinates to use to create the geometry * @returns The OpenLayers Geometry */ static createGeometryFromType( geometryType: TypeStyleGeometry, - coordinates: Coordinate | Coordinate[] | Coordinate[][] | number[] + coordinates: Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][] ): OLGeometry { switch (geometryType) { case 'Point': - // Create a point geometry + // Create a Point geometry return new Point(coordinates as Coordinate); case 'LineString': - // Create a line geometry - return new LineString(coordinates as Coordinate[] | number[]); + // If it's actually a MultiLineString + if (GeometryApi.isArrayOfArrayOfCoordinates(coordinates)) { + // Create a MultiLine geometry + return new MultiLineString(coordinates); + } + + // Create a Line geometry + return new LineString(coordinates as Coordinate[]); + + case 'MultiLineString': + // Create a MultiLine geometry + return new MultiLineString(coordinates as Coordinate[][]); case 'Polygon': - // Create a polygon geometry - return new Polygon(coordinates as Coordinate[][] | number[]); + // If it's actually a MultiPolygon + if (GeometryApi.isArrayOfArrayOfArrayOfCoordinates(coordinates)) { + // Create a MultiPolygon geometry + return new MultiPolygon(coordinates); + } + + // Create a Polygon geometry + return new Polygon(coordinates as Coordinate[][]); + + case 'MultiPolygon': + // Create a MultiPolygon geometry + return new MultiPolygon(coordinates as Coordinate[][][]); // Add support for other geometry types as needed default: throw new Error(`Unsupported geometry type: ${geometryType}`); } } + + /** + * Typeguards when a list of coordinates should actually be a single coordinate, such as a Point. + * @param {Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]} coordinates - The coordinates to check + * @returns {Coordinate} when the coordinates represent a Point + */ + static isCoordinates(coordinates: Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]): coordinates is Coordinate { + return Array.isArray(coordinates) && coordinates.length > 0 && !Array.isArray(coordinates[0]); + } + + /** + * Typeguards when a list of coordinates should actually be a single coordinate, such as a LineString. + * @param {Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]} coordinates - The coordinates to check + * @returns {Coordinate[]} when the coordinates represent a LineString + */ + static isArrayOfCoordinates(coordinates: Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]): coordinates is Coordinate[] { + return ( + Array.isArray(coordinates) && + coordinates.length > 0 && + Array.isArray(coordinates[0]) && + coordinates[0].length > 0 && + !Array.isArray(coordinates[0][0]) + ); + } + + /** + * Typeguards when a list of coordinates should actually be a single coordinate, such as a MultiLineString or Polygon. + * @param {Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]} coordinates - The coordinates to check + * @returns {Coordinate[][]} when the coordinates represent a MultiLineString or Polygon + */ + static isArrayOfArrayOfCoordinates( + coordinates: Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][] + ): coordinates is Coordinate[][] { + return ( + Array.isArray(coordinates) && + coordinates.length > 0 && + Array.isArray(coordinates[0]) && + coordinates[0].length > 0 && + Array.isArray(coordinates[0][0]) + ); + } + + /** + * Typeguards when a list of coordinates should actually be a single coordinate, such as a MultiPolygon. + * @param {Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][]} coordinates - The coordinates to check + * @returns {Coordinate[][][]} when the coordinates represent a MultiPolygon + */ + static isArrayOfArrayOfArrayOfCoordinates( + coordinates: Coordinate | Coordinate[] | Coordinate[][] | Coordinate[][][] + ): coordinates is Coordinate[][][] { + return ( + Array.isArray(coordinates) && + coordinates.length > 0 && + Array.isArray(coordinates[0]) && + coordinates[0].length > 0 && + Array.isArray(coordinates[0][0]) && + coordinates[0][0].length > 0 && + Array.isArray(coordinates[0][0][0]) + ); + } } /** 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 0b8659394e6..70f40fee326 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 @@ -19,7 +19,6 @@ import { GroupLayerEntryConfig } from '@/core/utils/config/validation-classes/gr import { CONST_LAYER_ENTRY_TYPES, TypeFeatureInfoEntryPartial, - TypeFieldEntry, TypeLayerEntryConfig, TypeStyleGeometry, codedValueType, @@ -27,6 +26,12 @@ import { rangeDomainType, } from '@/geo/map/map-schema-types'; import { CONST_LAYER_TYPES } from '@/geo/layer/geoview-layers/abstract-geoview-layers'; +import { + esriConvertEsriGeometryTypeToOLGeometryType, + esriParseFeatureInfoEntries, + esriQueryRecordsByUrl, + esriQueryRelatedRecordsByUrl, +} from '@/geo/layer/gv-layers/utils'; import { EsriDynamic, geoviewEntryIsEsriDynamic } from './raster/esri-dynamic'; import { EsriFeature, geoviewEntryIsEsriFeature } from './vector/esri-feature'; import { EsriBaseRenderer, getStyleFromEsriRenderer } from '@/geo/utils/renderer/esri-renderer'; @@ -407,21 +412,8 @@ export async function commonProcessLayerMetadata< * @returns TypeFeatureInfoEntryPartial[] an array of relared records of type TypeFeatureInfoEntryPartial */ export function parseFeatureInfoEntries(records: TypeJsonObject[]): TypeFeatureInfoEntryPartial[] { - // Loop on the Esri results - return records.map((rec: TypeJsonObject) => { - // Prep the TypeFeatureInfoEntryPartial - const featInfo: TypeFeatureInfoEntryPartial = { - fieldInfo: {}, - }; - - // Loop on the Esri attributes - Object.entries(rec.attributes).forEach((tupleAttrValue: [string, unknown]) => { - featInfo.fieldInfo[tupleAttrValue[0]] = { value: tupleAttrValue[1] } as TypeFieldEntry; - }); - - // Return the TypeFeatureInfoEntryPartial - return featInfo; - }); + // Redirect + return esriParseFeatureInfoEntries(records); } /** @@ -429,26 +421,9 @@ export function parseFeatureInfoEntries(records: TypeJsonObject[]): TypeFeatureI * @param {string} url An Esri url indicating a feature layer to query * @returns {TypeFeatureInfoEntryPartial[] | null} An array of relared records of type TypeFeatureInfoEntryPartial, or an empty array. */ -export async function queryRecordsByUrl(url: string): 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. - // Query the data - try { - const response = await fetch(url); - const respJson = await response.json(); - if (respJson.error) { - logger.logInfo('There is a problem with this query: ', url); - throw new Error(`Error code = ${respJson.error.code} ${respJson.error.message}` || ''); - } - - // Return the array of TypeFeatureInfoEntryPartial - return parseFeatureInfoEntries(respJson.features); - } catch (error) { - // Log - logger.logError('esri-layer-common.queryRecordsByUrl()\n', error); - throw error; - } +export function queryRecordsByUrl(url: string): Promise { + // Redirect + return esriQueryRecordsByUrl(url); } /** @@ -457,26 +432,9 @@ export async function queryRecordsByUrl(url: string): Promise { - // Query the data - try { - const response = await fetch(url); - const respJson = await response.json(); - if (respJson.error) { - logger.logInfo('There is a problem with this query: ', url); - throw new Error(`Error code = ${respJson.error.code} ${respJson.error.message}` || ''); - } - - // If any related record groups found - if (respJson.relatedRecordGroups.length > 0) - // Return the array of TypeFeatureInfoEntryPartial - return parseFeatureInfoEntries(respJson.relatedRecordGroups[recordGroupIndex].relatedRecords); - return Promise.resolve([]); - } catch (error) { - // Log - logger.logError('esri-layer-common.queryRelatedRecordsByUrl()\n', error); - throw error; - } +export function queryRelatedRecordsByUrl(url: string, recordGroupIndex: number): Promise { + // Redirect + return esriQueryRelatedRecordsByUrl(url, recordGroupIndex); } /** @@ -485,19 +443,6 @@ export async function queryRelatedRecordsByUrl(url: string, recordGroupIndex: nu * @returns {TypeStyleGeometry} The corresponding TypeStyleGeometry */ export function convertEsriGeometryTypeToOLGeometryType(esriGeometryType: string): TypeStyleGeometry { - switch (esriGeometryType) { - case 'esriGeometryPoint': - case 'esriGeometryMultipoint': - return 'Point'; - - case 'esriGeometryPolyline': - return 'LineString'; - - case 'esriGeometryPolygon': - case 'esriGeometryMultiPolygon': - return 'Polygon'; - - default: - throw new Error(`Unsupported geometry type: ${esriGeometryType}`); - } + // Redirect + return esriConvertEsriGeometryTypeToOLGeometryType(esriGeometryType); } 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 be05fb60ebf..5694d172df8 100644 --- a/packages/geoview-core/src/geo/layer/gv-layers/utils.ts +++ b/packages/geoview-core/src/geo/layer/gv-layers/utils.ts @@ -7,12 +7,14 @@ import { rangeDomainType, TypeFieldEntry, TypeFeatureInfoLayerConfig, + TypeGeometry, } from '@/geo/map/map-schema-types'; import { TypeOutfieldsType } from '@/api/config/types/map-schema-types'; import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config'; import { EsriDynamicLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-dynamic-layer-entry-config'; 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'; /** * Returns the type of the specified field. @@ -76,12 +78,17 @@ export function esriGetFieldDomain( * * @returns TypeFeatureInfoEntryPartial[] an array of relared records of type TypeFeatureInfoEntryPartial */ -export function esriParseFeatureInfoEntries(records: TypeJsonObject[]): TypeFeatureInfoEntryPartial[] { +export function esriParseFeatureInfoEntries(records: TypeJsonObject[], geometryType?: TypeStyleGeometry): TypeFeatureInfoEntryPartial[] { // Loop on the Esri results return records.map((rec: TypeJsonObject) => { + // The coordinates + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const coordinates = (rec.geometry?.points || rec.geometry?.paths || rec.geometry?.rings || [rec.geometry?.x, rec.geometry?.y]) as any; // MultiPoint or Line or Polygon or Point schema + // Prep the TypeFeatureInfoEntryPartial const featInfo: TypeFeatureInfoEntryPartial = { fieldInfo: {}, + geometry: geometryType ? (GeometryApi.createGeometryFromType(geometryType, coordinates) as unknown as TypeGeometry) : null, }; // Loop on the Esri attributes @@ -96,10 +103,11 @@ export function esriParseFeatureInfoEntries(records: TypeJsonObject[]): TypeFeat /** * Asynchronously queries an Esri feature layer given the url and returns an array of `TypeFeatureInfoEntryPartial` records. - * @param {url} string An Esri url indicating a feature layer to query + * @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) * @returns {TypeFeatureInfoEntryPartial[] | null} An array of relared records of type TypeFeatureInfoEntryPartial, or an empty array. */ -export async function esriQueryRecordsByUrl(url: string): Promise { +export async function esriQueryRecordsByUrl(url: string, geometryType?: TypeStyleGeometry): 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. @@ -108,19 +116,43 @@ export async function esriQueryRecordsByUrl(url: string): Promise { + // Query + const oids = objectIds.join(','); + const url = `${layerUrl}/query?where=&objectIds=${oids}&outFields=${fields}&returnGeometry=${geometry}&f=json`; + + // Redirect + return esriQueryRecordsByUrl(url, geometryType); +} + /** * Asynchronously queries an Esri relationship table given the url and returns an array of `TypeFeatureInfoEntryPartial` records. * @param {url} string An Esri url indicating a relationship table to query @@ -133,7 +165,7 @@ export async function esriQueryRelatedRecordsByUrl(url: string, recordGroupIndex const response = await fetch(url); const respJson = await response.json(); if (respJson.error) { - logger.logInfo('There is a problem with this query: ', url); + // Throw throw new Error(`Error code = ${respJson.error.code} ${respJson.error.message}` || ''); } @@ -144,7 +176,7 @@ export async function esriQueryRelatedRecordsByUrl(url: string, recordGroupIndex return Promise.resolve([]); } catch (error) { // Log - logger.logError('esri-layer-common.queryRelatedRecordsByUrl()\n', error); + logger.logError('There is a problem with this query: ', url, error); throw error; } } diff --git a/packages/geoview-core/src/geo/map/feature-highlight.ts b/packages/geoview-core/src/geo/map/feature-highlight.ts index 5fdd47f4945..367e38faa6f 100644 --- a/packages/geoview-core/src/geo/map/feature-highlight.ts +++ b/packages/geoview-core/src/geo/map/feature-highlight.ts @@ -3,7 +3,7 @@ import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'; import Feature from 'ol/Feature'; -import { LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon } from 'ol/geom'; +import { Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon } from 'ol/geom'; import { Extent, getCenter } from 'ol/extent'; import { fromExtent } from 'ol/geom/Polygon'; import { Coordinate } from 'ol/coordinate'; diff --git a/packages/geoview-core/src/geo/map/map-schema-types.ts b/packages/geoview-core/src/geo/map/map-schema-types.ts index 4f646b0292b..9d0235c3e46 100644 --- a/packages/geoview-core/src/geo/map/map-schema-types.ts +++ b/packages/geoview-core/src/geo/map/map-schema-types.ts @@ -267,7 +267,7 @@ export type TypeFieldEntry = { * Purposely linking this simpler type to the main TypeFeatureInfoEntry type here, in case, for future we want * to add more information on one or the other and keep things loosely linked together. */ -export type TypeFeatureInfoEntryPartial = Pick; +export type TypeFeatureInfoEntryPartial = Pick; /** The simplified layer statuses */ export type TypeLayerStatusSimplified = 'loading' | 'loaded' | 'error'; @@ -828,7 +828,7 @@ export type TypeStyleSettings = TypeBaseStyleConfig | TypeSimpleStyleConfig | Ty * Valid keys for the TypeStyleConfig object. */ // TODO: Refactor - Layers/Config refactoring. The values here have been renamed to lower case, make sure to lower here and adjust everywhere as part of config migration. -export type TypeStyleGeometry = 'Point' | 'LineString' | 'Polygon'; +export type TypeStyleGeometry = 'Point' | 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon'; /** ****************************************************************************************************************************** * Type of Style to apply to the GeoView vector layer based on geometry types. 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 9e97229840e..c3fcd8e2c2a 100644 --- a/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts +++ b/packages/geoview-core/src/geo/utils/renderer/geoview-renderer.ts @@ -1507,17 +1507,23 @@ const processStyle: Record