feat: complete governed Walloon coverage 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 03:39:08 +02:00
parent 17d4442e60
commit c2101ea8f9
37 changed files with 1813 additions and 45 deletions
+68 -4
View File
@@ -9,7 +9,7 @@ import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/da
import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl } from '../../lib/thematicRaster'
import { thematicRasterImageUrl, walousRasterImageUrl } from '../../lib/thematicRaster'
import { bathymetryRasterImageUrl } from '../../lib/bathymetryRaster'
import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus'
import {
@@ -59,6 +59,7 @@ const EMPTY_TEMPORAL_SERIES: DatasetCreateResponse[] = []
type DataThemeId =
| 'administrative'
| 'buildings'
| 'land_cover'
| 'space_occupation'
| 'open_space'
| 'population'
@@ -110,7 +111,7 @@ function productSupportsSelection(product: OnDemandMapProduct, bbox: VectorSelec
if (product.kind === 'dhmv' || product.kind === 'flood_hazard') {
return dimensions.areaSquareMetres <= 280_000_000
}
if (product.kind === 'thematic_raster') {
if (product.kind === 'thematic_raster' || product.kind === 'walous') {
return dimensions.widthMetres <= 50_000
&& dimensions.heightMetres <= 50_000
&& dimensions.areaSquareMetres <= 2_800_000_000
@@ -133,6 +134,13 @@ const DATA_THEMES: DataTheme[] = [
description: 'Gebouwen en gebouwcontouren uit GRB of een andere persistente bron.',
tokens: ['buildings', 'building', 'gebouwen', 'gebouw', 'bebouwing', 'gbg'],
},
{
id: 'land_cover',
label: 'Landbedekking',
shortLabel: 'Landbedekking',
description: 'Fysieke en biologische bodembedekking uit een officieel regionaal classificatieraster.',
tokens: ['land_cover', 'land_cover_use', 'landbedekking', 'walous', 'occupation du sol'],
},
{
id: 'space_occupation',
label: 'Ruimtebeslag',
@@ -257,6 +265,7 @@ const DATA_THEMES: DataTheme[] = [
const COVERAGE_THEME_BY_MAP_THEME: Record<DataThemeId, string> = {
administrative: 'admin',
buildings: 'buildings',
land_cover: 'land_cover_use',
space_occupation: 'land_cover_use',
open_space: 'land_cover_use',
population: 'population',
@@ -303,6 +312,7 @@ function coverageZoneLabel(zone: string): string {
const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }> = {
administrative: { fill: '#5f6f7f', line: '#344554' },
buildings: { fill: '#d45f3d', line: '#9f3e24' },
land_cover: { fill: '#4f7b4f', line: '#315c39' },
space_occupation: { fill: '#be3e33', line: '#8f2c24' },
open_space: { fill: '#267a46', line: '#175c32' },
population: { fill: '#7559a6', line: '#5b3f88' },
@@ -358,6 +368,12 @@ function datasetAvailabilityLabel(
const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'
return `${resolutionLabel} officiële bron${Number.isFinite(year) ? ` · ${year}` : ''}`
}
if (dataset.dataset_type === 'raster' && dataset.source_name === 'spw_walous_land_cover') {
const resolution = Number(dataset.source_metadata?.['analysis_resolution_m'])
const year = Number(dataset.source_metadata?.['observation_year'])
const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'
return `${resolutionLabel} WALOUS-landbedekking${Number.isFinite(year) ? ` · ${year}` : ''}`
}
if (dataset.source_name === 'ngi_adminvector') {
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële bestuursgebieden`
}
@@ -406,6 +422,9 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
if (dataset.source_name === 'department_omgeving_thematic_raster') {
return dataset.source_metadata?.['theme'] === theme.id
}
if (dataset.source_name === 'spw_walous_land_cover') {
return theme.id === 'land_cover'
}
if (dataset.source_name === 'dov_soil_map') {
return theme.id === 'soil'
}
@@ -522,6 +541,7 @@ function pickThemeDataset(
(dataset.source_name === 'inbo_bwk_natura2000' ? 95_000 : 0) +
(dataset.source_name === 'agentschap_landbouw_zeevisserij_agricultural_parcels' ? 98_000 : 0) +
(dataset.source_name === 'department_omgeving_thematic_raster' ? 5_000_000 : 0) +
(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 === 'vmm_flood_hazard' ? 5_000_000 : 0) +
@@ -636,6 +656,10 @@ function formatDatasetObservation(dataset: DatasetCreateResponse): string {
return `referentiejaar ${observationYear}`
}
}
if (dataset.source_name === 'spw_walous_land_cover') {
const observationYear = Number(dataset.source_metadata?.['observation_year'])
return Number.isFinite(observationYear) ? `WALOUS referentiejaar ${observationYear}` : 'WALOUS landbedekking'
}
const period = dataset.source_metadata?.['acquisition_period']
if (typeof period === 'string' && period.trim()) {
return `opnameperiode ${period}`
@@ -1085,6 +1109,29 @@ export function MapWorkspace({
})
}
}
const latestWalous = officialMapProducts.walous
.filter((product) => product.configured && productCoversZones(product.coverage_zones, effectiveZones))
.sort((left, right) => right.observation_year - left.observation_year)[0]
if (latestWalous) {
result.push({
kind: 'walous',
productKey: latestWalous.key,
historyProductKeys: officialMapProducts.walous
.filter(
(product) =>
product.configured
&& product.key !== latestWalous.key
&& productCoversZones(product.coverage_zones, effectiveZones),
)
.map((product) => product.key),
displayName: latestWalous.display_name,
theme: 'land_cover',
availabilityLabel: `${latestWalous.analysis_resolution_m ?? latestWalous.native_resolution_m} m analyse · ${latestWalous.observation_year} · automatisch bij selectie`,
attribution: latestWalous.attribution,
limitationMessage: latestWalous.limitation_message,
coverageZones: latestWalous.coverage_zones,
})
}
for (const product of officialMapProducts.officialVector.filter((item) =>
productCoversZones(item.coverage_zones, effectiveZones),
)) {
@@ -1319,6 +1366,20 @@ export function MapWorkspace({
: [],
[activeThemeDataset, selectedProjectId, thematicRasterBounds],
)
const walousRasterBounds = activeThemeDataset?.source_name === 'spw_walous_land_cover'
? activeThemeDataset.source_metadata?.['bbox_epsg4326']
: null
const walousRasterImageOverlays = useMemo(
() => activeThemeDataset?.source_name === 'spw_walous_land_cover' && selectedProjectId && Array.isArray(walousRasterBounds) && walousRasterBounds.length === 4
? [{
url: walousRasterImageUrl(selectedProjectId, activeThemeDataset.id),
bbox: walousRasterBounds.map(Number) as [number, number, number, number],
label: getDatasetDisplayName(activeThemeDataset),
opacity: 0.82,
}]
: [],
[activeThemeDataset, selectedProjectId, walousRasterBounds],
)
const bathymetryRasterBounds = activeThemeDataset?.source_name === 'spw_bathymetry'
? activeThemeDataset.source_metadata?.['bbox_epsg4326']
: null
@@ -1338,6 +1399,8 @@ export function MapWorkspace({
const activeImageOverlays = useMemo(
() => bathymetryRasterImageOverlays.length > 0
? bathymetryRasterImageOverlays
: walousRasterImageOverlays.length > 0
? walousRasterImageOverlays
: thematicRasterImageOverlays.length > 0
? thematicRasterImageOverlays
: floodHazardImageOverlays.length > 0
@@ -1345,7 +1408,7 @@ export function MapWorkspace({
: terrainImageOverlays.length > 0
? terrainImageOverlays
: orthophotoImageOverlay ? [orthophotoImageOverlay] : [],
[bathymetryRasterImageOverlays, floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays],
[bathymetryRasterImageOverlays, floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays, walousRasterImageOverlays],
)
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const themeTemporalSeriesMap = useMemo(
@@ -1908,6 +1971,7 @@ export function MapWorkspace({
kind: onDemandProduct.kind,
productKey: onDemandProduct.productKey,
displayName: onDemandProduct.displayName,
historyProductKeys: onDemandProduct.historyProductKeys,
},
acquisitionBboxes: onDemandProduct.acquisitionBboxes,
featureLimit: resultFeatureLimit,
@@ -2471,7 +2535,7 @@ export function MapWorkspace({
/>
<div className="geo-map-legend" aria-label="Kaartlegende">
<span><i className="geo-legend-area" /> Werkgebied</span>
{thematicRasterImageOverlays.length > 0 ? (
{thematicRasterImageOverlays.length > 0 || walousRasterImageOverlays.length > 0 ? (
<span className="geo-legend-thematic">
<i className={`geo-legend-ramp geo-legend-ramp-${activeTheme.id}`} />
<small>{thematicLegendMin} {thematicLegendMax}</small>
@@ -9,6 +9,7 @@ import { bathymetryRasterSelectionToMapSelection } from '../lib/bathymetryRaster
export type MapThemeAcquisitionKind =
| 'thematic_raster'
| 'walous'
| 'dhmv'
| 'flood_hazard'
| 'grb'
@@ -19,6 +20,7 @@ export interface MapThemeAcquisition {
kind: MapThemeAcquisitionKind
productKey: string
displayName: string
historyProductKeys?: string[]
}
export interface MapThemeQuery<TThemeId extends string> {
@@ -113,7 +115,7 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
const resultLimit = featureLimit ?? 1000
if (acquisition) {
const requestedBboxes = acquisitionBboxes?.length ? acquisitionBboxes : [bbox]
const acquisitionResults = await settleWithConcurrency(requestedBboxes, 1, async (acquisitionBbox) => {
const acquireProduct = async (acquisitionBbox: VectorSelectionBBox, productKey: string) => {
const commonPayload = {
bbox: acquisitionBbox,
area_id: areaId,
@@ -124,6 +126,11 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
...commonPayload,
product_key: acquisition.productKey,
})
: acquisition.kind === 'walous'
? await datasetsApi.acquireWalous(selectedProjectId, {
...commonPayload,
product_key: productKey,
})
: acquisition.kind === 'dhmv'
? await datasetsApi.acquireDhmv(selectedProjectId, {
...commonPayload,
@@ -152,13 +159,38 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
)
}
return datasetsApi.get(selectedProjectId, acquisitionJob.output_dataset_id)
})
}
const acquisitionResults = await settleWithConcurrency(
requestedBboxes,
1,
(acquisitionBbox) => acquireProduct(acquisitionBbox, acquisition.productKey),
)
const failedAcquisition = acquisitionResults.find((item) => item.status === 'rejected')
if (failedAcquisition?.status === 'rejected') {
throw failedAcquisition.reason
}
acquiredDatasets = acquisitionResults.flatMap((item) => item.status === 'fulfilled' ? [item.value] : [])
dataset = acquiredDatasets[0]
const historyProductKeys = acquisition.kind === 'walous'
? [...new Set(acquisition.historyProductKeys ?? [])].filter((key) => key !== acquisition.productKey)
: []
if (historyProductKeys.length > 0) {
const historyRequests = historyProductKeys.flatMap((productKey) =>
requestedBboxes.map((acquisitionBbox) => ({ acquisitionBbox, productKey })),
)
const historyResults = await settleWithConcurrency(
historyRequests,
1,
({ acquisitionBbox, productKey }) => acquireProduct(acquisitionBbox, productKey),
)
const failedHistory = historyResults.find((item) => item.status === 'rejected')
if (failedHistory?.status === 'rejected') {
throw failedHistory.reason
}
acquiredDatasets.push(
...historyResults.flatMap((item) => item.status === 'fulfilled' ? [item.value] : []),
)
}
}
if (!dataset) {
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
@@ -197,8 +229,13 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
: dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster'
? thematicRasterSelectionToMapSelection(await datasetsApi.selectThematicRaster(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}))
area_id: areaId,
}))
: dataset.dataset_type === 'raster' && dataset.source_name === 'spw_walous_land_cover'
? thematicRasterSelectionToMapSelection(await datasetsApi.selectWalous(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}))
: dataset.dataset_type === 'raster' && dataset.source_name === 'spw_bathymetry'
? bathymetryRasterSelectionToMapSelection(await datasetsApi.selectBathymetryRaster(selectedProjectId, dataset.id, {
bbox,
+5 -1
View File
@@ -14,6 +14,7 @@ import type {
export interface OfficialMapProducts {
thematic: ThematicRasterProductRead[]
walous: ThematicRasterProductRead[]
dhmv: DhmvProductRead[]
floodHazard: FloodHazardProductRead[]
grb: GrbProductRead[]
@@ -23,6 +24,7 @@ export interface OfficialMapProducts {
const EMPTY_PRODUCTS: OfficialMapProducts = {
thematic: [],
walous: [],
dhmv: [],
floodHazard: [],
grb: [],
@@ -50,16 +52,18 @@ export function useOfficialMapProducts(selectedProjectId: string | null) {
setError(null)
void Promise.all([
datasetsApi.listThematicRasterProducts(selectedProjectId),
datasetsApi.listWalousProducts(selectedProjectId),
datasetsApi.listDhmvProducts(selectedProjectId),
datasetsApi.listFloodHazardProducts(selectedProjectId),
datasetsApi.listGrbProducts(selectedProjectId),
datasetsApi.listOfficialVectorProducts(selectedProjectId),
datasetsApi.listBathymetrySources(selectedProjectId),
])
.then(([thematic, dhmv, floodHazard, grb, officialVector, bathymetry]) => {
.then(([thematic, walous, dhmv, floodHazard, grb, officialVector, bathymetry]) => {
if (!cancelled) {
setProducts({
thematic: thematic.items,
walous: walous.items,
dhmv: dhmv.items,
floodHazard: floodHazard.items,
grb: grb.items,
+1
View File
@@ -5,6 +5,7 @@ const NON_IMAGERY_RASTER_SOURCES = new Set([
'digitaal_vlaanderen_dhmv',
'vmm_flood_hazard',
'spw_bathymetry',
'spw_walous_land_cover',
])
export function isDetectionImageryDataset(dataset: DatasetCreateResponse): boolean {
+7
View File
@@ -17,6 +17,7 @@ const DATASET_LABEL_BY_LAYER: Record<string, string> = {
open_space: 'Open ruimte',
accessibility: 'Knooppuntwaarde',
services: 'Voorzieningenniveau',
land_cover: 'Landbedekking',
building_registry: 'Gebouwenregister',
regional_boundary: 'Grens vervoerregio Kempen',
municipality_boundaries: 'Gemeentegrenzen Kempen',
@@ -37,6 +38,8 @@ const DATASET_SOURCE_LABELS: Record<string, string> = {
vmm_flood_hazard: 'Vlaamse Milieumaatschappij',
vmm_vha_bathymetry_profiles: 'VMM / Vlaamse Hydrografische Atlas',
spw_bathymetry: 'Service public de Wallonie',
spw_walous_land_cover: 'Service public de Wallonie',
spw_flood_hazard: 'Service public de Wallonie',
department_omgeving_thematic_raster: 'Departement Omgeving',
dov_soil_map: 'Databank Ondergrond Vlaanderen',
}
@@ -65,6 +68,10 @@ export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
const productName = dataset.source_metadata?.['product_display_name']
return typeof productName === 'string' && productName.trim() ? productName : 'Officieel Vlaams themaraster'
}
if (dataset.source_name === 'spw_walous_land_cover') {
const productName = dataset.source_metadata?.['product_display_name']
return typeof productName === 'string' && productName.trim() ? productName : 'WALOUS landbedekking'
}
const layer = (dataset.reference_layer_name ?? dataset.source_metadata?.layer_name ?? dataset.source_metadata?.layer_type ?? '')
.toString()
.toLowerCase()
+4
View File
@@ -4,6 +4,10 @@ export function thematicRasterImageUrl(projectId: string, datasetId: string): st
return `/api/v1/projects/${projectId}/datasets/${datasetId}/raster/thematic/image`
}
export function walousRasterImageUrl(projectId: string, datasetId: string): string {
return `/api/v1/projects/${projectId}/datasets/${datasetId}/raster/walous/image`
}
export function thematicRasterSelectionToMapSelection(result: ThematicRasterSelectionResponse): VectorSelectionResponse {
return {
selection_bbox: result.selection_bbox,
+10
View File
@@ -220,12 +220,22 @@ export const datasetsApi = {
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/thematic-raster/acquire`, payload),
listThematicRasterProducts: (projectId: string): Promise<{ items: ThematicRasterProductRead[]; total: number }> =>
apiGet<{ items: ThematicRasterProductRead[]; total: number }>(`/api/v1/projects/${projectId}/datasets/thematic-raster/products`),
acquireWalous: (projectId: string, payload: ThematicRasterAcquireRequest): Promise<JobRead> =>
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/walous/acquire`, payload),
listWalousProducts: (projectId: string): Promise<{ items: ThematicRasterProductRead[]; total: number }> =>
apiGet<{ items: ThematicRasterProductRead[]; total: number }>(`/api/v1/projects/${projectId}/datasets/walous/products`),
selectThematicRaster: (
projectId: string,
datasetId: string,
payload: { bbox: VectorSelectionRequest['bbox']; area_id?: string },
): Promise<ThematicRasterSelectionResponse> =>
apiPost<ThematicRasterSelectionResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/thematic/select`, payload),
selectWalous: (
projectId: string,
datasetId: string,
payload: { bbox: VectorSelectionRequest['bbox']; area_id?: string },
): Promise<ThematicRasterSelectionResponse> =>
apiPost<ThematicRasterSelectionResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/walous/select`, payload),
refreshMetadata: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
apiPost<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/metadata/refresh`, {}),
inspectRaster: (projectId: string, datasetId: string): Promise<RasterInspectResponse> =>
+7 -3
View File
@@ -576,11 +576,12 @@ export interface ThematicRasterAcquireRequest {
export interface ThematicRasterProductRead {
key: string
display_name: string
theme: 'space_occupation' | 'open_space' | 'forest' | 'agriculture' | 'population' | 'accessibility' | 'services'
metric_kind: 'binary_area' | 'population_density' | 'index_score' | 'normalized_score'
theme: 'space_occupation' | 'open_space' | 'forest' | 'agriculture' | 'population' | 'accessibility' | 'services' | 'land_cover'
metric_kind: 'binary_area' | 'population_density' | 'index_score' | 'normalized_score' | 'categorical_area'
coverage_id: string
native_resolution_m: number
source_crs: 'EPSG:31370'
analysis_resolution_m?: number | null
source_crs: 'EPSG:31370' | 'EPSG:3812'
source_value_unit: string
observation_year: number
source_version: string
@@ -591,6 +592,9 @@ export interface ThematicRasterProductRead {
legend_max_label: string
included_source_values: number[]
limitation_message: string
coverage_zones: string[]
configured: boolean
status: 'configured' | 'source_not_provisioned'
}
export interface ThematicRasterSelectionResponse {