feat: complete Wallonia land cover and terrain sources
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-22 06:28:46 +02:00
parent d45d34186d
commit cee6cd05ae
46 changed files with 2542 additions and 276 deletions
@@ -45,6 +45,7 @@ const ANALYTICAL_SOURCE_NAMES = new Set([
'agentschap_landbouw_zeevisserij_agricultural_parcels',
'dov_soil_map',
'digitaal_vlaanderen_dhmv',
'spw_terrain',
'vmm_flood_hazard',
'vmm_vha_bathymetry_profiles',
])
@@ -100,7 +100,7 @@ export function SourceCatalogPanel({
const buildingsRegisterDatasets = ready.filter(
(dataset) => dataset.source_name === 'digitaal_vlaanderen_buildings_addresses_register',
)
const dhmvDatasets = ready.filter((dataset) => dataset.source_name === 'digitaal_vlaanderen_dhmv')
const dhmvDatasets = ready.filter((dataset) => ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? ''))
const floodHazardDatasets = ready.filter((dataset) => dataset.source_name === 'vmm_flood_hazard')
const bathymetryProfileDatasets = ready.filter(
(dataset) => dataset.source_name === 'vmm_vha_bathymetry_profiles',
@@ -310,11 +310,11 @@ export function DetectionLab({
<p className="ai-quality-guidance">
{detectionQualityInterpretation(selectedOperatorProfile?.f1)}
</p>
{selectedOperatorProfile && !selectedOperatorProfile.nationallyValidated ? (
{selectedOperatorProfile && selectedDetectionModel?.nationally_validated !== true ? (
<div className="result-state result-state-warning" role="status">
<strong>Nog niet nationaal gevalideerd</strong>
<p>
Dit model is operationeel voor gecontroleerde beeldanalyse, maar de gemeten kwaliteit geldt alleen voor {selectedOperatorProfile.validationScope}.
Dit model is operationeel voor gecontroleerde beeldanalyse, maar de gemeten kwaliteit geldt alleen voor {selectedDetectionModel?.validation_scope ?? selectedOperatorProfile.validationScope}.
Resultaten elders in Belgie of op zee vereisen lokale referentiedata en QA voordat ze als betrouwbaar kunnen worden vrijgegeven.
</p>
</div>
@@ -11,7 +11,6 @@ export interface DetectionOperatorProfile {
positiveSampleCount: number
maxBackgroundDetections: number
validationScope: string
nationallyValidated: boolean
description: string
limitationMessage: string
}
@@ -30,7 +29,6 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
positiveSampleCount: 7,
maxBackgroundDetections: 0,
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
nationallyValidated: false,
description:
'Aanbevolen profiel met een evenwicht tussen gevonden en gemiste kleine gebouwen, opnieuw gemeten over zeven onafhankelijke testgebieden in Mol en de Kempen.',
limitationMessage:
@@ -49,7 +47,6 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
positiveSampleCount: 7,
maxBackgroundDetections: 0,
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
nationallyValidated: false,
description: 'Voorgaand profiel voor controles waarbij minder foutieve vondsten belangrijker zijn dan maximale dekking.',
limitationMessage:
'De lege-achtergrondtest is geslaagd. Dit profiel vindt minder onterechte objecten, maar mist meer kleine gebouwen dan het aanbevolen profiel.',
@@ -67,7 +64,6 @@ export const DETECTION_OPERATOR_PROFILES: DetectionOperatorProfile[] = [
positiveSampleCount: 7,
maxBackgroundDetections: 0,
validationScope: '7 onafhankelijke testgebieden in Mol en de Kempen',
nationallyValidated: false,
description: 'Profiel met hoge precisie voor controles waarbij zo weinig mogelijk foutieve vondsten zwaarder wegen dan volledige dekking.',
limitationMessage:
'Goedgekeurd na de lege-achtergrondtest. Resultaten in dun bebouwde context blijven altijd controlebewijs en geen automatische waarheid.',
+39 -8
View File
@@ -108,7 +108,7 @@ function productSupportsSelection(product: OnDemandMapProduct, bbox: VectorSelec
const scale = selectionAnalysisScale(bbox)
if (scale === 'overview') return false
const dimensions = selectionDimensions(bbox)
if (product.kind === 'dhmv' || product.kind === 'flood_hazard') {
if (product.kind === 'dhmv' || product.kind === 'spw_terrain' || product.kind === 'flood_hazard') {
return dimensions.areaSquareMetres <= 280_000_000
}
if (product.kind === 'thematic_raster' || product.kind === 'walous') {
@@ -338,7 +338,7 @@ function datasetAvailabilityLabel(
): string {
const partitionCount = partitions.length
const regionalSuffix = partitionCount > 1 ? ` · ${partitionCount} gemeenten` : ''
if (dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv') {
if (dataset.dataset_type === 'raster' && ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
return `${Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'} hoogtegrid${regionalSuffix}`
}
@@ -416,7 +416,7 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
if (dataset.source_name === 'spw_bathymetry') {
return theme.id === 'bathymetry'
}
if (dataset.source_name === 'digitaal_vlaanderen_dhmv') {
if (['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')) {
return theme.id === 'elevation'
}
if (dataset.source_name === 'department_omgeving_thematic_raster') {
@@ -444,7 +444,7 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
function isPartitionedRaster(dataset: DatasetCreateResponse | null | undefined): boolean {
return Boolean(
dataset?.dataset_type === 'raster'
&& ['digitaal_vlaanderen_dhmv', 'vmm_flood_hazard'].includes(dataset.source_name ?? ''),
&& ['digitaal_vlaanderen_dhmv', 'spw_terrain', 'vmm_flood_hazard'].includes(dataset.source_name ?? ''),
)
}
@@ -544,6 +544,7 @@ function pickThemeDataset(
(dataset.source_name === 'spw_walous_land_cover' ? 5_000_000 : 0) +
(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 === 'spw_terrain' ? 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) +
@@ -873,6 +874,7 @@ export function MapWorkspace({
selectedCoverageZones?.includes('flanders')
|| (!selectedCoverageZones && activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME),
)
const walloniaScopeSelected = Boolean(selectedCoverageZones?.includes('wallonia'))
const {
themeInsights,
themeInsightsLoading: themeResultsLoading,
@@ -992,6 +994,14 @@ export function MapWorkspace({
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected),
) ?? null
}
if (walloniaScopeSelected && officialMapProducts.spwTerrain.some((product) => product.configured)) {
result.elevation = availableMapDatasets.find(
(dataset) =>
dataset.source_name === 'spw_terrain'
&& datasetProductKey(dataset) === 'spw_mnt_1m_2021_2022'
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected),
) ?? null
}
if (flandersScopeSelected && officialMapProducts.thematic.length > 0) {
for (const product of officialMapProducts.thematic) {
if (!result[product.theme]) {
@@ -1021,6 +1031,7 @@ export function MapWorkspace({
flandersScopeSelected,
floodHazardDatasets,
officialMapProducts.dhmv.length,
officialMapProducts.spwTerrain,
officialMapProducts.floodHazard.length,
officialMapProducts.grb,
officialMapProducts.officialVector,
@@ -1032,6 +1043,7 @@ export function MapWorkspace({
selectedMapArea?.name,
selectedMapAreaId,
selectedCoverageZones,
walloniaScopeSelected,
])
const themePartitionMap = useMemo(
() =>
@@ -1066,6 +1078,7 @@ export function MapWorkspace({
effectiveZones?.includes('flanders')
|| (!effectiveZones && activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME),
)
const includesWallonia = Boolean(effectiveZones?.includes('wallonia'))
if (includesFlanders) {
for (const product of officialMapProducts.thematic) {
result.push({
@@ -1161,6 +1174,21 @@ export function MapWorkspace({
coverageZones: ['flanders'],
})
}
const spwTerrainProduct = includesWallonia
? officialMapProducts.spwTerrain.find((product) => product.configured)
: null
if (spwTerrainProduct) {
result.push({
kind: 'spw_terrain',
productKey: spwTerrainProduct.key,
displayName: spwTerrainProduct.display_name,
theme: 'elevation',
availabilityLabel: `${spwTerrainProduct.analysis_resolution_m} m analyse · ${spwTerrainProduct.acquisition_period} · automatisch bij selectie`,
attribution: spwTerrainProduct.attribution,
limitationMessage: spwTerrainProduct.limitation_message,
coverageZones: spwTerrainProduct.coverage_zones,
})
}
const floodProduct = includesFlanders
? officialMapProducts.floodHazard.find(
(product) => product.key === selectedFloodHazardProductKey,
@@ -1319,7 +1347,7 @@ export function MapWorkspace({
activeTheme.id === 'elevation' && selectedProjectId
? activeThemePartitions.flatMap((dataset) => {
const bounds = dataset.source_metadata?.['bbox_epsg4326']
return dataset.source_name === 'digitaal_vlaanderen_dhmv'
return ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '')
&& Array.isArray(bounds)
&& bounds.length === 4
? [{
@@ -1530,7 +1558,7 @@ export function MapWorkspace({
const terrainReliefMetric = activeSupportingMetrics.find((metric) => metric.metric_key === 'relief_m')
const populationDensityMetric = activeSupportingMetrics.find((metric) => metric.metric_key === 'population_density_mean_per_ha')
const scoreMedianMetric = activeSupportingMetrics.find((metric) => metric.metric_key.endsWith('_median'))
const activeSecondaryMetric = activeMetricUnit === 'm TAW'
const activeSecondaryMetric = activeMetricUnit === 'm TAW' || activeMetricUnit === 'm DNG'
? terrainReliefMetric ? `${terrainReliefMetric.metric_value.toLocaleString('nl-BE', { maximumFractionDigits: 2 })} m reliëf` : null
: activeMetricUnit === 'inwoners'
? populationDensityMetric ? selectionMetricLabel(populationDensityMetric) : null
@@ -1543,7 +1571,7 @@ export function MapWorkspace({
: null
const activeSecondaryLabel = activeMetricUnit === 'ha'
? 'Aandeel selectie'
: activeMetricUnit === 'm TAW'
: activeMetricUnit === 'm TAW' || activeMetricUnit === 'm DNG'
? 'Reliëf'
: activeMetricUnit === 'inwoners'
? 'Gemiddelde dichtheid'
@@ -2301,7 +2329,7 @@ export function MapWorkspace({
<p className="error">{officialMapProductsError}</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'elevation' && officialMapProducts.dhmv.length > 0 ? (
{analysisMode === 'current' && activeTheme.id === 'elevation' && flandersScopeSelected && officialMapProducts.dhmv.length > 0 ? (
<label className="geo-scope-select">
Hoogtemodel
<select
@@ -2334,6 +2362,9 @@ export function MapWorkspace({
<small>DTM meet het maaiveld; DSM bevat ook gebouwen en vegetatie.</small>
</label>
) : null}
{analysisMode === 'current' && activeTheme.id === 'elevation' && walloniaScopeSelected && officialMapProducts.spwTerrain.some((product) => product.configured) ? (
<p className="geo-data-notice">SPW MNT 2021-2022 · 1 m bron · 5 m begrensde analyse · hoogte in m DNG.</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && officialMapProducts.floodHazard.length > 0 ? (
<label className="geo-scope-select">
@@ -108,7 +108,7 @@ export function persistedDatasetSupportsSelection(
const scale = selectionAnalysisScale(bbox)
if (scale === 'overview') return false
const dimensions = selectionDimensions(bbox)
if (dataset.source_name === 'digitaal_vlaanderen_dhmv' || dataset.source_name === 'vmm_flood_hazard') {
if (dataset.source_name === 'digitaal_vlaanderen_dhmv' || dataset.source_name === 'spw_terrain' || dataset.source_name === 'vmm_flood_hazard') {
return dimensions.areaSquareMetres <= 280_000_000
}
if (dataset.source_name === 'department_omgeving_thematic_raster') {