Add governed bathymetry profile workflow
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 15:25:00 +02:00
parent c0cab9e3c5
commit 5b4e18059a
31 changed files with 1990 additions and 4 deletions
+13
View File
@@ -635,3 +635,16 @@ validation remain inside the collapsed follow-up list with an explicit
priority and examples of the measurements they should eventually support.
Theme/time-series detail and active-source limitations are also collapsed by
default so the Data workspace stays readable.
## Bathymetry profile map theme
Ready `vmm_vha_bathymetry_profiles` Datasets activate the `Waterbodem` map
theme. The layer renders persisted profile points through the existing
MapLibre/GeoJSON path. Clicking a point shows the watercourse, profile number,
measurement date, nullable registered depth and an allowlisted official VHA
profile-document link.
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.
The Sources workspace lists the future MDK North Sea and SPW Walloon
bathymetry integrations as planned until backend acquisition is operational.
@@ -19,6 +19,7 @@ const THEME_LABELS: Record<string, string> = {
nature_value: 'Natuurwaarde',
agriculture: 'Landbouw',
water: 'Water',
bathymetry: 'Waterbodem en dwarsprofielen',
flood_hazard: 'Overstromingsgevaar',
elevation: 'Hoogte en reliëf',
roads: 'Wegen en transport',
@@ -81,6 +82,9 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
)
const dhmvDatasets = ready.filter((dataset) => dataset.source_name === 'digitaal_vlaanderen_dhmv')
const floodHazardDatasets = ready.filter((dataset) => dataset.source_name === 'vmm_flood_hazard')
const bathymetryProfileDatasets = ready.filter(
(dataset) => dataset.source_name === 'vmm_vha_bathymetry_profiles',
)
const latestBuildingsRegister = [...buildingsRegisterDatasets].sort(
(left, right) => new Date(right.observed_at ?? 0).getTime() - new Date(left.observed_at ?? 0).getTime(),
)[0]
@@ -184,7 +188,7 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
</div>
</details>
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 || dhmvDatasets.length > 0 || floodHazardDatasets.length > 0 ? (
{waterinfoDatasets.length > 0 || historicalOrthophotos.length > 0 || bwkDatasets.length > 0 || agricultureDatasets.length > 0 || buildingsRegisterDatasets.length > 0 || dhmvDatasets.length > 0 || floodHazardDatasets.length > 0 || bathymetryProfileDatasets.length > 0 ? (
<details className="source-loaded-disclosure">
<summary>Actieve broncollecties en hun beperkingen</summary>
<div className="source-catalog-loaded" aria-label="Aanvullende ingeladen bronnen">
@@ -244,6 +248,18 @@ export function SourceCatalogPanel({ datasets }: SourceCatalogPanelProps): JSX.E
<p>Gemodelleerde maximumdiepte per kansscenario. Geen actuele waterstand, bathymetrie of permanent watervolume.</p>
</article>
) : null}
{bathymetryProfileDatasets.length > 0 ? (
<article>
<strong>VHA-dwarsprofielen waterbodem</strong>
<span>
{bathymetryProfileDatasets.reduce(
(total, dataset) => total + Number(dataset.feature_count ?? 0),
0,
).toLocaleString('nl-BE')} profielpunten · historische meetcampagnes
</span>
<p>Meetvelden en officiële profielbladen per punt. Geen continue actuele bathymetrie of volume zonder een gelijktijdig waterpeil.</p>
</article>
) : null}
</div>
</details>
) : null}
+59 -1
View File
@@ -36,7 +36,7 @@ const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
type DataThemeId = 'buildings' | 'space_occupation' | 'open_space' | 'population' | 'forest' | 'nature_value' | 'agriculture' | 'soil' | 'water' | 'flood_hazard' | 'elevation' | 'accessibility' | 'services' | 'roads' | 'parcels'
type DataThemeId = 'buildings' | 'space_occupation' | 'open_space' | 'population' | 'forest' | 'nature_value' | 'agriculture' | 'soil' | 'water' | 'bathymetry' | 'flood_hazard' | 'elevation' | 'accessibility' | 'services' | 'roads' | 'parcels'
interface DataTheme {
id: DataThemeId
@@ -116,6 +116,13 @@ const DATA_THEMES: DataTheme[] = [
description: 'Waterlopen, grachten, kanalen en wateroppervlakken.',
tokens: ['waterways', 'waterway', 'water', 'hydro', 'river', 'stream', 'canal', 'waterloop'],
},
{
id: 'bathymetry',
label: 'Waterbodem',
shortLabel: 'Dwarsprofielen',
description: 'Officiële historische VHA-dwarsprofielen met meetvelden en brondocumenten.',
tokens: ['bathymetry', 'bathymetry_profiles', 'dwarsprofielen', 'waterbodem'],
},
{
id: 'flood_hazard',
label: 'Overstroming',
@@ -170,6 +177,7 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
agriculture: { fill: '#7b8f32', line: '#53671d' },
soil: { fill: '#9a7040', line: '#6f4c27' },
water: { fill: '#2676a8', line: '#155b85' },
bathymetry: { fill: '#0e7490', line: '#164e63' },
flood_hazard: { fill: '#1597c2', line: '#075985' },
elevation: { fill: '#a57a4b', line: '#315f59' },
accessibility: { fill: '#0f766e', line: '#115e59' },
@@ -188,6 +196,11 @@ function datasetAvailabilityLabel(dataset: DatasetCreateResponse, partitionCount
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} overstromingsscenario${regionalSuffix}`
}
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
const profiles = dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0
const documents = Number(dataset.source_metadata?.['document_count'] ?? 0)
return `${profiles.toLocaleString('nl-BE')} profielen · ${documents.toLocaleString('nl-BE')} bronbladen`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const year = Number(dataset.source_metadata?.['observation_year'])
@@ -221,6 +234,9 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
if (dataset.source_name === 'vmm_flood_hazard') {
return theme.id === 'flood_hazard'
}
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
return theme.id === 'bathymetry'
}
if (dataset.source_name === 'digitaal_vlaanderen_dhmv') {
return theme.id === 'elevation'
}
@@ -310,6 +326,7 @@ function pickThemeDataset(
(dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register' ? 5_000_000 : 0) +
(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_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)
@@ -618,6 +635,12 @@ export function MapWorkspace({
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
const regionalScopeSelected = Boolean(selectedMapArea && !isMunicipalityAreaName(selectedMapArea.name))
const featureProperties = selectedMapFeature?.properties ?? null
const isBathymetryProfile = featureProperties?.['provider'] === 'vmm_vha_bathymetry_profiles'
|| featureProperties?.['measurement_semantics'] === 'historical_cross_section_profile_point'
const bathymetryDocumentUrl = typeof featureProperties?.['source_document_url'] === 'string'
&& featureProperties['source_document_url'].startsWith('https://vha.waterinfo.be/')
? featureProperties['source_document_url']
: null
const featureSummaryEntries = featureProperties
? Object.entries(featureProperties)
.filter(([, value]) => value !== null && value !== undefined && typeof value !== 'object')
@@ -2555,6 +2578,41 @@ export function MapWorkspace({
</div>
{selectedMapFeature ? (
<>
{isBathymetryProfile ? (
<div className="bathymetry-profile-summary" aria-label="Samenvatting VHA-dwarsprofiel">
<div>
<span>Waterloop</span>
<strong>{String(featureProperties?.['watercourse_name'] ?? 'Onbekende waterloop')}</strong>
</div>
<div>
<span>Profiel</span>
<strong>{String(featureProperties?.['profile_number'] ?? 'n.v.t.')}</strong>
</div>
<div>
<span>Meetdatum</span>
<strong>{String(featureProperties?.['measurement_date'] ?? 'Niet geregistreerd')}</strong>
</div>
<div>
<span>Geregistreerde diepte</span>
<strong>
{typeof featureProperties?.['recorded_depth_m'] === 'number'
? `${featureProperties['recorded_depth_m'].toLocaleString('nl-BE')} m`
: 'Niet als veld beschikbaar'}
</strong>
</div>
{bathymetryDocumentUrl ? (
<a href={bathymetryDocumentUrl} target="_blank" rel="noreferrer">
Officieel profielblad openen
</a>
) : (
<small>Voor dit meetpunt is geen digitaal profielblad gekoppeld.</small>
)}
<p>
Historisch dwarsprofiel. Dit punt is geen continue actuele bodemkaart en levert zonder
gelijktijdig waterpeil geen actueel watervolume.
</p>
</div>
) : null}
<div className="feature-extract-grid" aria-label="Geometrie van het geselecteerde object">
<div>
<span>Geometrie</span>
+5
View File
@@ -4,6 +4,7 @@ const DATASET_LABEL_BY_LAYER: Record<string, string> = {
buildings: 'Bebouwing',
roads: 'Wegen',
water: 'Water',
bathymetry_profiles: 'Dwarsprofielen waterbodem',
parcels: 'Percelen',
population: 'Bevolking',
forest: 'Bos en groen',
@@ -34,6 +35,7 @@ const DATASET_SOURCE_LABELS: Record<string, string> = {
vrbg: 'Digitaal Vlaanderen',
waterinfo: 'Waterinfo Vlaanderen',
vmm_flood_hazard: 'Vlaamse Milieumaatschappij',
vmm_vha_bathymetry_profiles: 'VMM / Vlaamse Hydrografische Atlas',
department_omgeving_thematic_raster: 'Departement Omgeving',
dov_soil_map: 'Databank Ondergrond Vlaanderen',
}
@@ -52,6 +54,9 @@ export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
const productName = dataset.source_metadata?.['product_display_name']
return typeof productName === 'string' && productName.trim() ? productName : 'VMM-overstromingsscenario'
}
if (dataset.source_name === 'vmm_vha_bathymetry_profiles') {
return 'VHA-dwarsprofielen waterbodem'
}
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'
+36
View File
@@ -384,6 +384,42 @@ export const OFFICIAL_SOURCE_PORTFOLIO: OfficialSourceDefinition[] = [
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/ogc-api-features-vlaamse-hydrografische-atlas-waterlopen',
matches: (dataset) => sourceNameIs(dataset, 'vmm_vha'),
},
{
key: 'vha_bathymetry_profiles',
domain: 'climate',
name: 'VHA-dwarsprofielen waterbodem',
owner: 'Vlaamse Milieumaatschappij',
coverage: 'Historische profielpunten op Vlaamse waterlopen',
value: 'Gemeten profielinformatie en officiële bronbladen op exacte kaartlocaties.',
metricExamples: 'aantal profielen, geregistreerde diepte en breedte per meetpunt',
priority: 'next',
url: 'https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer',
matches: (dataset) => sourceNameIs(dataset, 'vmm_vha_bathymetry_profiles'),
},
{
key: 'mdk_bcp_bathymetry',
domain: 'climate',
name: 'Dieptemodel Belgisch Continentaal Plat',
owner: 'Agentschap Maritieme Dienstverlening en Kust',
coverage: 'Belgische Noordzee, 20 x 20 m raster in LAT',
value: 'Continue zeebodemhoogte voor mariene selectie en evolutie.',
metricExamples: 'minimum/maximum diepte, diepteklassen en bodemvolume per peilcampagne',
priority: 'planned',
url: 'https://www.vlaanderen.be/datavindplaats/catalogus/dieptemodel-van-de-zeebodem-belgisch-continentaal-plat-noordzee',
matches: (dataset) => sourceNameIs(dataset, 'mdk_bcp_bathymetry'),
},
{
key: 'spw_waterway_bathymetry',
domain: 'climate',
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'),
},
]
export function operationalSources(
+8
View File
@@ -22,6 +22,8 @@ import type {
FloodHazardAcquireRequest,
FloodHazardProductRead,
FloodHazardSelectionResponse,
BathymetryProfileAcquireRequest,
BathymetrySourceRead,
DhmvProductRead,
TerrainSelectionResponse,
ThematicRasterAcquireRequest,
@@ -167,6 +169,12 @@ export const datasetsApi = {
payload: { bbox: VectorSelectionRequest['bbox']; area_id?: string; product_key: string },
): Promise<FloodHazardSelectionResponse> =>
apiPost<FloodHazardSelectionResponse>(`/api/v1/projects/${projectId}/datasets/raster/flood-hazard/select`, payload),
listBathymetrySources: (projectId: string): Promise<{ items: BathymetrySourceRead[]; total: number }> =>
apiGet<{ items: BathymetrySourceRead[]; total: number }>(
`/api/v1/projects/${projectId}/datasets/bathymetry/sources`,
),
acquireBathymetryProfiles: (projectId: string, payload: BathymetryProfileAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/bathymetry/profiles/acquire`, 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 }> =>
+59
View File
@@ -5966,6 +5966,11 @@ section {
background: rgba(38, 118, 168, 0.24);
}
.geo-map-legend .geo-legend-layer-bathymetry {
background: #0e7490;
border-color: #164e63;
}
.geo-map-legend .geo-legend-layer-soil {
border-color: #6f4c27;
background: rgba(154, 112, 64, 0.24);
@@ -6036,6 +6041,60 @@ section {
.geo-legend-ramp-population { background: linear-gradient(90deg, #eee7f6, #673a97); }
.geo-legend-ramp-accessibility { background: linear-gradient(90deg, #e9f1f4, #0f766e); }
.geo-legend-ramp-services { background: linear-gradient(90deg, #fff4bf, #b66d16); }
.geo-legend-ramp-bathymetry { background: linear-gradient(90deg, #cffafe, #0e7490); }
.bathymetry-profile-summary {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
margin-bottom: 0.9rem;
padding: 0.8rem;
border: 1px solid #a5d8e6;
border-radius: 6px;
background: #effbff;
}
.bathymetry-profile-summary > div {
display: grid;
gap: 0.2rem;
min-width: 0;
}
.bathymetry-profile-summary span,
.bathymetry-profile-summary small {
color: #46646f;
font-size: 0.75rem;
}
.bathymetry-profile-summary strong {
overflow-wrap: anywhere;
color: #113d4b;
}
.bathymetry-profile-summary a,
.bathymetry-profile-summary p,
.bathymetry-profile-summary small {
grid-column: 1 / -1;
}
.bathymetry-profile-summary a {
width: fit-content;
color: #075985;
font-weight: 700;
}
.bathymetry-profile-summary p {
margin: 0;
color: #315867;
font-size: 0.78rem;
line-height: 1.45;
}
@media (max-width: 760px) {
.bathymetry-profile-summary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.geo-map-legend .geo-legend-added {
border-color: #15803d;
+27
View File
@@ -442,6 +442,33 @@ export interface FloodHazardSelectionResponse {
generated_at: string
}
export interface BathymetryProfileAcquireRequest {
bbox: VectorSelectionBBox
area_id?: string | null
force_refresh?: boolean
}
export interface BathymetrySourceRead {
key: string
display_name: string
owner: string
authority_level: 'authoritative' | 'contextual'
geographic_coverage: string
data_kind: string
query_modes: string[]
vertical_reference: string
horizontal_crs: string
native_resolution?: string | null
integration_status: 'operational' | 'available_not_integrated' | 'catalog_only'
acquisition_supported: boolean
configured: boolean
service_url?: string | null
catalog_url: string
attribution: string
license_note: string
limitation_message: string
}
export interface ThematicRasterAcquireRequest {
bbox: VectorSelectionBBox
area_id?: string | null