From db9640421874dcd63fa73f8f96aac451d0877abc Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 21 Jul 2026 23:03:58 +0200 Subject: [PATCH] fix(map): bound overview feature payloads --- docs/CODEX_EXECUTION_LOG.md | 4 ++++ frontend/src/components/map/MapWorkspace.tsx | 4 ++++ frontend/src/components/map/mapWorkspaceUtils.test.ts | 3 +++ frontend/src/components/map/mapWorkspaceUtils.ts | 7 +++++++ frontend/src/hooks/useMapThemeSelectionInsights.ts | 10 ++++++---- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 2aa19774..e8e9f0d9 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -10999,6 +10999,10 @@ rasters could still enter a Belgium-scale analysis. The shared selection guard now applies the same real pixel budgets to persisted rasters: country-scale overview analysis keeps PostGIS vector and national statistical sources, while high-resolution rasters require detail or safely bounded regional extent. +Overview responses now return at most 25 representative GeoJSON features per +source (250 regionally, 1,000 in detail) while `total_feature_count` and every +PostGIS metric continue to cover the complete selection. This prevents large +profile properties from blocking the browser without weakening measurements. Validation: diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx index 9b2c888a..1c37f22f 100644 --- a/frontend/src/components/map/MapWorkspace.tsx +++ b/frontend/src/components/map/MapWorkspace.tsx @@ -45,6 +45,7 @@ import { selectionAnalysisScale, selectionAreaSquareMetres, selectionDimensions, + selectionFeatureLimit, selectionMetricLabel, splitSelectionBbox, } from './mapWorkspaceUtils' @@ -1830,12 +1831,14 @@ export function MapWorkspace({ } } const availableThemes: Array> = [] + const resultFeatureLimit = selectionFeatureLimit(bbox) for (const theme of DATA_THEMES) { const dataset = themeDatasetMap[theme.id] if (dataset && persistedDatasetSupportsSelection(dataset, bbox)) { availableThemes.push({ themeId: theme.id, dataset, + featureLimit: resultFeatureLimit, partitioned: regionalScopeSelected && (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)), }) @@ -1852,6 +1855,7 @@ export function MapWorkspace({ displayName: onDemandProduct.displayName, }, acquisitionBboxes: onDemandProduct.acquisitionBboxes, + featureLimit: resultFeatureLimit, }) } continue diff --git a/frontend/src/components/map/mapWorkspaceUtils.test.ts b/frontend/src/components/map/mapWorkspaceUtils.test.ts index 78ad4cda..88e47616 100644 --- a/frontend/src/components/map/mapWorkspaceUtils.test.ts +++ b/frontend/src/components/map/mapWorkspaceUtils.test.ts @@ -12,6 +12,7 @@ import { selectionAnalysisScale, selectionAreaSquareMetres, selectionDimensions, + selectionFeatureLimit, splitSelectionBbox, } from './mapWorkspaceUtils' import type { VectorSelectionBBox, VectorSelectionResponse } from '../../types' @@ -156,5 +157,7 @@ describe('map workspace selection guards', () => { { dataset_type: 'vector', source_name: 'statbel' }, overview, )).toBe(true) + expect(selectionFeatureLimit(overview)).toBe(25) + expect(selectionFeatureLimit({ ...overview, min_x: 5, max_x: 5.1, min_y: 51, max_y: 51.1 })).toBe(1000) }) }) diff --git a/frontend/src/components/map/mapWorkspaceUtils.ts b/frontend/src/components/map/mapWorkspaceUtils.ts index 0afcf3e1..7a35801d 100644 --- a/frontend/src/components/map/mapWorkspaceUtils.ts +++ b/frontend/src/components/map/mapWorkspaceUtils.ts @@ -117,6 +117,13 @@ export function persistedDatasetSupportsSelection( return scale === 'detail' } +export function selectionFeatureLimit(bbox: VectorSelectionBBox): number { + const scale = selectionAnalysisScale(bbox) + if (scale === 'overview') return 25 + if (scale === 'regional') return 250 + return 1000 +} + export function splitSelectionBbox( bbox: VectorSelectionBBox, maxTileSideMetres = 18_000, diff --git a/frontend/src/hooks/useMapThemeSelectionInsights.ts b/frontend/src/hooks/useMapThemeSelectionInsights.ts index 642a715f..d0c84cf4 100644 --- a/frontend/src/hooks/useMapThemeSelectionInsights.ts +++ b/frontend/src/hooks/useMapThemeSelectionInsights.ts @@ -27,6 +27,7 @@ export interface MapThemeQuery { partitioned?: boolean acquisition?: MapThemeAcquisition acquisitionBboxes?: VectorSelectionBBox[] + featureLimit?: number } export interface MapThemeInsight { @@ -104,9 +105,10 @@ export function useMapThemeSelectionInsights( const settled = await settleWithConcurrency( queries, 3, - async ({ themeId, dataset: existingDataset, partitioned, acquisition, acquisitionBboxes }) => { + async ({ themeId, dataset: existingDataset, partitioned, acquisition, acquisitionBboxes, featureLimit }) => { let dataset = existingDataset let acquiredDatasets: DatasetCreateResponse[] = [] + const resultLimit = featureLimit ?? 1000 if (acquisition) { const requestedBboxes = acquisitionBboxes?.length ? acquisitionBboxes : [bbox] const acquisitionResults = await settleWithConcurrency(requestedBboxes, 1, async (acquisitionBbox) => { @@ -209,18 +211,18 @@ export function useMapThemeSelectionInsights( dataset_ids: acquiredDatasetIds, bbox, area_id: areaId, - limit: 1000, + limit: resultLimit, }) : dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned ? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, { bbox, area_id: areaId, - limit: 1000, + limit: resultLimit, }) : await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, { bbox, area_id: areaId, - limit: 1000, + limit: resultLimit, }), } },