Skip to content

Commit

Permalink
fix(data-table) DataTable now attempts to export the geometries for t…
Browse files Browse the repository at this point in the history
…he EsriDynamic layers (Canadian-Geospatial-Platform#2579)

* DataTable now attempts to export the geometries for the EsriDynamic layers

* Added support for MultiPolygon
  • Loading branch information
Alex-NRCan authored Nov 6, 2024
1 parent 0768201 commit a03c902
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 142 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -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
Expand All @@ -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]) };
}

Expand All @@ -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<string, unknown>[] = [];
Object.keys(fieldInfo).forEach((key) => {
if (key !== 'geoviewID') {
const tmpObj: Record<string, unknown> = {};
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<string, unknown>[] = [];
Object.keys(fieldInfo).forEach((key) => {
if (key !== 'geoviewID') {
const tmpObj: Record<string, unknown> = {};
tmpObj[key] = fieldInfo[key]!.value;
formattedInfo.push(tmpObj);
const getJson = useCallback(
async (fetchGeometriesDuringProcess: boolean): Promise<string | undefined> => {
// 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<void>((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
Expand All @@ -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 <MenuItem onClick={handleExportData}>{t('dataTable.jsonExportBtn')}</MenuItem>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,6 +38,7 @@ export interface ILayerState {
actions: {
deleteLayer: (layerPath: string) => void;
getExtentFromFeatures: (layerPath: string, featureIds: string[]) => Promise<Extent | undefined>;
queryLayerEsriDynamic: (layerPath: string, objectIDs: number[]) => Promise<TypeFeatureInfoEntryPartial[]>;
getLayer: (layerPath: string) => TypeLegendLayer | undefined;
getLayerBounds: (layerPath: string) => number[] | undefined;
getLayerDeleteInProgress: () => boolean;
Expand Down Expand Up @@ -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<TypeFeatureInfoEntryPartial[]> => {
// 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.
Expand Down
Loading

0 comments on commit a03c902

Please sign in to comment.