Add governed SPW bathymetry analysis
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 04:07:01 +02:00
parent ae3d3dc634
commit 70c5e34ecf
37 changed files with 1852 additions and 102 deletions
+6 -2
View File
@@ -695,8 +695,12 @@ The UI always labels the points as historical cross-sections. It does not
present them as a continuous bed raster and does not calculate water volume.
Municipality partitions remain hidden for a regional Area until their backend
manifest reports complete coverage. The Sources workspace lists MDK North Sea
as read-only probe-only and SPW Walloon bathymetry as planned until bounded
raster/download acquisition is operational.
as read-only probe-only. Ready `spw_bathymetry` rasters activate the same
`Waterbodem` theme for Wallonia with a bounded MapLibre image overlay and
selection metrics for mDNG waterbed height, measured surface and source
coverage. SPW import remains an explicit backend operator workflow; the
browser never downloads the source archive. Missing water level, depth,
volume and vertical-datum conversion remain clearly unsupported.
For a complete regional manifest, the theme card totals all data-bearing
municipality partitions instead of displaying one representative partition.
+16 -6
View File
@@ -212,7 +212,10 @@ function App(): JSX.Element {
const thematicRasters = datasets.filter(
(dataset) => dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster' && dataset.status === 'ready',
)
return [...vectors, ...terrain, ...floodHazards, ...thematicRasters]
const bathymetryRasters = datasets.filter(
(dataset) => dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry' && dataset.status === 'ready',
)
return [...vectors, ...terrain, ...floodHazards, ...thematicRasters, ...bathymetryRasters]
},
[datasets],
)
@@ -694,12 +697,17 @@ function App(): JSX.Element {
? FLANDERS_WORKSPACE_LABEL
: selectedProject?.name ?? 'Geen werkruimte'
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied')
const bathymetryContextActive = Boolean(
const bathymetryProfileContextActive = Boolean(
activeWorkspace === 'map'
&& selectedDataset?.source_name === 'vmm_vha_bathymetry_profiles',
)
const bathymetryRasterContextActive = Boolean(
activeWorkspace === 'map'
&& selectedDataset?.source_name === 'spw_bathymetry',
)
const bathymetryContextActive = bathymetryProfileContextActive || bathymetryRasterContextActive
const regionalBathymetryContextActive = Boolean(
bathymetryContextActive
bathymetryProfileContextActive
&& selectedDataset?.source_metadata?.regional_partitions_complete === true
&& selectedArea
&& !/^Gemeente\s/i.test(selectedArea.name),
@@ -744,9 +752,11 @@ function App(): JSX.Element {
const layerContextLabel = activeWorkspace === 'map' && mapContextLayerLabel
? mapContextLayerLabel
: bathymetryContextActive
? regionalBathymetryContextActive
? `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen · ${regionalBathymetryPartitions.length} gemeenten`
: `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen`
? bathymetryRasterContextActive
? `${Number(selectedDataset?.source_metadata?.analysis_resolution_m ?? 0.5).toLocaleString('nl-BE')} m raster · mDNG · ${String(selectedDataset?.source_metadata?.survey_period ?? '2019-2022')}`
: regionalBathymetryContextActive
? `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen · ${regionalBathymetryPartitions.length} gemeenten`
: `${bathymetryProfileCount.toLocaleString('nl-BE')} profielen`
: mapFeatureCollection
? `${mapFeatureCount.toLocaleString('nl-BE')} objecten`
: workspaceDataLoading
+33 -4
View File
@@ -9,6 +9,7 @@ import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl } from '../../lib/thematicRaster'
import { bathymetryRasterImageUrl } from '../../lib/bathymetryRaster'
import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus'
import {
MAP_ANALYSIS_BUDGET_MS,
@@ -306,6 +307,11 @@ function datasetAvailabilityLabel(
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const period = String(dataset.source_metadata?.['survey_period'] ?? '2019-2022')
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} waterbodemhoogte · ${period}`
}
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
const profiles = partitions.reduce(
(total, item) => total + (item.feature_count ?? item.vector_summary?.feature_count ?? 0),
@@ -362,6 +368,9 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
return theme.id === 'bathymetry'
}
if (dataset.source_name === 'spw_bathymetry') {
return theme.id === 'bathymetry'
}
if (dataset.source_name === 'digitaal_vlaanderen_dhmv') {
return theme.id === 'elevation'
}
@@ -492,6 +501,7 @@ function pickThemeDataset(
(dataset.source_name === 'digitaal_vlaanderen_dhmv' ? 5_000_000 : 0) +
(dataset.source_name === 'vmm_flood_hazard' ? 5_000_000 : 0) +
(dataset.source_name === 'vmm_vha_bathymetry_profiles' ? 5_000_000 : 0) +
(dataset.source_name === 'spw_bathymetry' ? 5_100_000 : 0) +
(dataset.source_metadata?.['product_key'] === 'dtm_1m' ? 1_000_000 : 0) +
(dataset.source_metadata?.['product_key'] === 'pluviaal_current_t100' ? 1_000_000 : 0) +
(dataset.dataset_role === 'reference' ? 10_000 : 0)
@@ -594,6 +604,9 @@ function formatDatasetObservation(dataset: DatasetCreateResponse): string {
? `historische profielen ${firstMeasurement}-${lastMeasurement}`
: 'historische profielmetingen'
}
if (dataset.source_name === 'spw_bathymetry') {
return `samengestelde waterbodemmeting ${String(dataset.source_metadata?.['survey_period'] ?? '2019-2022')} · mDNG`
}
if (dataset.source_name === 'department_omgeving_thematic_raster') {
const observationYear = Number(dataset.source_metadata?.['observation_year'])
if (Number.isFinite(observationYear)) {
@@ -1206,17 +1219,33 @@ export function MapWorkspace({
: [],
[activeThemeDataset, selectedProjectId, thematicRasterBounds],
)
const bathymetryRasterBounds = activeThemeDataset?.source_name === 'spw_bathymetry'
? activeThemeDataset.source_metadata?.['bbox_epsg4326']
: null
const bathymetryRasterImageOverlays = useMemo(
() => activeThemeDataset?.source_name === 'spw_bathymetry' && selectedProjectId && Array.isArray(bathymetryRasterBounds) && bathymetryRasterBounds.length === 4
? [{
url: bathymetryRasterImageUrl(selectedProjectId, activeThemeDataset.id),
bbox: bathymetryRasterBounds.map(Number) as [number, number, number, number],
label: 'Waterbodemhoogte in mDNG',
opacity: 0.86,
}]
: [],
[activeThemeDataset, bathymetryRasterBounds, selectedProjectId],
)
const thematicLegendMin = String(activeThemeDataset?.source_metadata?.['legend_min_label'] ?? 'Lagere waarde')
const thematicLegendMax = String(activeThemeDataset?.source_metadata?.['legend_max_label'] ?? 'Hogere waarde')
const activeImageOverlays = useMemo(
() => thematicRasterImageOverlays.length > 0
? thematicRasterImageOverlays
: floodHazardImageOverlays.length > 0
() => bathymetryRasterImageOverlays.length > 0
? bathymetryRasterImageOverlays
: thematicRasterImageOverlays.length > 0
? thematicRasterImageOverlays
: floodHazardImageOverlays.length > 0
? floodHazardImageOverlays
: terrainImageOverlays.length > 0
? terrainImageOverlays
: orthophotoImageOverlay ? [orthophotoImageOverlay] : [],
[floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays],
[bathymetryRasterImageOverlays, floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays],
)
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const themeTemporalSeriesMap = useMemo(
+8 -1
View File
@@ -5,6 +5,7 @@ import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionRespons
import { terrainSelectionToMapSelection } from '../lib/terrainSelection'
import { floodHazardSelectionToMapSelection } from '../lib/floodHazardSelection'
import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
import { bathymetryRasterSelectionToMapSelection } from '../lib/bathymetryRaster'
interface MapSelectionExtractOptions {
selectedProjectId: string | null
@@ -44,7 +45,8 @@ export function useMapSelectionExtract({
const terrainDataset = selectedDataset.dataset_type === 'raster' && selectedDataset.source_name === 'digitaal_vlaanderen_dhmv'
const floodHazardDataset = selectedDataset.dataset_type === 'raster' && selectedDataset.source_name === 'vmm_flood_hazard'
const thematicRasterDataset = selectedDataset.dataset_type === 'raster' && selectedDataset.source_name === 'department_omgeving_thematic_raster'
if (!isVectorDatasetType(selectedDataset.dataset_type) && !terrainDataset && !floodHazardDataset && !thematicRasterDataset) {
const bathymetryRasterDataset = selectedDataset.dataset_type === 'raster' && selectedDataset.source_name === 'spw_bathymetry'
if (!isVectorDatasetType(selectedDataset.dataset_type) && !terrainDataset && !floodHazardDataset && !thematicRasterDataset && !bathymetryRasterDataset) {
setMapSelectionError('Gebiedsanalyse ondersteunt een vectorlaag of een beheerd thematisch raster.')
return null
}
@@ -70,6 +72,11 @@ export function useMapSelectionExtract({
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
}))
: bathymetryRasterDataset
? bathymetryRasterSelectionToMapSelection(await datasetsApi.selectBathymetryRaster(selectedProjectId, selectedDataset.id, {
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
}))
: await datasetsApi.selectVectorFeatures(selectedProjectId, selectedDataset.id, {
bbox: { ...bbox, crs: 'EPSG:4326' },
area_id: areaId,
@@ -5,6 +5,7 @@ import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionRespons
import { terrainSelectionToMapSelection } from '../lib/terrainSelection'
import { floodHazardSelectionToMapSelection } from '../lib/floodHazardSelection'
import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
import { bathymetryRasterSelectionToMapSelection } from '../lib/bathymetryRaster'
export type MapThemeAcquisitionKind = 'thematic_raster' | 'dhmv' | 'flood_hazard' | 'grb' | 'official_vector'
@@ -148,6 +149,11 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
bbox,
area_id: areaId,
}))
: dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry'
? bathymetryRasterSelectionToMapSelection(await datasetsApi.selectBathymetryRaster(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}))
: dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned
? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, {
bbox,
+52
View File
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import {
bathymetryRasterImageUrl,
bathymetryRasterSelectionToMapSelection,
} from './bathymetryRaster'
describe('bathymetry raster mapping', () => {
it('uses the governed image endpoint', () => {
expect(bathymetryRasterImageUrl('project-1', 'dataset-1')).toBe(
'/api/v1/projects/project-1/datasets/dataset-1/raster/bathymetry/image',
)
})
it('maps persisted bed-elevation metrics without turning them into object counts', () => {
const mapped = bathymetryRasterSelectionToMapSelection({
dataset_id: 'dataset-1',
product_key: 'spw_bathymetry_50cm_mdng',
selection_bbox: { min_x: 4.85, min_y: 50.45, max_x: 4.87, max_y: 50.47, crs: 'EPSG:4326' },
selected_cell_count: 100,
valid_cell_count: 25,
coverage_ratio: 0.25,
resolution_m: 0.5,
vertical_reference: 'mDNG',
survey_period: '2019-2022',
summary: {
metric_label: 'Gemiddelde waterbodemhoogte',
metric_value: 74.5,
metric_unit: 'm mDNG',
aggregation_method: 'mean_valid_source_cells',
primary_metric_key: 'bed_elevation_mean_m',
metrics: [{
metric_key: 'bed_elevation_mean_m',
metric_label: 'Gemiddelde waterbodemhoogte',
metric_value: 74.5,
metric_unit: 'm mDNG',
aggregation_method: 'mean_valid_source_cells',
is_estimate: false,
}],
},
unsupported_metrics: ['current_water_depth_m', 'water_volume_m3'],
limitation_message: 'Geen gelijktijdig waterpeil.',
generated_at: '2026-07-19T00:00:00Z',
})
expect(mapped.feature_count).toBe(25)
expect(mapped.geojson.features).toEqual([])
expect(mapped.summary?.primary_metric_key).toBe('bed_elevation_mean_m')
expect(mapped.summary?.metric_unit).toBe('m mDNG')
expect(mapped.summary?.is_estimate).toBe(false)
expect(mapped.summary?.warning).toContain('waterpeil')
})
})
+29
View File
@@ -0,0 +1,29 @@
import type { BathymetryRasterSelectionResponse, VectorSelectionResponse } from '../types'
export function bathymetryRasterImageUrl(projectId: string, datasetId: string): string {
return `/api/v1/projects/${projectId}/datasets/${datasetId}/raster/bathymetry/image`
}
export function bathymetryRasterSelectionToMapSelection(
result: BathymetryRasterSelectionResponse,
): VectorSelectionResponse {
return {
selection_bbox: result.selection_bbox,
selection_area_id: result.selection_area_id,
feature_count: result.valid_cell_count,
total_feature_count: result.valid_cell_count,
limit: 0,
truncated: false,
geojson: { type: 'FeatureCollection', features: [] },
summary: {
...result.summary,
feature_count: result.valid_cell_count,
is_estimate: false,
warning: result.limitation_message,
metrics: result.summary.metrics.map((metric) => ({
...metric,
is_estimate: false,
})),
},
}
}
+1
View File
@@ -4,6 +4,7 @@ const NON_IMAGERY_RASTER_SOURCES = new Set([
'department_omgeving_thematic_raster',
'digitaal_vlaanderen_dhmv',
'vmm_flood_hazard',
'spw_bathymetry',
])
export function isDetectionImageryDataset(dataset: DatasetCreateResponse): boolean {
+4
View File
@@ -36,6 +36,7 @@ const DATASET_SOURCE_LABELS: Record<string, string> = {
waterinfo: 'Waterinfo Vlaanderen',
vmm_flood_hazard: 'Vlaamse Milieumaatschappij',
vmm_vha_bathymetry_profiles: 'VMM / Vlaamse Hydrografische Atlas',
spw_bathymetry: 'Service public de Wallonie',
department_omgeving_thematic_raster: 'Departement Omgeving',
dov_soil_map: 'Databank Ondergrond Vlaanderen',
}
@@ -57,6 +58,9 @@ export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
return 'VHA-dwarsprofielen waterbodem'
}
if (dataset.source_name === 'spw_bathymetry') {
return 'SPW waterbodemhoogte 0,5 m'
}
if (dataset.source_name === 'department_omgeving_thematic_raster') {
const productName = dataset.source_metadata?.['product_display_name']
return typeof productName === 'string' && productName.trim() ? productName : 'Officieel Vlaams themaraster'
+5 -5
View File
@@ -414,11 +414,11 @@ export const OFFICIAL_SOURCE_PORTFOLIO: OfficialSourceDefinition[] = [
name: 'Waalse vaarweg- en stuwmeerbathymetrie',
owner: 'Service public de Wallonie',
coverage: 'Gemeten Waalse vaarwegen en stuwmeren, 0,5 m in mDNG',
value: 'Hoogwaardige bodemrasters voor federale uitbreiding buiten Vlaanderen.',
metricExamples: 'bodemhoogte, diepteprofiel en vergelijkbare meetcampagnes',
priority: 'planned',
url: 'https://geoportail.wallonie.be/catalogue/c450c28f-d357-48af-8423-62d524632cf9.html',
matches: (dataset) => sourceNameIs(dataset, 'spw_walloon_waterway_bathymetry'),
value: 'Officiële waterbodemhoogtes voor begrensde analyse buiten Vlaanderen.',
metricExamples: 'bodemhoogte, gemeten oppervlakte en brondekking',
priority: 'next',
url: 'https://geoportail.wallonie.be/catalogue/0a544b42-0b30-4c8e-85e7-38149b99eae0.html',
matches: (dataset) => sourceNameIs(dataset, 'spw_bathymetry'),
},
]
+10
View File
@@ -23,6 +23,7 @@ import type {
FloodHazardProductRead,
FloodHazardSelectionResponse,
BathymetryProfileAcquireRequest,
BathymetryRasterSelectionResponse,
BathymetrySourceProbeRead,
BathymetrySourceRead,
DhmvProductRead,
@@ -206,6 +207,15 @@ export const datasetsApi = {
`/api/v1/projects/${projectId}/datasets/bathymetry/profiles/partitions/select`,
payload,
),
selectBathymetryRaster: (
projectId: string,
datasetId: string,
payload: { bbox: VectorSelectionRequest['bbox']; area_id?: string },
): Promise<BathymetryRasterSelectionResponse> =>
apiPost<BathymetryRasterSelectionResponse>(
`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/bathymetry/select`,
payload,
),
acquireThematicRaster: (projectId: string, payload: ThematicRasterAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/thematic-raster/acquire`, payload),
listThematicRasterProducts: (projectId: string): Promise<{ items: ThematicRasterProductRead[]; total: number }> =>
+24
View File
@@ -542,6 +542,30 @@ export interface BathymetrySourceProbeRead {
limitation_message: string
}
export interface BathymetryRasterSelectionResponse {
dataset_id: string
product_key: 'spw_bathymetry_50cm_mdng'
selection_bbox: VectorSelectionBBox
selection_area_id?: string | null
selected_cell_count: number
valid_cell_count: number
coverage_ratio: number
resolution_m: number
vertical_reference: 'mDNG'
survey_period: string
summary: {
metric_label: string
metric_value: number
metric_unit: string
aggregation_method: string
primary_metric_key: string
metrics: VectorSelectionMetric[]
}
unsupported_metrics: string[]
limitation_message: string
generated_at: string
}
export interface ThematicRasterAcquireRequest {
bbox: VectorSelectionBBox
area_id?: string | null