Add governed SPW bathymetry analysis
This commit is contained in:
+6
-2
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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 }> =>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user