fix(map): read all relevant selection sources
This commit is contained in:
@@ -9,6 +9,17 @@
|
|||||||
|
|
||||||
## 1.0.0 - Final Belgium and Belgian North Sea release (2026-07-19)
|
## 1.0.0 - Final Belgium and Belgian North Sea release (2026-07-19)
|
||||||
|
|
||||||
|
- Fixed rectangle analysis so it materializes and reads all applicable
|
||||||
|
official sources instead of acquiring only the active theme and labelling
|
||||||
|
every other result `Bron ontbreekt`. Provider work is bounded to three
|
||||||
|
concurrent requests, persisted artifacts remain reusable and real failures
|
||||||
|
are shown as load failures.
|
||||||
|
- Registered VMM VHA historical profile points as the operational bounded
|
||||||
|
Flemish bathymetry-adjacent source and added them to the map product
|
||||||
|
catalogue. Continuous depth and water volume remain explicitly unsupported.
|
||||||
|
- Live-smoked a Mol rectangle through NGI, Statbel, GRB, Flemish thematic
|
||||||
|
rasters, DHMV, VMM flood hazard, BWK, DOV and VHA, including persisted
|
||||||
|
Dataset output and semantic vector/raster metrics.
|
||||||
- Rebuilt the complete frontend presentation as the Stitch-guided GeoIntel
|
- Rebuilt the complete frontend presentation as the Stitch-guided GeoIntel
|
||||||
Atlas Workbench. The new shell uses one compact icon navigation rail, one
|
Atlas Workbench. The new shell uses one compact icon navigation rail, one
|
||||||
context bar and task-specific work surfaces across Map, Sources, AI
|
context bar and task-specific work surfaces across Map, Sources, AI
|
||||||
|
|||||||
@@ -208,6 +208,25 @@ SOURCE_DEFINITIONS = (
|
|||||||
"agentschap_landbouw_zeevisserij_agricultural_parcels",
|
"agentschap_landbouw_zeevisserij_agricultural_parcels",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_contract(
|
||||||
|
source_name="vmm_vha_bathymetry_profiles",
|
||||||
|
display_name="VHA historische dwarsprofielen",
|
||||||
|
authority_level="authoritative",
|
||||||
|
coverage_zones=("flanders",),
|
||||||
|
themes=("bathymetry",),
|
||||||
|
native_layers=("digitale_atlas_profile_points",),
|
||||||
|
geometry_types=("Point",),
|
||||||
|
acquisition_mode="bounded_api",
|
||||||
|
integration_status="operational",
|
||||||
|
source_url="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0",
|
||||||
|
attribution="Vlaamse Milieumaatschappij (VMM), Vlaamse Hydrografische Atlas",
|
||||||
|
license_note="Hergebruik volgens de voorwaarden van de Vlaamse overheid en de bronmetadata.",
|
||||||
|
limitation_message=(
|
||||||
|
"Historische puntmetingen met bronafhankelijke meetdatum en verticale referentie; "
|
||||||
|
"geen continue actuele bodemkaart en zonder gelijktijdig waterpeil geen watervolume."
|
||||||
|
),
|
||||||
|
materialized_source_names=("vmm_vha_bathymetry_profiles",),
|
||||||
|
),
|
||||||
_contract(
|
_contract(
|
||||||
source_name="spw_geoportail",
|
source_name="spw_geoportail",
|
||||||
display_name="SPW Geoportail Wallonie",
|
display_name="SPW Geoportail Wallonie",
|
||||||
|
|||||||
@@ -64,9 +64,13 @@ def test_coverage_catalog_uses_normalized_contracts_and_does_not_change_provider
|
|||||||
"rbins_marine_reporting_units",
|
"rbins_marine_reporting_units",
|
||||||
"rbins_msp_2026",
|
"rbins_msp_2026",
|
||||||
"mdk_bathymetry",
|
"mdk_bathymetry",
|
||||||
|
"vmm_vha_bathymetry_profiles",
|
||||||
}
|
}
|
||||||
assert next(source for source in catalog.sources if source.source_name == "ngi_adminvector").license_note == "CC BY 4.0"
|
assert next(source for source in catalog.sources if source.source_name == "ngi_adminvector").license_note == "CC BY 4.0"
|
||||||
assert next(source for source in catalog.sources if source.source_name == "mdk_bathymetry").integration_status == "not_configured"
|
assert next(source for source in catalog.sources if source.source_name == "mdk_bathymetry").integration_status == "not_configured"
|
||||||
|
assert next(
|
||||||
|
source for source in catalog.sources if source.source_name == "vmm_vha_bathymetry_profiles"
|
||||||
|
).integration_status == "operational"
|
||||||
|
|
||||||
response = TestClient(app).get("/api/v1/external/coverage/catalog")
|
response = TestClient(app).get("/api/v1/external/coverage/catalog")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -268,6 +272,43 @@ def test_spw_bathymetry_materialization_is_source_specific() -> None:
|
|||||||
assert with_bathymetry.items[0].materialized_dataset_ids == [bathymetry_id]
|
assert with_bathymetry.items[0].materialized_dataset_ids == [bathymetry_id]
|
||||||
|
|
||||||
|
|
||||||
|
def test_vha_bathymetry_profiles_are_operational_only_inside_the_persisted_selection() -> None:
|
||||||
|
project_id = uuid4()
|
||||||
|
scope = [
|
||||||
|
scope_area("Belgium land", box(2.5, 49.5, 6.4, 51.5)),
|
||||||
|
scope_area("Flanders", box(2.5, 50.7, 5.9, 51.5)),
|
||||||
|
]
|
||||||
|
selection = CoverageBBox(minx=5.101, miny=51.171, maxx=5.109, maxy=51.179)
|
||||||
|
without_profiles = CoverageRegistryService.resolve(
|
||||||
|
FakeSession(project=SimpleNamespace(id=project_id), areas=scope, datasets=[]),
|
||||||
|
project_id,
|
||||||
|
selection,
|
||||||
|
["bathymetry"],
|
||||||
|
)
|
||||||
|
assert without_profiles.items[0].status == "partial"
|
||||||
|
assert without_profiles.items[0].source_names == ["vmm_vha_bathymetry_profiles"]
|
||||||
|
|
||||||
|
profile_id = uuid4()
|
||||||
|
profiles = SimpleNamespace(
|
||||||
|
id=profile_id,
|
||||||
|
status="ready",
|
||||||
|
source_name="vmm_vha_bathymetry_profiles",
|
||||||
|
reference_layer_name="bathymetry_profile_points",
|
||||||
|
source_metadata={
|
||||||
|
"coverage_zones": ["flanders"],
|
||||||
|
"bbox_epsg4326": [5.1, 51.17, 5.11, 51.18],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with_profiles = CoverageRegistryService.resolve(
|
||||||
|
FakeSession(project=SimpleNamespace(id=project_id), areas=scope, datasets=[profiles]),
|
||||||
|
project_id,
|
||||||
|
selection,
|
||||||
|
["bathymetry"],
|
||||||
|
)
|
||||||
|
assert with_profiles.items[0].status == "operational"
|
||||||
|
assert with_profiles.items[0].materialized_dataset_ids == [profile_id]
|
||||||
|
|
||||||
|
|
||||||
def test_mixed_land_and_north_sea_selection_remains_split() -> None:
|
def test_mixed_land_and_north_sea_selection_remains_split() -> None:
|
||||||
project_id = uuid4()
|
project_id = uuid4()
|
||||||
project = SimpleNamespace(id=project_id)
|
project = SimpleNamespace(id=project_id)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def test_map_first_explorer_is_the_default_product_flow() -> None:
|
|||||||
assert "<h3>Inzichten</h3>" in workspace
|
assert "<h3>Inzichten</h3>" in workspace
|
||||||
assert "Teken rechthoek" in workspace
|
assert "Teken rechthoek" in workspace
|
||||||
assert "Volledig werkgebied" in workspace
|
assert "Volledig werkgebied" in workspace
|
||||||
assert "Alle beschikbare thema" in workspace
|
assert "Alle relevante thema" in workspace
|
||||||
assert "Bron nog niet ingeladen" in workspace
|
assert "Bron nog niet ingeladen" in workspace
|
||||||
assert "useMapThemeSelectionInsights" in workspace
|
assert "useMapThemeSelectionInsights" in workspace
|
||||||
assert "datasetsApi.selectVectorFeatures" in read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
assert "datasetsApi.selectVectorFeatures" in read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ def test_end_user_dataset_sources_are_human_readable() -> None:
|
|||||||
assert "department_omgeving_land_use: 'Departement Omgeving'" in display
|
assert "department_omgeving_land_use: 'Departement Omgeving'" in display
|
||||||
assert "statbel: 'Statbel'" in display
|
assert "statbel: 'Statbel'" in display
|
||||||
assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace
|
assert "getDatasetSourceDisplayName(activeThemeDataset)" in workspace
|
||||||
assert "dataset ? getDatasetSourceDisplayName(dataset)" in workspace
|
assert "resultDataset ? getDatasetSourceDisplayName(resultDataset)" in workspace
|
||||||
assert "Snel naar een gemeente (optioneel)" in workspace
|
assert "Snel naar een gemeente (optioneel)" in workspace
|
||||||
assert "latestDatasetBySeries" in catalog
|
assert "latestDatasetBySeries" in catalog
|
||||||
assert "Historische meetmomenten" in catalog
|
assert "Historische meetmomenten" in catalog
|
||||||
|
|||||||
@@ -29,19 +29,23 @@ def test_flanders_workspace_exposes_governed_thematic_products_on_demand() -> No
|
|||||||
assert "/datasets/thematic-raster/acquire" in api
|
assert "/datasets/thematic-raster/acquire" in api
|
||||||
|
|
||||||
|
|
||||||
def test_selection_reads_persisted_themes_but_acquires_only_the_active_theme() -> None:
|
def test_selection_reads_and_bounded_acquires_all_relevant_themes() -> None:
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
||||||
app = read("frontend/src/App.tsx")
|
app = read("frontend/src/App.tsx")
|
||||||
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
||||||
|
|
||||||
assert "for (const theme of DATA_THEMES)" in workspace
|
assert "for (const theme of DATA_THEMES)" in workspace
|
||||||
assert ".filter((product) => product.theme === activeThemeId)" in workspace
|
assert "? onDemandProductsForZones(resolvedZones)" in workspace
|
||||||
|
assert ".filter((product) => product.theme === activeThemeId)" not in workspace
|
||||||
assert "await loadThemeInsights(bbox, availableThemes, areaId)" in workspace
|
assert "await loadThemeInsights(bbox, availableThemes, areaId)" in workspace
|
||||||
assert "!regionalPartitionedThemeActive && !onDemandThemeActive" in workspace
|
assert "!regionalPartitionedThemeActive && !onDemandThemeActive" in workspace
|
||||||
assert "onRefreshProjectData" in workspace
|
assert "onRefreshProjectData" in workspace
|
||||||
assert "selectedProjectId ? loadProjectData(selectedProjectId)" in app
|
assert "selectedProjectId ? loadProjectData(selectedProjectId)" in app
|
||||||
assert "successful.some((item) => item.acquisition)" in selection_hook
|
assert "successful.some((item) => item.acquisition)" in selection_hook
|
||||||
assert "await onDatasetsChanged()" in selection_hook
|
assert "await onDatasetsChanged()" in selection_hook
|
||||||
|
assert "settleWithConcurrency(" in selection_hook
|
||||||
|
assert "queries," in selection_hook
|
||||||
|
assert "3," in selection_hook
|
||||||
|
|
||||||
|
|
||||||
def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
|
def test_regional_on_demand_sources_require_a_bounded_drawn_selection() -> None:
|
||||||
|
|||||||
@@ -25,7 +25,15 @@ def test_map_selection_can_acquire_dhmv_and_flood_hazard_products() -> None:
|
|||||||
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
selection_hook = read("frontend/src/hooks/useMapThemeSelectionInsights.ts")
|
||||||
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
||||||
|
|
||||||
assert "'thematic_raster' | 'dhmv' | 'flood_hazard' | 'grb'" in selection_hook
|
for acquisition_kind in (
|
||||||
|
"'thematic_raster'",
|
||||||
|
"'dhmv'",
|
||||||
|
"'flood_hazard'",
|
||||||
|
"'grb'",
|
||||||
|
"'official_vector'",
|
||||||
|
"'bathymetry_profiles'",
|
||||||
|
):
|
||||||
|
assert acquisition_kind in selection_hook
|
||||||
assert "datasetsApi.acquireDhmv" in selection_hook
|
assert "datasetsApi.acquireDhmv" in selection_hook
|
||||||
assert "datasetsApi.acquireFloodHazard" in selection_hook
|
assert "datasetsApi.acquireFloodHazard" in selection_hook
|
||||||
assert "datasetsApi.acquireThematicRaster" in selection_hook
|
assert "datasetsApi.acquireThematicRaster" in selection_hook
|
||||||
|
|||||||
@@ -984,6 +984,13 @@ per zone/theme combination, matching source names and IDs of actually
|
|||||||
materialized Datasets. An empty `themes` list requests the complete normalized
|
materialized Datasets. An empty `themes` list requests the complete normalized
|
||||||
vocabulary. Unknown themes fail with `COVERAGE_THEME_UNSUPPORTED`.
|
vocabulary. Unknown themes fail with `COVERAGE_THEME_UNSUPPORTED`.
|
||||||
|
|
||||||
|
The Map workbench uses this response before a rectangle analysis. It queries
|
||||||
|
matching persisted Datasets and may call the already documented bounded
|
||||||
|
acquisition endpoints for applicable `partial` source contracts. Those calls
|
||||||
|
run through a client queue with at most three concurrent acquisitions. An
|
||||||
|
`unsupported` or `not_configured` theme is not rendered as a failed
|
||||||
|
measurement; an attempted provider failure is reported explicitly.
|
||||||
|
|
||||||
### GET `/api/v1/external/providers`
|
### GET `/api/v1/external/providers`
|
||||||
|
|
||||||
Returns all configured provider capability descriptors.
|
Returns all configured provider capability descriptors.
|
||||||
|
|||||||
@@ -10909,3 +10909,40 @@ Behavior preservation:
|
|||||||
Ollama or model-runtime behavior changed;
|
Ollama or model-runtime behavior changed;
|
||||||
- all existing actions continue through the original React hooks and service
|
- all existing actions continue through the original React hooks and service
|
||||||
layer.
|
layer.
|
||||||
|
|
||||||
|
## 2026-07-19 - Complete bounded rectangle analysis
|
||||||
|
|
||||||
|
Implemented:
|
||||||
|
|
||||||
|
- removed the active-theme-only acquisition filter from Map rectangle
|
||||||
|
analysis;
|
||||||
|
- added a deterministic acquisition queue capped at three concurrent official
|
||||||
|
provider requests;
|
||||||
|
- restricted the result list to themes that are operational or have an
|
||||||
|
applicable bounded adapter in the resolved coverage zone;
|
||||||
|
- replaced misleading `Bron ontbreekt` rows with explicit provider-failure
|
||||||
|
states and counts results per theme rather than per contributing layer;
|
||||||
|
- registered VMM VHA historical profile points as an operational bounded
|
||||||
|
Flanders bathymetry source and connected its existing acquire/select routes
|
||||||
|
to the Map product catalogue.
|
||||||
|
|
||||||
|
Live evidence:
|
||||||
|
|
||||||
|
- a bounded Mol rectangle persisted and selected all relevant source families:
|
||||||
|
NGI, Statbel, four GRB products, seven Flemish thematic rasters, DHMV,
|
||||||
|
VMM flood hazard, BWK/Natura 2000, DOV soil and VHA profile points;
|
||||||
|
- semantic results included building/forest/agricultural/open-space/water/
|
||||||
|
parcel/soil/nature hectares, road kilometres, inhabitants, terrain height,
|
||||||
|
flood area, accessibility/service scores and historical profile counts;
|
||||||
|
- no browser-direct provider request, fabricated value or vertical-datum
|
||||||
|
conversion was introduced.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
|
||||||
|
- the complete readiness gate passed: 1,053 backend tests, 24 frontend tests,
|
||||||
|
backend compile, frontend TypeScript/typecheck, production build, Alembic
|
||||||
|
head `202607160001` and script smoke checks;
|
||||||
|
- a live API smoke over `5.10,51.17,5.11,51.18` proved all 17 applicable Mol
|
||||||
|
themes through acquisition or persisted national data and semantic
|
||||||
|
selection metrics;
|
||||||
|
- browser verification follows against the deployed commit on port 1202.
|
||||||
|
|||||||
@@ -799,6 +799,14 @@ profile numbers, measurement dates, available structured depth/width values
|
|||||||
and official document URLs. Scanned documents remain evidence; missing fields
|
and official document URLs. Scanned documents remain evidence; missing fields
|
||||||
are not filled by fabricated OCR output.
|
are not filled by fabricated OCR output.
|
||||||
|
|
||||||
|
VHA is registered as the operational bounded source
|
||||||
|
`vmm_vha_bathymetry_profiles` for the Flemish `bathymetry` coverage contract.
|
||||||
|
The Map workbench can therefore acquire the profile points as part of the same
|
||||||
|
bounded selection queue as the other applicable Flemish themes. The resulting
|
||||||
|
metric remains a count of historical profiles unless structured source values
|
||||||
|
support an additional metric; it is never presented as continuous water depth
|
||||||
|
or volume.
|
||||||
|
|
||||||
The following sources are audited:
|
The following sources are audited:
|
||||||
|
|
||||||
- MDK Dieptemodel Belgisch Continentaal Plat/Noordzee: 20 x 20 m continuous
|
- MDK Dieptemodel Belgisch Continentaal Plat/Noordzee: 20 x 20 m continuous
|
||||||
|
|||||||
+9
-3
@@ -61,9 +61,11 @@ geen open productroadmap meer.
|
|||||||
selecteerbaar werkgebied; een technische project- of regioselectie is niet
|
selecteerbaar werkgebied; een technische project- of regioselectie is niet
|
||||||
vereist.
|
vereist.
|
||||||
- [x] Kies een begrijpbaar datathema, teken een rechthoek of gebruik het
|
- [x] Kies een begrijpbaar datathema, teken een rechthoek of gebruik het
|
||||||
volledige werkgebied en analyseer alle reeds beschikbare thema's uit
|
volledige werkgebied en analyseer alle relevante thema's uit PostGIS.
|
||||||
PostGIS; alleen het actief gekozen thema mag een nieuwe begrensde
|
Een getekende selectie mag ontbrekende operationele bronproducten begrensd
|
||||||
on-demandbron ophalen.
|
en met maximaal drie gelijktijdige aanvragen materialiseren; niet-toepasselijke
|
||||||
|
of niet-operationele regionale thema's worden niet als ontbrekende meting
|
||||||
|
gepresenteerd.
|
||||||
- [x] Gebruik voor getekende en handmatig ingevoerde rechthoeken overal
|
- [x] Gebruik voor getekende en handmatig ingevoerde rechthoeken overal
|
||||||
`bbox ∩ Area`; een bbox rond het volledige werkgebied resolveert naar de
|
`bbox ∩ Area`; een bbox rond het volledige werkgebied resolveert naar de
|
||||||
exacte persistente Area-geometrie en activeert pas dan de full-Area fast path.
|
exacte persistente Area-geometrie en activeert pas dan de full-Area fast path.
|
||||||
@@ -99,6 +101,10 @@ geen open productroadmap meer.
|
|||||||
- [x] Maak de vijf bestaande officiële beleidsrasters veilig op aanvraag
|
- [x] Maak de vijf bestaande officiële beleidsrasters veilig op aanvraag
|
||||||
beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache,
|
beschikbaar voor elke begrensde selectie in Vlaanderen, met Dataset-cache,
|
||||||
semantische metrics en zonder 1.425 vooraf geladen rasters.
|
semantische metrics en zonder 1.425 vooraf geladen rasters.
|
||||||
|
- [x] Maak de volledige selectieanalyse in Vlaanderen operationeel voor NGI,
|
||||||
|
Statbel, GRB, beleidsrasters, DHMV, overstromingsgevaar, BWK, DOV en VHA:
|
||||||
|
één rechthoek materialiseert ontbrekende officiële bronproducten, leest
|
||||||
|
persistente vector/rastergegevens uit en rapporteert inhoudelijke eenheden.
|
||||||
- [x] Vervang de gegroeide dashboardpresentatie door de Stitch-gestuurde Atlas
|
- [x] Vervang de gegroeide dashboardpresentatie door de Stitch-gestuurde Atlas
|
||||||
Workbench met een compacte navigatierail, vaste contextbalk, taakgerichte
|
Workbench met een compacte navigatierail, vaste contextbalk, taakgerichte
|
||||||
schermen en gevalideerde desktop-, ultrawide- en mobiele layouts.
|
schermen en gevalideerde desktop-, ultrawide- en mobiele layouts.
|
||||||
|
|||||||
+20
-9
@@ -69,16 +69,27 @@ result. Water explicitly explains that volume cannot be derived without a
|
|||||||
reliable depth or bathymetry source. The advanced workbench remains available
|
reliable depth or bathymetry source. The advanced workbench remains available
|
||||||
but is not required for the primary choose-theme, draw-area, read-result flow.
|
but is not required for the primary choose-theme, draw-area, read-result flow.
|
||||||
|
|
||||||
The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The active `area_id` constrains every drawn/manual selection to `bbox ∩ Area`; `Volledig werkgebied` uses a bbox enclosing the Area and therefore resolves to the exact persisted geometry. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count.
|
The primary workflow is deliberately short: choose a municipality or the
|
||||||
|
complete region, choose a data theme, drag a rectangle on the MapLibre map and
|
||||||
|
read the resulting evidence. Releasing the drag resolves the coverage zone,
|
||||||
|
reuses every applicable persisted Dataset and bounded-acquires missing
|
||||||
|
operational products with a concurrency ceiling of three provider requests.
|
||||||
|
The active `area_id` constrains every drawn/manual selection to `bbox ∩ Area`;
|
||||||
|
`Volledig werkgebied` uses a bbox enclosing the Area and therefore resolves to
|
||||||
|
the exact persisted geometry. The result panel shows only themes that are
|
||||||
|
applicable and operational for the resolved zone. It reports a real provider
|
||||||
|
failure as `Inladen mislukt`; it never turns an unsupported regional theme into
|
||||||
|
`Bron ontbreekt`. Map rendering remains capped at 1,000 features while
|
||||||
|
`total_feature_count` reports the exact database count.
|
||||||
|
|
||||||
In the Flanders workbench, ruimtebeslag, open ruimte, population density,
|
In Flanders, a bounded selection can combine current NGI administration and
|
||||||
node value and service level may appear as `Op aanvraag`. Drawing a rectangle
|
Statbel population with GRB buildings/roads/water/parcels, the governed
|
||||||
or choosing a municipality uses the existing backend Job/Dataset flow to
|
thematic rasters, DHMV terrain, VMM flood hazard, BWK/Natura 2000, DOV soil and
|
||||||
acquire and persist those five official rasters for the exact selection, then
|
VHA historical profile points. Every acquisition still uses the existing
|
||||||
shows all available semantic metrics together. Identical requests reuse the
|
backend Job/Dataset flow and identical requests reuse the persisted artifact.
|
||||||
persisted artifact. The complete Flanders Area is deliberately unavailable for
|
The complete Flanders Area is deliberately unavailable for monolithic
|
||||||
these rasters because it exceeds the backend safety ceiling; this does not
|
on-demand rasters because it exceeds the backend safety ceiling; this does not
|
||||||
limit vector or partitioned bathymetry analysis of the complete region.
|
limit ordinary rectangle analysis or audited regional partitions.
|
||||||
|
|
||||||
Detection Lab only lists ready imagery rasters. Governed height, flood-hazard
|
Detection Lab only lists ready imagery rasters. Governed height, flood-hazard
|
||||||
and thematic policy rasters remain available in the map explorer but are
|
and thematic policy rasters remain available in the map explorer but are
|
||||||
|
|||||||
@@ -1042,6 +1042,22 @@ export function MapWorkspace({
|
|||||||
limitationMessage: product.limitation_message,
|
limitationMessage: product.limitation_message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
for (const source of officialMapProducts.bathymetry.filter(
|
||||||
|
(item) =>
|
||||||
|
item.key === 'vha_inland_profiles'
|
||||||
|
&& item.acquisition_supported
|
||||||
|
&& item.configured,
|
||||||
|
)) {
|
||||||
|
result.push({
|
||||||
|
kind: 'bathymetry_profiles',
|
||||||
|
productKey: source.key,
|
||||||
|
displayName: source.display_name,
|
||||||
|
theme: 'bathymetry',
|
||||||
|
availabilityLabel: 'historische profielpunten · laad bij selectie',
|
||||||
|
attribution: source.attribution,
|
||||||
|
limitationMessage: source.limitation_message,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const product of officialMapProducts.officialVector.filter((item) =>
|
for (const product of officialMapProducts.officialVector.filter((item) =>
|
||||||
productCoversZones(item.coverage_zones, effectiveZones),
|
productCoversZones(item.coverage_zones, effectiveZones),
|
||||||
@@ -1101,6 +1117,27 @@ export function MapWorkspace({
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}, [onDemandProductsForZones, selectedCoverageZones])
|
}, [onDemandProductsForZones, selectedCoverageZones])
|
||||||
|
const selectionRelevantThemes = useMemo(() => {
|
||||||
|
if (!mapSelectionBbox || !coverage) {
|
||||||
|
return DATA_THEMES
|
||||||
|
}
|
||||||
|
const boundedThemes = new Set(
|
||||||
|
onDemandProductsForZones(coverage.intersected_zones).map((product) => product.theme),
|
||||||
|
)
|
||||||
|
return DATA_THEMES.filter((theme) => {
|
||||||
|
if (boundedThemes.has(theme.id)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!themeDatasetMap[theme.id]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const coverageTheme = COVERAGE_THEME_BY_MAP_THEME[theme.id]
|
||||||
|
return coverage.items.some(
|
||||||
|
(item) => item.theme === coverageTheme && item.status === 'operational',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, [coverage, mapSelectionBbox, onDemandProductsForZones, themeDatasetMap])
|
||||||
|
const unavailableSelectionThemeCount = Math.max(DATA_THEMES.length - selectionRelevantThemes.length, 0)
|
||||||
const activeOnDemandMapProduct = themeDatasetMap[activeTheme.id]
|
const activeOnDemandMapProduct = themeDatasetMap[activeTheme.id]
|
||||||
? null
|
? null
|
||||||
: onDemandProductMap.get(activeTheme.id) ?? null
|
: onDemandProductMap.get(activeTheme.id) ?? null
|
||||||
@@ -1290,6 +1327,10 @@ export function MapWorkspace({
|
|||||||
}),
|
}),
|
||||||
[themeInsights],
|
[themeInsights],
|
||||||
)
|
)
|
||||||
|
const readSelectionThemeCount = useMemo(
|
||||||
|
() => new Set(themeResults.map((result) => result.theme.id)).size,
|
||||||
|
[themeResults],
|
||||||
|
)
|
||||||
const activeThemeInsight = themeResults.find((item) => item.theme.id === activeThemeId)
|
const activeThemeInsight = themeResults.find((item) => item.theme.id === activeThemeId)
|
||||||
const activeResultDataset = activeThemeInsight?.dataset ?? activeThemeDataset
|
const activeResultDataset = activeThemeInsight?.dataset ?? activeThemeDataset
|
||||||
const activeSelectionResult = activeThemeInsight?.result
|
const activeSelectionResult = activeThemeInsight?.result
|
||||||
@@ -1703,7 +1744,7 @@ export function MapWorkspace({
|
|||||||
resolvedZones = resolvedCoverage.intersected_zones
|
resolvedZones = resolvedCoverage.intersected_zones
|
||||||
}
|
}
|
||||||
const resolvedProducts = analysisMode === 'current'
|
const resolvedProducts = analysisMode === 'current'
|
||||||
? onDemandProductsForZones(resolvedZones).filter((product) => product.theme === activeThemeId)
|
? onDemandProductsForZones(resolvedZones)
|
||||||
: []
|
: []
|
||||||
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
|
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
|
||||||
for (const theme of DATA_THEMES) {
|
for (const theme of DATA_THEMES) {
|
||||||
@@ -2389,7 +2430,7 @@ export function MapWorkspace({
|
|||||||
) : mapSelectionLoading || themeResultsLoading || temporalComparisonLoading ? (
|
) : mapSelectionLoading || themeResultsLoading || temporalComparisonLoading ? (
|
||||||
<div className="geo-results-loading" role="status">
|
<div className="geo-results-loading" role="status">
|
||||||
<span />
|
<span />
|
||||||
<strong>Gegevens worden uit PostGIS gelezen…</strong>
|
<strong>Officiële bronnen worden begrensd geladen en geanalyseerd…</strong>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -2498,26 +2539,35 @@ export function MapWorkspace({
|
|||||||
|
|
||||||
<div className="geo-theme-results">
|
<div className="geo-theme-results">
|
||||||
<div className="geo-results-title-row">
|
<div className="geo-results-title-row">
|
||||||
<h4>Alle beschikbare thema’s</h4>
|
<h4>Alle relevante thema’s</h4>
|
||||||
<span>{themeResults.length} bevraagd</span>
|
<span>{readSelectionThemeCount} van {selectionRelevantThemes.length} uitgelezen</span>
|
||||||
</div>
|
</div>
|
||||||
{DATA_THEMES.map((theme) => {
|
{selectionRelevantThemes.flatMap((theme) => {
|
||||||
const dataset = themeDatasetMap[theme.id]
|
const dataset = themeDatasetMap[theme.id]
|
||||||
const item = themeResults.find((result) => result.theme.id === theme.id)
|
const items = themeResults.filter((result) => result.theme.id === theme.id)
|
||||||
return (
|
const rows = items.length > 0 ? items : [null]
|
||||||
<div className="geo-theme-result-row" key={theme.id}>
|
return rows.map((item, index) => {
|
||||||
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
const resultDataset = item?.dataset ?? dataset
|
||||||
<span>
|
return (
|
||||||
<strong>{theme.label}</strong>
|
<div className="geo-theme-result-row" key={`${theme.id}:${resultDataset?.id ?? index}`}>
|
||||||
<small>{dataset ? getDatasetSourceDisplayName(dataset) : 'Geen bron gekoppeld'}</small>
|
<span className={`geo-theme-symbol geo-theme-symbol-${theme.id}`} aria-hidden="true" />
|
||||||
</span>
|
<span>
|
||||||
<span className="geo-theme-result-value">
|
<strong>{theme.label}</strong>
|
||||||
<b>{item ? resultMetricLabel(item.result) : dataset ? 'Niet bevraagd' : 'Bron ontbreekt'}</b>
|
<small>{resultDataset ? getDatasetSourceDisplayName(resultDataset) : 'Officiële bron kon niet worden geladen'}</small>
|
||||||
{item?.result.summary ? <small>{item.result.summary.metric_label}</small> : null}
|
</span>
|
||||||
</span>
|
<span className="geo-theme-result-value">
|
||||||
</div>
|
<b>{item ? resultMetricLabel(item.result) : 'Inladen mislukt'}</b>
|
||||||
)
|
{item?.result.summary ? <small>{item.result.summary.metric_label}</small> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
})}
|
})}
|
||||||
|
{unavailableSelectionThemeCount > 0 ? (
|
||||||
|
<p className="geo-data-notice">
|
||||||
|
{unavailableSelectionThemeCount} thema’s zijn voor deze zone niet van toepassing of hebben nog geen gevalideerde operationele koppeling.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { settleWithConcurrency } from './useMapThemeSelectionInsights'
|
||||||
|
|
||||||
|
describe('settleWithConcurrency', () => {
|
||||||
|
it('keeps result order and never exceeds the acquisition limit', async () => {
|
||||||
|
let active = 0
|
||||||
|
let maximumActive = 0
|
||||||
|
|
||||||
|
const results = await settleWithConcurrency([0, 1, 2, 3, 4, 5], 3, async (value) => {
|
||||||
|
active += 1
|
||||||
|
maximumActive = Math.max(maximumActive, active)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, (5 - value) * 2))
|
||||||
|
active -= 1
|
||||||
|
return value * 10
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(maximumActive).toBe(3)
|
||||||
|
expect(results).toEqual([
|
||||||
|
{ status: 'fulfilled', value: 0 },
|
||||||
|
{ status: 'fulfilled', value: 10 },
|
||||||
|
{ status: 'fulfilled', value: 20 },
|
||||||
|
{ status: 'fulfilled', value: 30 },
|
||||||
|
{ status: 'fulfilled', value: 40 },
|
||||||
|
{ status: 'fulfilled', value: 50 },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retains individual acquisition failures without stopping the queue', async () => {
|
||||||
|
const results = await settleWithConcurrency(['ok', 'fail', 'later'], 2, async (value) => {
|
||||||
|
if (value === 'fail') {
|
||||||
|
throw new Error('provider unavailable')
|
||||||
|
}
|
||||||
|
return value.toUpperCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(results[0]).toEqual({ status: 'fulfilled', value: 'OK' })
|
||||||
|
expect(results[1].status).toBe('rejected')
|
||||||
|
expect(results[2]).toEqual({ status: 'fulfilled', value: 'LATER' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -7,7 +7,13 @@ import { floodHazardSelectionToMapSelection } from '../lib/floodHazardSelection'
|
|||||||
import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
|
import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
|
||||||
import { bathymetryRasterSelectionToMapSelection } from '../lib/bathymetryRaster'
|
import { bathymetryRasterSelectionToMapSelection } from '../lib/bathymetryRaster'
|
||||||
|
|
||||||
export type MapThemeAcquisitionKind = 'thematic_raster' | 'dhmv' | 'flood_hazard' | 'grb' | 'official_vector'
|
export type MapThemeAcquisitionKind =
|
||||||
|
| 'thematic_raster'
|
||||||
|
| 'dhmv'
|
||||||
|
| 'flood_hazard'
|
||||||
|
| 'grb'
|
||||||
|
| 'official_vector'
|
||||||
|
| 'bathymetry_profiles'
|
||||||
|
|
||||||
export interface MapThemeAcquisition {
|
export interface MapThemeAcquisition {
|
||||||
kind: MapThemeAcquisitionKind
|
kind: MapThemeAcquisitionKind
|
||||||
@@ -30,6 +36,31 @@ export interface MapThemeInsight<TThemeId extends string> {
|
|||||||
result: VectorSelectionResponse
|
result: VectorSelectionResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function settleWithConcurrency<T, TResult>(
|
||||||
|
items: T[],
|
||||||
|
concurrency: number,
|
||||||
|
task: (item: T, index: number) => Promise<TResult>,
|
||||||
|
): Promise<Array<PromiseSettledResult<TResult>>> {
|
||||||
|
const results = new Array<PromiseSettledResult<TResult>>(items.length)
|
||||||
|
const workerCount = Math.min(items.length, Math.max(1, Math.floor(concurrency)))
|
||||||
|
let nextIndex = 0
|
||||||
|
|
||||||
|
const runWorker = async () => {
|
||||||
|
while (nextIndex < items.length) {
|
||||||
|
const index = nextIndex
|
||||||
|
nextIndex += 1
|
||||||
|
try {
|
||||||
|
results[index] = { status: 'fulfilled', value: await task(items[index], index) }
|
||||||
|
} catch (reason) {
|
||||||
|
results[index] = { status: 'rejected', reason }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: workerCount }, () => runWorker()))
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
export function useMapThemeSelectionInsights<TThemeId extends string>(
|
export function useMapThemeSelectionInsights<TThemeId extends string>(
|
||||||
selectedProjectId: string | null,
|
selectedProjectId: string | null,
|
||||||
onDatasetsChanged?: () => Promise<unknown>,
|
onDatasetsChanged?: () => Promise<unknown>,
|
||||||
@@ -69,8 +100,10 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
setThemeInsightsLoading(true)
|
setThemeInsightsLoading(true)
|
||||||
setThemeInsightsError(null)
|
setThemeInsightsError(null)
|
||||||
try {
|
try {
|
||||||
const settled = await Promise.allSettled(
|
const settled = await settleWithConcurrency(
|
||||||
queries.map(async ({ themeId, dataset: existingDataset, partitioned, acquisition }) => {
|
queries,
|
||||||
|
3,
|
||||||
|
async ({ themeId, dataset: existingDataset, partitioned, acquisition }) => {
|
||||||
let dataset = existingDataset
|
let dataset = existingDataset
|
||||||
if (acquisition) {
|
if (acquisition) {
|
||||||
const commonPayload = {
|
const commonPayload = {
|
||||||
@@ -98,10 +131,12 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
...commonPayload,
|
...commonPayload,
|
||||||
product_key: acquisition.productKey as 'buildings' | 'roads' | 'water' | 'parcels',
|
product_key: acquisition.productKey as 'buildings' | 'roads' | 'water' | 'parcels',
|
||||||
})
|
})
|
||||||
: await datasetsApi.acquireOfficialVector(selectedProjectId, {
|
: acquisition.kind === 'bathymetry_profiles'
|
||||||
...commonPayload,
|
? await datasetsApi.acquireBathymetryProfiles(selectedProjectId, commonPayload)
|
||||||
product_key: acquisition.productKey,
|
: await datasetsApi.acquireOfficialVector(selectedProjectId, {
|
||||||
})
|
...commonPayload,
|
||||||
|
product_key: acquisition.productKey,
|
||||||
|
})
|
||||||
if (acquisitionJob.status !== 'success' || !acquisitionJob.output_dataset_id) {
|
if (acquisitionJob.status !== 'success' || !acquisitionJob.output_dataset_id) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
acquisitionJob.error_message
|
acquisitionJob.error_message
|
||||||
@@ -166,7 +201,7 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
|
|||||||
limit: 1000,
|
limit: 1000,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
)
|
)
|
||||||
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
|
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
|
||||||
const failures = settled.flatMap((item, index) => (
|
const failures = settled.flatMap((item, index) => (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react'
|
|||||||
import { datasetsApi, externalApi } from '../services/api'
|
import { datasetsApi, externalApi } from '../services/api'
|
||||||
import { formatError } from '../lib/formatError'
|
import { formatError } from '../lib/formatError'
|
||||||
import type {
|
import type {
|
||||||
|
BathymetrySourceRead,
|
||||||
DhmvProductRead,
|
DhmvProductRead,
|
||||||
FloodHazardProductRead,
|
FloodHazardProductRead,
|
||||||
GrbProductRead,
|
GrbProductRead,
|
||||||
@@ -15,6 +16,7 @@ export interface OfficialMapProducts {
|
|||||||
floodHazard: FloodHazardProductRead[]
|
floodHazard: FloodHazardProductRead[]
|
||||||
grb: GrbProductRead[]
|
grb: GrbProductRead[]
|
||||||
officialVector: OfficialVectorProductRead[]
|
officialVector: OfficialVectorProductRead[]
|
||||||
|
bathymetry: BathymetrySourceRead[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_PRODUCTS: OfficialMapProducts = {
|
const EMPTY_PRODUCTS: OfficialMapProducts = {
|
||||||
@@ -23,6 +25,7 @@ const EMPTY_PRODUCTS: OfficialMapProducts = {
|
|||||||
floodHazard: [],
|
floodHazard: [],
|
||||||
grb: [],
|
grb: [],
|
||||||
officialVector: [],
|
officialVector: [],
|
||||||
|
bathymetry: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOfficialMapProducts(selectedProjectId: string | null) {
|
export function useOfficialMapProducts(selectedProjectId: string | null) {
|
||||||
@@ -49,8 +52,9 @@ export function useOfficialMapProducts(selectedProjectId: string | null) {
|
|||||||
datasetsApi.listFloodHazardProducts(selectedProjectId),
|
datasetsApi.listFloodHazardProducts(selectedProjectId),
|
||||||
datasetsApi.listGrbProducts(selectedProjectId),
|
datasetsApi.listGrbProducts(selectedProjectId),
|
||||||
datasetsApi.listOfficialVectorProducts(selectedProjectId),
|
datasetsApi.listOfficialVectorProducts(selectedProjectId),
|
||||||
|
datasetsApi.listBathymetrySources(selectedProjectId),
|
||||||
])
|
])
|
||||||
.then(([thematic, dhmv, floodHazard, grb, officialVector]) => {
|
.then(([thematic, dhmv, floodHazard, grb, officialVector, bathymetry]) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setProducts({
|
setProducts({
|
||||||
thematic: thematic.items,
|
thematic: thematic.items,
|
||||||
@@ -58,6 +62,7 @@ export function useOfficialMapProducts(selectedProjectId: string | null) {
|
|||||||
floodHazard: floodHazard.items,
|
floodHazard: floodHazard.items,
|
||||||
grb: grb.items,
|
grb: grb.items,
|
||||||
officialVector: officialVector.items,
|
officialVector: officialVector.items,
|
||||||
|
bathymetry: bathymetry.items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user