Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(table): EsriDynamic does not have right icon after no fetch of ge… #2590

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@
"labeled": true
},
"listOfGeoviewLayerConfig": [
{
"geoviewLayerId": "472ef86d-7f7c-423b-a7d2-b6f92b79fd6d",
"geoviewLayerType": "geoCore"
},
{
"geoviewLayerId": "historical-flood",
"geoviewLayerName": "Historical Flood Events (HFE)",
"externalDateFormat": "mm/dd/yyyy hh:mm:ss-05:00",
"metadataAccessPath": "https://maps-cartes.services.geo.ca/server_serveur/rest/services/NRCan/historical_flood_event_en/MapServer",
"geoviewLayerType": "esriDynamic",
"listOfLayerEntryConfig": [
{
"layerId": "0"
}
]
},
{
"geoviewLayerId": "uniqueValueId",
"geoviewLayerName": "uniqueValue",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import {
import { ConfigBaseClass } from '@/core/utils/config/validation-classes/config-base-class';
import { ILayerState, TypeLegend, TypeLegendResultSetEntry } from '@/core/stores/store-interface-and-intial-values/layer-state';
import { AbstractEventProcessor } from '@/api/event-processors/abstract-event-processor';

import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config';
import {
TypeClassBreakStyleConfig,
TypeClassBreakStyleInfo,
TypeFeatureInfoEntry,
TypeStyleGeometry,
TypeUniqueValueStyleConfig,
TypeUniqueValueStyleInfo,
isClassBreakStyleConfig,
isSimpleStyleConfig,
isUniqueValueStyleConfig,
Expand Down Expand Up @@ -588,4 +593,167 @@ export class LegendEventProcessor extends AbstractEventProcessor {
// Set updated legend layers
this.getLayerState(mapId).setterActions.setLegendLayers(curLayers);
}

/**
* Filters features based on their visibility settings defined in the layer's unique value or class break style configuration.
*
* @static
* @param {string} mapId - The unique identifier of the map instance
* @param {string} layerPath - The path to the layer in the map configuration
* @param {any[]} features - Array of features to filter
*
* @returns {any[]} Filtered array of features based on their visibility settings
*
* @description
* This function processes features based on the layer's unique value style configuration:
* - If the layer doesn't use unique value or class break styling, returns all features unchanged
* - Features matching visible styles are included
* - Features matching invisible styles are excluded
* - Features with no matching style follow the defaultVisible setting
*/
static getFeatureVisibleFromClassVibility(mapId: string, layerPath: string, features: TypeFeatureInfoEntry[]): TypeFeatureInfoEntry[] {
// Get the layer config and geometry type
const layerConfig = MapEventProcessor.getMapViewerLayerAPI(mapId).getLayerEntryConfig(layerPath) as AbstractBaseLayerEntryConfig;
const [geometryType] = layerConfig.getTypeGeometries();

// Get the style
const layerStyle = layerConfig.style?.[geometryType];
let filteredFeatures = features;
if (layerStyle !== undefined && layerStyle!.styleType === 'uniqueValue') {
filteredFeatures = this.#processClassVisibilityUniqueValue(layerStyle as TypeUniqueValueStyleConfig, features);
} else if (layerStyle !== undefined && layerStyle!.styleType === 'classBreaks') {
filteredFeatures = this.#processClassVisibilityClassBreak(layerStyle as TypeClassBreakStyleConfig, features);
}

return filteredFeatures!;
}

/**
* Processes features based on unique value style configuration to determine their visibility.
*
* @param {TypeUniqueValueStyleConfig} uniqueValueStyle - The unique value style configuration
* @param {TypeFeatureInfoEntry[]} features - Array of features to process
* @returns {TypeFeatureInfoEntry[]} Filtered array of features based on visibility rules
*
* @description
* This function filters features based on their field values and the unique value style configuration:
* - Creates sets of visible and invisible values for efficient lookup
* - Combines multiple field values using semicolon separator
* - Determines feature visibility based on:
* - Explicit visibility rules in the style configuration
* - Default visibility for values not matching any style rule
*
* @static
* @private
*/
static #processClassVisibilityUniqueValue(
uniqueValueStyle: TypeUniqueValueStyleConfig,
features: TypeFeatureInfoEntry[]
): TypeFeatureInfoEntry[] {
const styleUnique = uniqueValueStyle.uniqueValueStyleInfo as TypeUniqueValueStyleInfo[];

// Create sets for visible and invisible values for faster lookup
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(';')));

// Filter features based on visibility
return features.filter((feature) => {
const fieldValues = uniqueValueStyle.fields.map((field) => feature.fieldInfo[field]!.value).join(';');

return visibleValues.has(fieldValues.toString()) || (uniqueValueStyle.defaultVisible && !unvisibleValues.has(fieldValues.toString()));
});
}

/**
* Processes features based on class break style configuration to determine their visibility.
*
* @private
*
* @param {TypeClassBreakStyleConfig} classBreakStyle - The class break style configuration
* @param {TypeFeatureInfoEntry[]} features - Array of features to process
* @returns {TypeFeatureInfoEntry[]} Filtered array of features based on class break visibility rules
*
* @description
* This function filters features based on numeric values falling within defined class breaks:
* - Sorts class breaks by minimum value for efficient binary search
* - Creates optimized lookup structure for break points
* - Uses binary search to find the appropriate class break for each feature
* - Determines feature visibility based on:
* - Whether the feature's value falls within a class break range
* - The visibility setting of the matching class break
* - Default visibility for values not matching any class break
*
* @static
* @private
*/
static #processClassVisibilityClassBreak(
classBreakStyle: TypeClassBreakStyleConfig,
features: TypeFeatureInfoEntry[]
): TypeFeatureInfoEntry[] {
const classBreaks = classBreakStyle.classBreakStyleInfo as TypeClassBreakStyleInfo[];

// Sort class breaks by minValue for binary search
// GV: Values can be number, date, string, null or undefined. Should it be only Date or Number
// GV: undefined or null should not be allowed in class break style
const sortedBreaks = [...classBreaks].sort((a, b) => (a.minValue as number) - (b.minValue as number));

// Create an optimized lookup structure
interface ClassBreakPoint {
minValue: number;
maxValue: number;
visible: boolean;
}
const breakPoints = sortedBreaks.map(
(brk): ClassBreakPoint => ({
minValue: brk.minValue as number,
maxValue: brk.maxValue as number,
visible: brk.visible as boolean,
})
);

// Binary search function to find the appropriate class break
const findClassBreak = (value: number): ClassBreakPoint | null => {
let left = 0;
let right = breakPoints.length - 1;

// Binary search through sorted break points to find matching class break
while (left <= right) {
// Calculate middle index to divide search space
const mid = Math.floor((left + right) / 2);
const breakPoint = breakPoints[mid];

// Check if value falls within current break point's range
if (value >= breakPoint!.minValue && value <= breakPoint!.maxValue) {
// Found matching break point, return it
return breakPoint;
}

// If value is less than current break point's minimum,
// search in lower half of remaining range
if (value < breakPoint.minValue) {
right = mid - 1;
} else {
// If value is greater than current break point's maximum,
// search in upper half of remaining range
left = mid + 1;
}
}

return null;
};

// Filter features using binary search
return features.filter((feature) => {
const val = feature.fieldInfo[String(classBreakStyle.field)]?.value;
const fieldValue = val != null ? parseFloat(String(val)) : 0;

// eslint-disable-next-line no-restricted-globals
if (isNaN(fieldValue)) {
return classBreakStyle.defaultVisible;
}

const matchingBreak = findClassBreak(fieldValue);
return matchingBreak ? matchingBreak.visible : classBreakStyle.defaultVisible;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps):
// get store actions and values
const { zoomToExtent, highlightBBox, transformPoints, showClickMarker, addHighlightedFeature, removeHighlightedFeature } =
useMapStoreActions();
const { applyMapFilters, setSelectedFeature, setColumnsFiltersVisibility } = useDataTableStoreActions();
const { applyMapFilters, setSelectedFeature, setColumnsFiltersVisibility, getFilteredDataFromLegendVisibility } =
useDataTableStoreActions();
const { getExtentFromFeatures } = useLayerStoreActions();
const language = useAppDisplayLanguage();
const datatableSettings = useDataTableLayerSettings();
Expand Down Expand Up @@ -346,7 +347,11 @@ function DataTable({ data, layerPath, tableHeight = '500px' }: DataTableProps):
const rows = useMemo(() => {
// Log
logger.logTraceUseMemo('DATA-TABLE - rows', data.features);
return (data?.features ?? []).map((feature) => {

// get filtered feature for unique value info style so non visible class is not in the table
const filterArray = getFilteredDataFromLegendVisibility(data.layerPath, data?.features ?? []);

return (filterArray ?? []).map((feature) => {
const featureInfo = {
ICON: (
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useLayerStoreActions } from '@/core/stores/store-interface-and-intial-v
import { TypeJsonObject } from '@/core/types/global-types';
import { useAppStoreActions } from '@/core/stores/store-interface-and-intial-values/app-state';
import { useMapProjection } from '@/core/stores/store-interface-and-intial-values/map-state';
import { GeometryApi } from '@/geo/layer/geometry/geometry';

interface JSONExportButtonProps {
rows: unknown[];
Expand Down Expand Up @@ -54,7 +55,7 @@ function JSONExportButton({ rows, features, layerPath }: JSONExportButtonProps):
builtGeometry = { type: 'MultiLineString', coordinates: geometry.getCoordinates() };
} else if (geometry instanceof Point) {
// TODO: There is no proper support for esriDynamic MultiPoint issue 2589... this is a workaround
if (Array.isArray(geometry.getCoordinates())) {
if (GeometryApi.isArrayOfCoordinates(geometry.getCoordinates())) {
builtGeometry = { type: 'MultiPoint', coordinates: geometry.getCoordinates() };
} else {
builtGeometry = { type: 'Point', coordinates: geometry.getCoordinates() };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DataTableEventProcessor } from '@/api/event-processors/event-processor-
import { TypeSetStore, TypeGetStore } from '@/core/stores/geoview-store';
import { useGeoViewStore } from '@/core/stores/stores-managers';
import { TypeFeatureInfoEntry, TypeLayerData, TypeResultSet, TypeResultSetEntry } from '@/geo/map/map-schema-types';
import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor';

// GV Important: See notes in header of MapEventProcessor file for information on the paradigm to apply when working with DataTableEventProcessor vs DataTaleState

Expand All @@ -22,6 +23,7 @@ export interface IDataTableState {
actions: {
addOrUpdateTableFilter(layerPath: string, filter: string): void;
applyMapFilters: (filterStrings: string) => void;
getFilteredDataFromLegendVisibility: (layerPath: string, features: TypeFeatureInfoEntry[]) => TypeFeatureInfoEntry[];
setActiveLayersData: (layers: TypeLayerData[]) => void;
setColumnFiltersEntry: (filtered: TypeColumnFiltersState, layerPath: string) => void;
setColumnsFiltersVisibility: (visible: boolean, layerPath: string) => void;
Expand Down Expand Up @@ -86,6 +88,9 @@ export function initialDataTableState(set: TypeSetStore, get: TypeGetStore): IDa
!!get()?.dataTableState?.layersDataTableSetting[layerPath]?.mapFilteredRecord
);
},
getFilteredDataFromLegendVisibility: (layerPath: string, features: TypeFeatureInfoEntry[]): TypeFeatureInfoEntry[] => {
return LegendEventProcessor.getFeatureVisibleFromClassVibility(get().mapId, layerPath, features);
},
setActiveLayersData: (activeLayerData: TypeLayerData[]) => {
// Redirect to setter
get().dataTableState.setterActions.setActiveLayersData(activeLayerData);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ import { generateId, getXMLHttpRequest, isJsonString, whenThisThen } from '@/cor
import { TypeJsonObject, toJsonObject } from '@/core/types/global-types';
import { TimeDimension, TypeDateFragments, DateMgt } from '@/core/utils/date-mgt';
import { logger } from '@/core/utils/logger';
import { AsyncSemaphore } from '@/core/utils/async-semaphore';
import { EsriDynamicLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/esri-dynamic-layer-entry-config';
import { OgcWmsLayerEntryConfig } from '@/core/utils/config/validation-classes/raster-validation-classes/ogc-wms-layer-entry-config';
import { VectorLayerEntryConfig } from '@/core/utils/config/validation-classes/vector-layer-entry-config';
import { AbstractBaseLayerEntryConfig } from '@/core/utils/config/validation-classes/abstract-base-layer-entry-config';
import { GroupLayerEntryConfig } from '@/core/utils/config/validation-classes/group-layer-entry-config';
import EventHelper, { EventDelegateBase } from '@/api/events/event-helper';
import { LegendEventProcessor } from '@/api/event-processors/event-processor-children/legend-event-processor';
import { MapEventProcessor } from '@/api/event-processors/event-processor-children/map-event-processor';
import {
TypeGeoviewLayerConfig,
Expand Down Expand Up @@ -1271,45 +1269,16 @@ export abstract class AbstractGeoViewLayer {
try {
if (!features.length) return [];

// Will hold the generic icon to use in formatting
let genericLegendInfo: string | null | undefined;
// We only want 1 task to fetch the generic legend (when we have to)
const semaphore = new AsyncSemaphore(1);

// Will be executed when we have to use a default canvas for a particular feature
const callbackToFetchDataUrl = (): Promise<string | null> => {
// TODO: Fix - Don't take 'iconImage' below, it's always the same image...
// TO.DOCONT: Use this.style.fields and this.style.[Geom].fields and this.style.[Geom].uniqueValueStyleInfo with a combination of the 'featureNeedingItsCanvas' to determine the style image
// TO.DOCONT: Also, get rid of 'genericLegendInfo' and 'semaphore' variables once code is rewritten to use the 'featureNeedingItsCanvas'

// Make sure one task at a time in this
return semaphore.withLock(async () => {
// Only execute this once in the callback. After this, once the semaphore is unlocked, it's either a string or null for as long as we're formatting
if (genericLegendInfo === undefined) {
genericLegendInfo = null; // Turn it to null, we are actively trying to find something (not undefined anymore)
const legend = await this.queryLegend(layerConfig.layerPath);
const legendIcons = LegendEventProcessor.getLayerIconImage(legend);
if (legendIcons) genericLegendInfo = legendIcons![0].iconImage || null;
}
return genericLegendInfo;
});
};

const featureInfo = layerConfig?.source?.featureInfo;

// Loop on the features to build the array holding the promises for their canvas
const promisedAllCanvasFound: Promise<{ feature: Feature; canvas: HTMLCanvasElement }>[] = [];
features.forEach((featureNeedingItsCanvas) => {
promisedAllCanvasFound.push(
new Promise((resolveCanvas) => {
getFeatureCanvas(
featureNeedingItsCanvas,
this.getStyle(layerConfig.layerPath)!,
layerConfig.filterEquation,
layerConfig.legendFilterIsOff,
true,
callbackToFetchDataUrl
)
// GV: Call the function with layerConfig.legendFilterIsOff = true to force the feature to get is canvas
// GV: If we don't, it will create canvas only for visible elements and because tables are stored feature will never get its canvas
getFeatureCanvas(featureNeedingItsCanvas, this.getStyle(layerConfig.layerPath)!, layerConfig.filterEquation, true, true)
.then((canvas) => {
resolveCanvas({ feature: featureNeedingItsCanvas, canvas });
})
Expand Down
Loading
Loading