Prefer persisted sources in map federation
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-19 02:08:52 +02:00
parent a823342b5b
commit 5f15753247
8 changed files with 96 additions and 21 deletions
+4
View File
@@ -34,6 +34,10 @@
reported as an NGI administrative boundary layer. reported as an NGI administrative boundary layer.
- Made bounded API coverage spatially honest: a persisted provider selection - Made bounded API coverage spatially honest: a persisted provider selection
is operational only for selections contained by its retained source bbox. is operational only for selections contained by its retained source bbox.
- Kept reusable persisted datasets ahead of on-demand catalog placeholders in
the workbench, so national Statbel 2025 population data is no longer
mislabeled as a 2019 thematic raster. Selection-bounded SPW/UrbIS results
remain available through the exact on-demand acquisition path.
## Autonomous Belgium and North Sea RC program (2026-07-17) ## Autonomous Belgium and North Sea RC program (2026-07-17)
@@ -49,7 +49,7 @@ def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
assert "regionalOnDemandThemeActive" in workspace assert "regionalOnDemandThemeActive" in workspace
assert "regionalRasterThemeActive || regionalOnDemandThemeActive" in workspace assert "regionalRasterThemeActive || regionalOnDemandThemeActive" in workspace
assert "Teken een begrensde rechthoek voor deze regionale analyse." in workspace assert "Teken een begrensde rechthoek voor deze regionale analyse." in workspace
assert "Vlaamse kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt" in workspace assert "regionale kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt" in workspace
def test_frontend_does_not_contact_external_map_services_directly() -> None: def test_frontend_does_not_contact_external_map_services_directly() -> None:
+13
View File
@@ -31,6 +31,19 @@
- Bounded provider materialization now also checks its persisted - Bounded provider materialization now also checks its persisted
`bbox_epsg4326`; one small PICC/UrbIS/GRB/raster acquisition can no longer `bbox_epsg4326`; one small PICC/UrbIS/GRB/raster acquisition can no longer
claim that an entire region is locally loaded. claim that an entire region is locally loaded.
- The live governed Statbel apply materialized the reviewed 2025 `NEW` edition
as Dataset `ee0a46e5-6139-4b0a-93b5-9dabb3b3dfc7`: 20,781 statistical
sectors, 565 municipalities, four repaired geometries and a reconciled
national total of 11,825,551 inhabitants (11,817,897 spatially located plus
7,654 explicitly unlocated).
- Live bounded PICC and UrbIS persistence proved the same service path:
54 Walloon building geometries retained in Dataset
`0c3c0a11-c00e-4c84-87be-8ffa64302dec` and 47 Brussels building geometries
in Dataset `de53ccc0-a494-4c4e-bd0d-16d807bfefe7`.
- Corrected the map catalog precedence so reusable persisted datasets win over
on-demand placeholders. National Statbel 2025 population is now presented
as the active source, while selection-bounded PICC/UrbIS acquisitions remain
available only through an exact bounded request.
- Focused backend and frontend suites passed before full release validation; - Focused backend and frontend suites passed before full release validation;
full local and Tower evidence follows in the final P5 gate. full local and Tower evidence follows in the final P5 gate.
@@ -68,7 +68,7 @@ applicable live bounded journey fails.
## P0 - Existing national layers become usable ## P0 - Existing national layers become usable
**State: implemented locally; live release evidence pending under P5.** **State: complete; live national and maritime source journeys proven.**
### Work ### Work
@@ -97,7 +97,7 @@ applicable live bounded journey fails.
## P1 - National Statbel population ## P1 - National Statbel population
**State: implementation complete; reviewed live national materialization pending.** **State: complete; reviewed 2025 national edition materialized live.**
### Work ### Work
@@ -120,9 +120,17 @@ applicable live bounded journey fails.
from the same authoritative Statbel edition. from the same authoritative Statbel edition.
- Mol remains within its documented baseline tolerance. - Mol remains within its documented baseline tolerance.
### Live evidence
- Reviewed dataset: `ee0a46e5-6139-4b0a-93b5-9dabb3b3dfc7`.
- 20,781 persisted sectors and all 565 municipalities are represented.
- National reconciliation: 11,825,551 inhabitants, including 7,654 explicitly
retained as unlocated rather than silently assigned to geometry.
- A bounded Mol selection returns an explicitly labelled area-weighted estimate.
## P2 - Wallonia bounded topographic baseline ## P2 - Wallonia bounded topographic baseline
**State: implemented locally; live Wallonia journey pending under P5.** **State: complete; bounded Wallonia acquisition and analysis proven live.**
### Governed source ### Governed source
@@ -158,9 +166,16 @@ Initial allowlisted layers:
- Provider failures and record-limit truncation fail closed. - Provider failures and record-limit truncation fail closed.
- No direct provider-to-`vector_features` write exists. - No direct provider-to-`vector_features` write exists.
### Live evidence
- Dataset `0c3c0a11-c00e-4c84-87be-8ffa64302dec` retained 54 valid building
geometries for the bounded Wallonia smoke selection.
- The persisted selection reported 54 buildings and 0.525363 ha without
extending operational coverage beyond the retained bbox.
## P3 - Brussels bounded UrbIS baseline ## P3 - Brussels bounded UrbIS baseline
**State: implemented locally; live Brussels journey pending under P5.** **State: complete; bounded Brussels acquisition and analysis proven live.**
### Governed source ### Governed source
@@ -183,6 +198,13 @@ capabilities probe before implementation is marked operational.
parcels. parcels.
- Catalogue-only or failed capabilities evidence remains `not_configured`. - Catalogue-only or failed capabilities evidence remains `not_configured`.
### Live evidence
- Dataset `de53ccc0-a494-4c4e-bd0d-16d807bfefe7` retained 47 UrbIS building
geometries for the bounded Brussels smoke selection.
- The persisted selection reported 47 buildings and 0.528894 ha without
claiming coverage outside the retained bbox.
## P4 - Maritime and bathymetry hardening ## P4 - Maritime and bathymetry hardening
**State: complete with MDK acquisition visibly blocked by strict-TLS evidence.** **State: complete with MDK acquisition visibly blocked by strict-TLS evidence.**
+1 -1
View File
@@ -9,7 +9,7 @@ maritieme databronnen bruikbaar in de volgende versie.
- [x] P0: bestaande NGI/RBINS-lagen als kaartthema en selectieanalyse ontsluiten. - [x] P0: bestaande NGI/RBINS-lagen als kaartthema en selectieanalyse ontsluiten.
- [x] P0: readiness baseren op bruikbare analysethema's, niet alleen datasetaantal. - [x] P0: readiness baseren op bruikbare analysethema's, niet alleen datasetaantal.
- [ ] P1: nationale Statbel-populatieflow veilig operationaliseren. - [x] P1: nationale Statbel-populatieflow veilig operationaliseren.
- [x] P2: bounded SPW PICC voor Waalse gebouwen, wegen en hydrographie. - [x] P2: bounded SPW PICC voor Waalse gebouwen, wegen en hydrographie.
- [x] P3: bounded UrbIS voor Brusselse gebouwen en percelen. - [x] P3: bounded UrbIS voor Brusselse gebouwen en percelen.
- [x] P4: maritieme thema's harden en bathymetrie fail-closed onderzoeken. - [x] P4: maritieme thema's harden en bathymetrie fail-closed onderzoeken.
+28 -15
View File
@@ -27,6 +27,7 @@ import {
getFeatureBBox, getFeatureBBox,
getFeatureCollectionBBox, getFeatureCollectionBBox,
getFeatureGeometrySummary, getFeatureGeometrySummary,
isSelectionBoundedDataset,
normalizeBboxFromCorners, normalizeBboxFromCorners,
operationalScopeProjectLabel, operationalScopeProjectLabel,
parseBboxInput, parseBboxInput,
@@ -411,6 +412,9 @@ function datasetCoversSelectedArea(
selectedAreaName: string | null | undefined, selectedAreaName: string | null | undefined,
regionalScope = false, regionalScope = false,
): boolean { ): boolean {
if (isSelectionBoundedDataset(dataset.source_metadata)) {
return false
}
const selectedZones = selectedAreaCoverageZones(selectedAreaName) const selectedZones = selectedAreaCoverageZones(selectedAreaName)
const configuredZones = dataset.source_metadata?.['coverage_zones'] const configuredZones = dataset.source_metadata?.['coverage_zones']
if (selectedZones && Array.isArray(configuredZones) && configuredZones.length > 0) { if (selectedZones && Array.isArray(configuredZones) && configuredZones.length > 0) {
@@ -929,19 +933,25 @@ export function MapWorkspace({
} }
if (flandersScopeSelected && officialMapProducts.thematic.length > 0) { if (flandersScopeSelected && officialMapProducts.thematic.length > 0) {
for (const product of officialMapProducts.thematic) { for (const product of officialMapProducts.thematic) {
result[product.theme] = null if (!result[product.theme]) {
result[product.theme] = null
}
} }
} }
if (flandersScopeSelected && officialMapProducts.grb.length > 0) { if (flandersScopeSelected && officialMapProducts.grb.length > 0) {
for (const product of officialMapProducts.grb) { for (const product of officialMapProducts.grb) {
result[product.key] = null if (!result[product.key]) {
result[product.key] = null
}
} }
} }
if (officialMapProducts.officialVector.length > 0) { if (officialMapProducts.officialVector.length > 0) {
for (const product of officialMapProducts.officialVector.filter((item) => for (const product of officialMapProducts.officialVector.filter((item) =>
productCoversZones(item.coverage_zones, selectedCoverageZones), productCoversZones(item.coverage_zones, selectedCoverageZones),
)) { )) {
result[product.theme] = null if (!result[product.theme]) {
result[product.theme] = null
}
} }
} }
return result return result
@@ -1077,7 +1087,9 @@ export function MapWorkspace({
} }
return result return result
}, [onDemandProductsForZones, selectedCoverageZones]) }, [onDemandProductsForZones, selectedCoverageZones])
const activeOnDemandMapProduct = onDemandProductMap.get(activeTheme.id) ?? null const activeOnDemandMapProduct = themeDatasetMap[activeTheme.id]
? null
: onDemandProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection) const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
@@ -1665,6 +1677,16 @@ export function MapWorkspace({
: [] : []
const availableThemes: Array<MapThemeQuery<DataThemeId>> = [] const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
for (const theme of DATA_THEMES) { for (const theme of DATA_THEMES) {
const dataset = themeDatasetMap[theme.id]
if (dataset) {
availableThemes.push({
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
})
continue
}
const onDemandProducts = resolvedProducts.filter((product) => product.theme === theme.id) const onDemandProducts = resolvedProducts.filter((product) => product.theme === theme.id)
if (onDemandProducts.length > 0) { if (onDemandProducts.length > 0) {
for (const onDemandProduct of onDemandProducts) { for (const onDemandProduct of onDemandProducts) {
@@ -1679,15 +1701,6 @@ export function MapWorkspace({
} }
continue continue
} }
const dataset = themeDatasetMap[theme.id]
if (dataset) {
availableThemes.push({
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
})
}
} }
await loadThemeInsights(bbox, availableThemes, areaId) await loadThemeInsights(bbox, availableThemes, areaId)
} }
@@ -1992,7 +2005,7 @@ export function MapWorkspace({
</small> </small>
</div> </div>
{officialMapProductsLoading && selectedProjectId ? ( {officialMapProductsLoading && selectedProjectId ? (
<p className="geo-data-notice">Beschikbare Vlaamse kaartbronnen worden gecontroleerd</p> <p className="geo-data-notice">Beschikbare officiële regionale kaartbronnen worden gecontroleerd</p>
) : null} ) : null}
{officialMapProductsError && selectedProjectId ? ( {officialMapProductsError && selectedProjectId ? (
<p className="error">{officialMapProductsError}</p> <p className="error">{officialMapProductsError}</p>
@@ -2154,7 +2167,7 @@ export function MapWorkspace({
: regionalRasterThemeActive : regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.' ? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThemeActive : onDemandThemeActive
? 'Teken een rechthoek; officiële Vlaamse kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt.' ? 'Teken een rechthoek; officiële regionale kaartbronnen worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive : regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.' ? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'} : 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { import {
bboxesEqual, bboxesEqual,
isSelectionBoundedDataset,
normalizeBboxFromCorners, normalizeBboxFromCorners,
parseBboxInput, parseBboxInput,
productCoversZones, productCoversZones,
@@ -63,4 +64,18 @@ describe('map workspace selection guards', () => {
expect(productCoversZones(['wallonia'], ['flanders', 'wallonia'])).toBe(true) expect(productCoversZones(['wallonia'], ['flanders', 'wallonia'])).toBe(true)
expect(productCoversZones(['brussels'], ['wallonia'])).toBe(false) expect(productCoversZones(['brussels'], ['wallonia'])).toBe(false)
}) })
it('distinguishes selection-bounded acquisitions from reusable regional datasets', () => {
expect(isSelectionBoundedDataset({
geometry_clipped_to_selection: true,
bbox_epsg4326: [4.55, 50.58, 4.56, 50.59],
})).toBe(true)
expect(isSelectionBoundedDataset({
geometry_clipped_to_selection: false,
bbox_epsg4326: [2.5, 49.5, 6.4, 51.6],
})).toBe(false)
expect(isSelectionBoundedDataset({
geometry_clipped_to_selection: true,
})).toBe(false)
})
}) })
@@ -35,6 +35,14 @@ export function productCoversZones(productZones: string[], selectedZones: string
return selectedZones === null || selectedZones.some((zone) => productZones.includes(zone)) return selectedZones === null || selectedZones.some((zone) => productZones.includes(zone))
} }
export function isSelectionBoundedDataset(
sourceMetadata: Record<string, unknown> | null | undefined,
): boolean {
return sourceMetadata?.['geometry_clipped_to_selection'] === true
&& Array.isArray(sourceMetadata?.['bbox_epsg4326'])
&& sourceMetadata['bbox_epsg4326'].length === 4
}
export function operationalScopeProjectLabel(project: ProjectRead): string { export function operationalScopeProjectLabel(project: ProjectRead): string {
if (project.name === MOL_PROJECT_NAME) { if (project.name === MOL_PROJECT_NAME) {
return 'Mol' return 'Mol'