Federate official Belgium data sources
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { AreaRead, DatasetCreateResponse, ProjectRead } from '../types'
|
||||
import { WorkbenchStatusStrip } from './WorkbenchStatusStrip'
|
||||
|
||||
const project = {
|
||||
id: 'project-1',
|
||||
name: 'Belgium and North Sea Workbench',
|
||||
region: 'Belgium and Belgian North Sea',
|
||||
} as unknown as ProjectRead
|
||||
|
||||
const areas = [{ id: 'area-1', name: 'Belgium land' }] as unknown as AreaRead[]
|
||||
|
||||
function renderStatus(datasets: DatasetCreateResponse[]): void {
|
||||
render(
|
||||
<WorkbenchStatusStrip
|
||||
selectedProject={project}
|
||||
areas={areas}
|
||||
datasets={datasets}
|
||||
qualityChecks={[]}
|
||||
exports={[]}
|
||||
activeLayerFeatureCount={1}
|
||||
selectedAreaHasGeometry
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('WorkbenchStatusStrip analytical readiness', () => {
|
||||
it('treats the persisted NGI administrative baseline as analyzable', () => {
|
||||
renderStatus([
|
||||
{
|
||||
id: 'dataset-1',
|
||||
dataset_type: 'vector',
|
||||
source: 'operator_official_import',
|
||||
source_name: 'ngi_adminvector',
|
||||
reference_layer_name: 'belgium_municipalities',
|
||||
dataset_role: 'reference',
|
||||
status: 'ready',
|
||||
} as unknown as DatasetCreateResponse,
|
||||
])
|
||||
|
||||
expect(screen.getByText('Kaartwerkruimte is gebruiksklaar')).toBeTruthy()
|
||||
expect(screen.getByText("1 analysethema")).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not call an unrelated stored context file an analysis theme', () => {
|
||||
renderStatus([
|
||||
{
|
||||
id: 'dataset-2',
|
||||
dataset_type: 'vector',
|
||||
source: 'manual',
|
||||
source_name: 'manual',
|
||||
reference_layer_name: 'custom_context',
|
||||
dataset_role: 'reference',
|
||||
status: 'ready',
|
||||
} as unknown as DatasetCreateResponse,
|
||||
])
|
||||
|
||||
expect(screen.getByText('Kaartwerkruimte vraagt aandacht')).toBeTruthy()
|
||||
expect(screen.getByText("0 analysethema's")).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -32,6 +32,59 @@ function countReferenceDatasets(datasets: DatasetCreateResponse[]): number {
|
||||
return datasets.filter((dataset) => dataset.dataset_role === 'reference').length
|
||||
}
|
||||
|
||||
const ANALYTICAL_SOURCE_NAMES = new Set([
|
||||
'ngi_adminvector',
|
||||
'rbins_marine_reporting_units',
|
||||
'rbins_msp_2026',
|
||||
'grb',
|
||||
'department_omgeving_land_use',
|
||||
'historical_landuse',
|
||||
'department_omgeving_thematic_raster',
|
||||
'statbel',
|
||||
'inbo_bwk_natura2000',
|
||||
'agentschap_landbouw_zeevisserij_agricultural_parcels',
|
||||
'dov_soil_map',
|
||||
'digitaal_vlaanderen_dhmv',
|
||||
'vmm_flood_hazard',
|
||||
'vmm_vha_bathymetry_profiles',
|
||||
])
|
||||
|
||||
const ANALYTICAL_LAYER_NAMES = new Set([
|
||||
'administrative',
|
||||
'belgium_land_boundary',
|
||||
'belgium_regions',
|
||||
'belgium_provinces',
|
||||
'belgium_municipalities',
|
||||
'buildings',
|
||||
'roads',
|
||||
'water',
|
||||
'parcels',
|
||||
'population',
|
||||
'forest',
|
||||
'nature_value',
|
||||
'agriculture',
|
||||
'soil',
|
||||
'bathymetry',
|
||||
'flood_hazard',
|
||||
'elevation',
|
||||
'maritime_planning',
|
||||
'marine_environment',
|
||||
'marine_legal_scopes',
|
||||
'marine_spatial_plan_2026',
|
||||
])
|
||||
|
||||
function isAnalyzableDataset(dataset: DatasetCreateResponse): boolean {
|
||||
if (dataset.status !== 'ready') return false
|
||||
const sourceName = String(dataset.source_name ?? dataset.source ?? '').toLowerCase()
|
||||
const layerName = String(
|
||||
dataset.source_metadata?.['theme']
|
||||
?? dataset.reference_layer_name
|
||||
?? dataset.source_metadata?.['layer_type']
|
||||
?? '',
|
||||
).toLowerCase()
|
||||
return ANALYTICAL_SOURCE_NAMES.has(sourceName) || ANALYTICAL_LAYER_NAMES.has(layerName)
|
||||
}
|
||||
|
||||
function statusLabel(state: StatusItem['state']): string {
|
||||
if (state === 'ready') return 'gereed'
|
||||
if (state === 'warning') return 'aandacht'
|
||||
@@ -73,6 +126,7 @@ export function WorkbenchStatusStrip({
|
||||
selectedAreaHasGeometry,
|
||||
}: WorkbenchStatusStripProps): JSX.Element {
|
||||
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
|
||||
const analyzableDatasets = datasets.filter(isAnalyzableDataset).length
|
||||
const vectorDatasets = countByDatasetType(datasets, 'vector') + countByDatasetType(datasets, 'geojson')
|
||||
const rasterDatasets = countByDatasetType(datasets, 'raster')
|
||||
const referenceDatasets = countReferenceDatasets(datasets)
|
||||
@@ -97,9 +151,12 @@ export function WorkbenchStatusStrip({
|
||||
{
|
||||
key: 'datasets',
|
||||
label: 'Bronnen',
|
||||
value: `${readyDatasets}/${datasets.length} beschikbaar`,
|
||||
detail: `${vectorDatasets} kaartlagen, ${rasterDatasets} luchtbeelden, ${referenceDatasets} officiële referenties`,
|
||||
state: datasets.length === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning',
|
||||
value: `${analyzableDatasets} analysethema${analyzableDatasets === 1 ? '' : "'s"}`,
|
||||
detail: (
|
||||
`${readyDatasets}/${datasets.length} bronnen gereed · `
|
||||
+ `${vectorDatasets} kaartlagen, ${rasterDatasets} rasters, ${referenceDatasets} officiële referenties`
|
||||
),
|
||||
state: analyzableDatasets === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning',
|
||||
},
|
||||
{
|
||||
key: 'map',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
||||
import GeoMap from '../GeoMap'
|
||||
import type { AreaRead, CoverageResolveResponse, CoverageStatus, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
|
||||
import { useMapThemeSelectionInsights, type MapThemeAcquisition, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
|
||||
@@ -30,10 +30,12 @@ import {
|
||||
normalizeBboxFromCorners,
|
||||
operationalScopeProjectLabel,
|
||||
parseBboxInput,
|
||||
productCoversZones,
|
||||
readablePropertyName,
|
||||
resultCountLabel,
|
||||
resultMetricLabel,
|
||||
safeFileStem,
|
||||
selectedAreaCoverageZones,
|
||||
selectedFeatureCollection,
|
||||
selectionAreaSquareMetres,
|
||||
selectionMetricLabel,
|
||||
@@ -43,7 +45,26 @@ 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' | 'bathymetry' | 'flood_hazard' | 'elevation' | 'accessibility' | 'services' | 'roads' | 'parcels'
|
||||
type DataThemeId =
|
||||
| 'administrative'
|
||||
| 'buildings'
|
||||
| 'space_occupation'
|
||||
| 'open_space'
|
||||
| 'population'
|
||||
| 'forest'
|
||||
| 'nature_value'
|
||||
| 'agriculture'
|
||||
| 'soil'
|
||||
| 'water'
|
||||
| 'bathymetry'
|
||||
| 'flood_hazard'
|
||||
| 'elevation'
|
||||
| 'accessibility'
|
||||
| 'services'
|
||||
| 'roads'
|
||||
| 'parcels'
|
||||
| 'maritime_planning'
|
||||
| 'marine_environment'
|
||||
|
||||
interface DataTheme {
|
||||
id: DataThemeId
|
||||
@@ -67,6 +88,13 @@ interface OnDemandMapProduct extends MapThemeAcquisition {
|
||||
}
|
||||
|
||||
const DATA_THEMES: DataTheme[] = [
|
||||
{
|
||||
id: 'administrative',
|
||||
label: 'Bestuurlijke indeling',
|
||||
shortLabel: 'Bestuursgebieden',
|
||||
description: 'Officiële lands-, gewest-, provincie- en gemeentegrenzen van het NGI.',
|
||||
tokens: ['administrative', 'adminvector', 'belgium_land_boundary', 'belgium_regions', 'belgium_provinces', 'belgium_municipalities'],
|
||||
},
|
||||
{
|
||||
id: 'buildings',
|
||||
label: 'Bebouwing',
|
||||
@@ -179,9 +207,24 @@ const DATA_THEMES: DataTheme[] = [
|
||||
description: 'Kadastrale of administratieve perceelcontouren.',
|
||||
tokens: ['parcels', 'parcel', 'percelen', 'perceel', 'cadastre', 'kadaster'],
|
||||
},
|
||||
{
|
||||
id: 'maritime_planning',
|
||||
label: 'Maritieme planning',
|
||||
shortLabel: 'Plan- en gebruikszones',
|
||||
description: 'Officiële gebruiks- en beschermingszones uit het Belgisch Marien Ruimtelijk Plan 2026-2034.',
|
||||
tokens: ['maritime_planning', 'marine_spatial_plan', 'rbins_msp', 'bmsp', 'imsp26'],
|
||||
},
|
||||
{
|
||||
id: 'marine_environment',
|
||||
label: 'Mariene rapportagezones',
|
||||
shortLabel: 'Zeegebieden',
|
||||
description: 'Officiële juridische en mariene rapportagegebieden voor het Belgische deel van de Noordzee.',
|
||||
tokens: ['marine_environment', 'marine_legal_scopes', 'marine_reporting_units', 'rbins_marine_reporting'],
|
||||
},
|
||||
]
|
||||
|
||||
const COVERAGE_THEME_BY_MAP_THEME: Record<DataThemeId, string> = {
|
||||
administrative: 'admin',
|
||||
buildings: 'buildings',
|
||||
space_occupation: 'land_cover_use',
|
||||
open_space: 'land_cover_use',
|
||||
@@ -198,6 +241,8 @@ const COVERAGE_THEME_BY_MAP_THEME: Record<DataThemeId, string> = {
|
||||
services: 'population',
|
||||
roads: 'roads',
|
||||
parcels: 'parcels',
|
||||
maritime_planning: 'maritime_planning',
|
||||
marine_environment: 'marine_environment',
|
||||
}
|
||||
|
||||
function coverageStatusLabel(status: CoverageStatus): string {
|
||||
@@ -225,6 +270,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' },
|
||||
space_occupation: { fill: '#be3e33', line: '#8f2c24' },
|
||||
open_space: { fill: '#267a46', line: '#175c32' },
|
||||
@@ -241,6 +287,8 @@ const DATA_THEME_MAP_STYLES: Record<DataThemeId, { fill: string; line: string }>
|
||||
services: { fill: '#b66d16', line: '#854d0e' },
|
||||
roads: { fill: '#6b7280', line: '#4b5563' },
|
||||
parcels: { fill: '#a7792f', line: '#7d571f' },
|
||||
maritime_planning: { fill: '#2f7f8f', line: '#145d6a' },
|
||||
marine_environment: { fill: '#3475a3', line: '#1c557d' },
|
||||
}
|
||||
|
||||
function datasetAvailabilityLabel(
|
||||
@@ -274,6 +322,15 @@ function datasetAvailabilityLabel(
|
||||
const resolutionLabel = Number.isFinite(resolution) ? `${resolution.toLocaleString('nl-BE')} m` : 'Raster'
|
||||
return `${resolutionLabel} officiële bron${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`
|
||||
}
|
||||
if (dataset.source_name === 'rbins_msp_2026') {
|
||||
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële planobjecten · 2026-2034`
|
||||
}
|
||||
if (dataset.source_name === 'rbins_marine_reporting_units') {
|
||||
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} officiële zeegebieden`
|
||||
}
|
||||
return `${(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten beschikbaar`
|
||||
}
|
||||
|
||||
@@ -313,6 +370,15 @@ function datasetMatchesTheme(dataset: DatasetCreateResponse, theme: DataTheme):
|
||||
if (dataset.source_name === 'dov_soil_map') {
|
||||
return theme.id === 'soil'
|
||||
}
|
||||
if (dataset.source_name === 'ngi_adminvector') {
|
||||
return theme.id === 'administrative'
|
||||
}
|
||||
if (dataset.source_name === 'rbins_msp_2026') {
|
||||
return theme.id === 'maritime_planning'
|
||||
}
|
||||
if (dataset.source_name === 'rbins_marine_reporting_units') {
|
||||
return theme.id === 'marine_environment'
|
||||
}
|
||||
const searchText = datasetSearchText(dataset)
|
||||
return theme.tokens.some((token) => searchText.includes(token))
|
||||
}
|
||||
@@ -342,8 +408,17 @@ function datasetProductKey(dataset: DatasetCreateResponse): string {
|
||||
function datasetCoversSelectedArea(
|
||||
dataset: DatasetCreateResponse,
|
||||
selectedAreaId: string | null,
|
||||
selectedAreaName: string | null | undefined,
|
||||
regionalScope = false,
|
||||
): boolean {
|
||||
const selectedZones = selectedAreaCoverageZones(selectedAreaName)
|
||||
const configuredZones = dataset.source_metadata?.['coverage_zones']
|
||||
if (selectedZones && Array.isArray(configuredZones) && configuredZones.length > 0) {
|
||||
const datasetZones = configuredZones.map((zone) => String(zone))
|
||||
if (!selectedZones.some((zone) => datasetZones.includes(zone))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const coverageScope = String(dataset.source_metadata?.['coverage_scope'] ?? '')
|
||||
if (isPartitionedBathymetry(dataset)) {
|
||||
return regionalScope
|
||||
@@ -363,6 +438,7 @@ function rasterPartitionsForDataset(
|
||||
datasets: DatasetCreateResponse[],
|
||||
representative: DatasetCreateResponse | null,
|
||||
selectedAreaId: string | null,
|
||||
selectedAreaName: string | null | undefined,
|
||||
regionalScope: boolean,
|
||||
): DatasetCreateResponse[] {
|
||||
if (!representative) {
|
||||
@@ -382,7 +458,7 @@ function rasterPartitionsForDataset(
|
||||
? String(dataset.source_metadata?.['partition_manifest_sha256'] ?? '') === manifestSha256
|
||||
: datasetProductKey(dataset) === productKey
|
||||
)
|
||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, true),
|
||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, true),
|
||||
)
|
||||
.sort((left, right) => String(left.area_id ?? '').localeCompare(String(right.area_id ?? '')))
|
||||
}
|
||||
@@ -391,12 +467,13 @@ function pickThemeDataset(
|
||||
datasets: DatasetCreateResponse[],
|
||||
theme: DataTheme,
|
||||
selectedAreaId: string | null,
|
||||
selectedAreaName: string | null | undefined,
|
||||
regionalScope = false,
|
||||
): DatasetCreateResponse | null {
|
||||
const candidates = datasets.filter(
|
||||
(dataset) =>
|
||||
datasetMatchesTheme(dataset, theme)
|
||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, regionalScope),
|
||||
&& datasetCoversSelectedArea(dataset, selectedAreaId, selectedAreaName, regionalScope),
|
||||
)
|
||||
candidates.sort((left, right) => {
|
||||
const priorityScore = (dataset: DatasetCreateResponse) =>
|
||||
@@ -723,7 +800,15 @@ export function MapWorkspace({
|
||||
})
|
||||
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
|
||||
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
|
||||
const flandersScopeSelected = activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME
|
||||
const selectedMapArea = areas.find((area) => area.id === selectedMapAreaId)
|
||||
const selectedCoverageZones = useMemo(
|
||||
() => selectedAreaCoverageZones(selectedMapArea?.name),
|
||||
[selectedMapArea?.name],
|
||||
)
|
||||
const flandersScopeSelected = Boolean(
|
||||
selectedCoverageZones?.includes('flanders')
|
||||
|| (!selectedCoverageZones && activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME),
|
||||
)
|
||||
const {
|
||||
themeInsights,
|
||||
themeInsightsLoading: themeResultsLoading,
|
||||
@@ -735,7 +820,8 @@ export function MapWorkspace({
|
||||
products: officialMapProducts,
|
||||
loading: officialMapProductsLoading,
|
||||
error: officialMapProductsError,
|
||||
} = useOfficialMapProducts(flandersScopeSelected ? selectedProjectId : null)
|
||||
resolveCoverage,
|
||||
} = useOfficialMapProducts(selectedProjectId)
|
||||
const {
|
||||
temporalComparison,
|
||||
temporalComparisonLoading,
|
||||
@@ -759,7 +845,6 @@ export function MapWorkspace({
|
||||
const [fullWorkflowMode, setFullWorkflowMode] = useState<'new' | 'reuse'>('new')
|
||||
const [mapAnalysisDurationMs, setMapAnalysisDurationMs] = useState<number | null>(null)
|
||||
const mapAnalysisRequestSequence = useRef(0)
|
||||
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'
|
||||
@@ -794,7 +879,7 @@ export function MapWorkspace({
|
||||
.filter(
|
||||
(dataset) =>
|
||||
dataset.source_name === 'vmm_flood_hazard'
|
||||
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, regionalScopeSelected),
|
||||
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected),
|
||||
)
|
||||
.sort((left, right) => floodScenarioLabel(left).localeCompare(floodScenarioLabel(right), 'nl'))
|
||||
if (!regionalScopeSelected) {
|
||||
@@ -809,13 +894,19 @@ export function MapWorkspace({
|
||||
}
|
||||
return Array.from(products.values())
|
||||
},
|
||||
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId],
|
||||
[availableMapDatasets, regionalScopeSelected, selectedMapArea?.name, selectedMapAreaId],
|
||||
)
|
||||
const themeDatasetMap = useMemo(() => {
|
||||
const result = Object.fromEntries(
|
||||
DATA_THEMES.map((theme) => [
|
||||
theme.id,
|
||||
pickThemeDataset(availableMapDatasets, theme, selectedMapAreaId, regionalScopeSelected),
|
||||
pickThemeDataset(
|
||||
availableMapDatasets,
|
||||
theme,
|
||||
selectedMapAreaId,
|
||||
selectedMapArea?.name,
|
||||
regionalScopeSelected,
|
||||
),
|
||||
]),
|
||||
) as Record<DataThemeId, DatasetCreateResponse | null>
|
||||
const selectedFloodHazard = floodHazardDatasets.find(
|
||||
@@ -833,7 +924,7 @@ export function MapWorkspace({
|
||||
(dataset) =>
|
||||
dataset.source_name === 'digitaal_vlaanderen_dhmv'
|
||||
&& datasetProductKey(dataset) === selectedDhmvProductKey
|
||||
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, regionalScopeSelected),
|
||||
&& datasetCoversSelectedArea(dataset, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected),
|
||||
) ?? null
|
||||
}
|
||||
if (flandersScopeSelected && officialMapProducts.thematic.length > 0) {
|
||||
@@ -846,8 +937,10 @@ export function MapWorkspace({
|
||||
result[product.key] = null
|
||||
}
|
||||
}
|
||||
if (flandersScopeSelected && officialMapProducts.officialVector.length > 0) {
|
||||
for (const product of officialMapProducts.officialVector) {
|
||||
if (officialMapProducts.officialVector.length > 0) {
|
||||
for (const product of officialMapProducts.officialVector.filter((item) =>
|
||||
productCoversZones(item.coverage_zones, selectedCoverageZones),
|
||||
)) {
|
||||
result[product.theme] = null
|
||||
}
|
||||
}
|
||||
@@ -865,7 +958,9 @@ export function MapWorkspace({
|
||||
selectedDhmvProductKey,
|
||||
selectedFloodHazardDatasetId,
|
||||
selectedFloodHazardProductKey,
|
||||
selectedMapArea?.name,
|
||||
selectedMapAreaId,
|
||||
selectedCoverageZones,
|
||||
])
|
||||
const themePartitionMap = useMemo(
|
||||
() =>
|
||||
@@ -876,11 +971,12 @@ export function MapWorkspace({
|
||||
availableMapDatasets,
|
||||
themeDatasetMap[theme.id],
|
||||
selectedMapAreaId,
|
||||
selectedMapArea?.name,
|
||||
regionalScopeSelected,
|
||||
),
|
||||
]),
|
||||
) as Record<DataThemeId, DatasetCreateResponse[]>,
|
||||
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap],
|
||||
[availableMapDatasets, regionalScopeSelected, selectedMapArea?.name, selectedMapAreaId, themeDatasetMap],
|
||||
)
|
||||
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
|
||||
const activeCoverageTheme = COVERAGE_THEME_BY_MAP_THEME[activeTheme.id]
|
||||
@@ -892,14 +988,16 @@ export function MapWorkspace({
|
||||
) ?? { operational: 0, partial: 0, not_configured: 0, unsupported: 0 },
|
||||
[coverage],
|
||||
)
|
||||
const onDemandProductMap = useMemo(
|
||||
() => {
|
||||
const result = new Map<DataThemeId, OnDemandMapProduct>()
|
||||
if (!flandersScopeSelected) {
|
||||
return result
|
||||
}
|
||||
const onDemandProductsForZones = useCallback((zones: string[] | null): OnDemandMapProduct[] => {
|
||||
const result: OnDemandMapProduct[] = []
|
||||
const effectiveZones = zones ?? selectedCoverageZones
|
||||
const includesFlanders = Boolean(
|
||||
effectiveZones?.includes('flanders')
|
||||
|| (!effectiveZones && activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME),
|
||||
)
|
||||
if (includesFlanders) {
|
||||
for (const product of officialMapProducts.thematic) {
|
||||
result.set(product.theme, {
|
||||
result.push({
|
||||
kind: 'thematic_raster',
|
||||
productKey: product.key,
|
||||
displayName: product.display_name,
|
||||
@@ -910,7 +1008,7 @@ export function MapWorkspace({
|
||||
})
|
||||
}
|
||||
for (const product of officialMapProducts.grb) {
|
||||
result.set(product.key, {
|
||||
result.push({
|
||||
kind: 'grb',
|
||||
productKey: product.key,
|
||||
displayName: product.display_name,
|
||||
@@ -920,52 +1018,65 @@ export function MapWorkspace({
|
||||
limitationMessage: product.limitation_message,
|
||||
})
|
||||
}
|
||||
for (const product of officialMapProducts.officialVector) {
|
||||
result.set(product.theme, {
|
||||
kind: 'official_vector',
|
||||
productKey: product.key,
|
||||
displayName: product.display_name,
|
||||
theme: product.theme,
|
||||
availabilityLabel: `${product.observation_label} · officiële vectorbron · laad bij selectie`,
|
||||
attribution: product.attribution,
|
||||
limitationMessage: product.limitation_message,
|
||||
})
|
||||
}
|
||||
const dhmvProduct = officialMapProducts.dhmv.find((product) => product.key === selectedDhmvProductKey)
|
||||
if (dhmvProduct) {
|
||||
result.set('elevation', {
|
||||
kind: 'dhmv',
|
||||
productKey: dhmvProduct.key,
|
||||
displayName: dhmvProduct.display_name,
|
||||
theme: 'elevation',
|
||||
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · laad bij selectie`,
|
||||
attribution: dhmvProduct.attribution,
|
||||
limitationMessage: dhmvProduct.limitation_message,
|
||||
})
|
||||
}
|
||||
const floodProduct = officialMapProducts.floodHazard.find(
|
||||
(product) => product.key === selectedFloodHazardProductKey,
|
||||
)
|
||||
if (floodProduct) {
|
||||
result.set('flood_hazard', {
|
||||
kind: 'flood_hazard',
|
||||
productKey: floodProduct.key,
|
||||
displayName: floodProduct.display_name,
|
||||
theme: 'flood_hazard',
|
||||
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · laad bij selectie`,
|
||||
attribution: floodProduct.attribution,
|
||||
limitationMessage: floodProduct.limitation_message,
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
[
|
||||
flandersScopeSelected,
|
||||
officialMapProducts,
|
||||
selectedDhmvProductKey,
|
||||
selectedFloodHazardProductKey,
|
||||
],
|
||||
)
|
||||
}
|
||||
for (const product of officialMapProducts.officialVector.filter((item) =>
|
||||
productCoversZones(item.coverage_zones, effectiveZones),
|
||||
)) {
|
||||
result.push({
|
||||
kind: 'official_vector',
|
||||
productKey: product.key,
|
||||
displayName: product.display_name,
|
||||
theme: product.theme,
|
||||
availabilityLabel: `${product.observation_label} · officiële vectorbron · laad bij selectie`,
|
||||
attribution: product.attribution,
|
||||
limitationMessage: product.limitation_message,
|
||||
})
|
||||
}
|
||||
const dhmvProduct = includesFlanders
|
||||
? officialMapProducts.dhmv.find((product) => product.key === selectedDhmvProductKey)
|
||||
: null
|
||||
if (dhmvProduct) {
|
||||
result.push({
|
||||
kind: 'dhmv',
|
||||
productKey: dhmvProduct.key,
|
||||
displayName: dhmvProduct.display_name,
|
||||
theme: 'elevation',
|
||||
availabilityLabel: `${dhmvProduct.native_resolution_m} m · ${dhmvProduct.acquisition_period} · laad bij selectie`,
|
||||
attribution: dhmvProduct.attribution,
|
||||
limitationMessage: dhmvProduct.limitation_message,
|
||||
})
|
||||
}
|
||||
const floodProduct = includesFlanders
|
||||
? officialMapProducts.floodHazard.find(
|
||||
(product) => product.key === selectedFloodHazardProductKey,
|
||||
)
|
||||
: null
|
||||
if (floodProduct) {
|
||||
result.push({
|
||||
kind: 'flood_hazard',
|
||||
productKey: floodProduct.key,
|
||||
displayName: floodProduct.display_name,
|
||||
theme: 'flood_hazard',
|
||||
availabilityLabel: `${floodProduct.native_resolution_m} m · ${floodProduct.climate_context} · T${floodProduct.return_period_years} · laad bij selectie`,
|
||||
attribution: floodProduct.attribution,
|
||||
limitationMessage: floodProduct.limitation_message,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}, [
|
||||
activeScopeProject?.name,
|
||||
officialMapProducts,
|
||||
selectedCoverageZones,
|
||||
selectedDhmvProductKey,
|
||||
selectedFloodHazardProductKey,
|
||||
])
|
||||
const onDemandProductMap = useMemo(() => {
|
||||
const result = new Map<DataThemeId, OnDemandMapProduct>()
|
||||
for (const product of onDemandProductsForZones(selectedCoverageZones)) {
|
||||
result.set(product.theme, product)
|
||||
}
|
||||
return result
|
||||
}, [onDemandProductsForZones, selectedCoverageZones])
|
||||
const activeOnDemandMapProduct = onDemandProductMap.get(activeTheme.id) ?? null
|
||||
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
|
||||
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
|
||||
@@ -993,15 +1104,20 @@ export function MapWorkspace({
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!flandersScopeSelected
|
||||
|| analysisMode !== 'current'
|
||||
analysisMode !== 'current'
|
||||
|| activeThemeAvailable
|
||||
|| (onDemandProductMap.size === 0 && !officialMapProductsError)
|
||||
|| (
|
||||
selectedProjectId
|
||||
&& officialMapProductsLoading
|
||||
&& onDemandProductMap.size === 0
|
||||
&& !officialMapProductsError
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const fallbackTheme = DATA_THEMES.find((theme) =>
|
||||
theme.id === 'space_occupation'
|
||||
flandersScopeSelected
|
||||
&& theme.id === 'space_occupation'
|
||||
&& Boolean(themeDatasetMap[theme.id] || onDemandProductMap.get(theme.id)),
|
||||
) ?? DATA_THEMES.find((theme) =>
|
||||
Boolean(themeDatasetMap[theme.id] || onDemandProductMap.get(theme.id)),
|
||||
@@ -1019,8 +1135,10 @@ export function MapWorkspace({
|
||||
analysisMode,
|
||||
flandersScopeSelected,
|
||||
officialMapProductsError,
|
||||
officialMapProductsLoading,
|
||||
onDemandProductMap,
|
||||
onOpenDatasetInMap,
|
||||
selectedProjectId,
|
||||
themeDatasetMap,
|
||||
])
|
||||
|
||||
@@ -1528,20 +1646,37 @@ export function MapWorkspace({
|
||||
}
|
||||
|
||||
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
|
||||
let resolvedZones = selectedCoverageZones
|
||||
if (analysisMode === 'current' && selectedProjectId) {
|
||||
const resolvedCoverage = await resolveCoverage({
|
||||
minx: bbox.min_x,
|
||||
miny: bbox.min_y,
|
||||
maxx: bbox.max_x,
|
||||
maxy: bbox.max_y,
|
||||
})
|
||||
if (!resolvedCoverage) {
|
||||
clearThemeInsights()
|
||||
return
|
||||
}
|
||||
resolvedZones = resolvedCoverage.intersected_zones
|
||||
}
|
||||
const resolvedProducts = analysisMode === 'current'
|
||||
? onDemandProductsForZones(resolvedZones)
|
||||
: []
|
||||
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
|
||||
for (const theme of DATA_THEMES) {
|
||||
const onDemandProduct = analysisMode === 'current'
|
||||
? onDemandProductMap.get(theme.id)
|
||||
: null
|
||||
if (onDemandProduct) {
|
||||
availableThemes.push({
|
||||
themeId: theme.id,
|
||||
acquisition: {
|
||||
kind: onDemandProduct.kind,
|
||||
productKey: onDemandProduct.productKey,
|
||||
displayName: onDemandProduct.displayName,
|
||||
},
|
||||
})
|
||||
const onDemandProducts = resolvedProducts.filter((product) => product.theme === theme.id)
|
||||
if (onDemandProducts.length > 0) {
|
||||
for (const onDemandProduct of onDemandProducts) {
|
||||
availableThemes.push({
|
||||
themeId: theme.id,
|
||||
acquisition: {
|
||||
kind: onDemandProduct.kind,
|
||||
productKey: onDemandProduct.productKey,
|
||||
displayName: onDemandProduct.displayName,
|
||||
},
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
@@ -1856,10 +1991,10 @@ export function MapWorkspace({
|
||||
: activeTheme.description}
|
||||
</small>
|
||||
</div>
|
||||
{officialMapProductsLoading && flandersScopeSelected ? (
|
||||
{officialMapProductsLoading && selectedProjectId ? (
|
||||
<p className="geo-data-notice">Beschikbare Vlaamse kaartbronnen worden gecontroleerd…</p>
|
||||
) : null}
|
||||
{officialMapProductsError && flandersScopeSelected ? (
|
||||
{officialMapProductsError && selectedProjectId ? (
|
||||
<p className="error">{officialMapProductsError}</p>
|
||||
) : null}
|
||||
|
||||
@@ -1877,7 +2012,12 @@ export function MapWorkspace({
|
||||
(item) =>
|
||||
item.source_name === 'digitaal_vlaanderen_dhmv'
|
||||
&& datasetProductKey(item) === productKey
|
||||
&& datasetCoversSelectedArea(item, selectedMapAreaId, regionalScopeSelected),
|
||||
&& datasetCoversSelectedArea(
|
||||
item,
|
||||
selectedMapAreaId,
|
||||
selectedMapArea?.name,
|
||||
regionalScopeSelected,
|
||||
),
|
||||
)
|
||||
if (dataset) {
|
||||
onOpenDatasetInMap(dataset)
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
bboxesEqual,
|
||||
normalizeBboxFromCorners,
|
||||
parseBboxInput,
|
||||
productCoversZones,
|
||||
resultMetricLabel,
|
||||
selectedAreaCoverageZones,
|
||||
selectionAreaSquareMetres,
|
||||
} from './mapWorkspaceUtils'
|
||||
import type { VectorSelectionResponse } from '../../types'
|
||||
@@ -47,4 +49,18 @@ describe('map workspace selection guards', () => {
|
||||
} as unknown as VectorSelectionResponse
|
||||
expect(resultMetricLabel(result)).toBe('14,24 ha')
|
||||
})
|
||||
|
||||
it('maps national work areas to regional provider zones without merging sources', () => {
|
||||
expect(selectedAreaCoverageZones('Belgium land')).toEqual([
|
||||
'belgium',
|
||||
'flanders',
|
||||
'wallonia',
|
||||
'brussels',
|
||||
])
|
||||
expect(selectedAreaCoverageZones('RC Ardennes inland')).toEqual(['wallonia'])
|
||||
expect(selectedAreaCoverageZones('Brussels-Capital Region')).toEqual(['brussels'])
|
||||
expect(selectedAreaCoverageZones('Language boundary')).toEqual(['flanders', 'wallonia'])
|
||||
expect(productCoversZones(['wallonia'], ['flanders', 'wallonia'])).toBe(true)
|
||||
expect(productCoversZones(['brussels'], ['wallonia'])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,28 @@ import {
|
||||
const MOL_PROJECT_NAME = 'Mol Municipality Workbench'
|
||||
const KEMPEN_PROJECT_NAME = 'Kempen Regional Workbench'
|
||||
|
||||
export function selectedAreaCoverageZones(areaName: string | null | undefined): string[] | null {
|
||||
const normalized = String(areaName ?? '').toLowerCase()
|
||||
if (!normalized) return null
|
||||
if (normalized.includes('coast land-sea')) return ['flanders', 'belgian_north_sea']
|
||||
if (normalized.includes('language boundary')) return ['flanders', 'wallonia']
|
||||
if (normalized.includes('north sea')) {
|
||||
return ['belgian_north_sea', 'territorial_sea', 'exclusive_economic_zone', 'continental_shelf']
|
||||
}
|
||||
if (normalized.includes('territorial sea')) return ['territorial_sea']
|
||||
if (normalized.includes('exclusive economic zone')) return ['exclusive_economic_zone']
|
||||
if (normalized.includes('continental shelf')) return ['continental_shelf']
|
||||
if (normalized.includes('brussels')) return ['brussels']
|
||||
if (normalized.includes('wallonia') || normalized.includes('ardennes')) return ['wallonia']
|
||||
if (normalized.includes('flanders') || normalized.includes('mol') || normalized.includes('kempen')) return ['flanders']
|
||||
if (normalized.includes('belgium land')) return ['belgium', 'flanders', 'wallonia', 'brussels']
|
||||
return null
|
||||
}
|
||||
|
||||
export function productCoversZones(productZones: string[], selectedZones: string[] | null): boolean {
|
||||
return selectedZones === null || selectedZones.some((zone) => productZones.includes(zone))
|
||||
}
|
||||
|
||||
export function operationalScopeProjectLabel(project: ProjectRead): string {
|
||||
if (project.name === MOL_PROJECT_NAME) {
|
||||
return 'Mol'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { datasetsApi } from '../services/api'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { datasetsApi, externalApi } from '../services/api'
|
||||
import { formatError } from '../lib/formatError'
|
||||
import type {
|
||||
DhmvProductRead,
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ThematicRasterProductRead,
|
||||
} from '../types'
|
||||
|
||||
interface OfficialMapProducts {
|
||||
export interface OfficialMapProducts {
|
||||
thematic: ThematicRasterProductRead[]
|
||||
dhmv: DhmvProductRead[]
|
||||
floodHazard: FloodHazardProductRead[]
|
||||
@@ -78,5 +78,26 @@ export function useOfficialMapProducts(selectedProjectId: string | null) {
|
||||
}
|
||||
}, [selectedProjectId])
|
||||
|
||||
return { products, loading, error }
|
||||
const resolveCoverage = useCallback(
|
||||
async (bbox: { minx: number; miny: number; maxx: number; maxy: number }) => {
|
||||
if (!selectedProjectId) {
|
||||
setError('Selecteer eerst een werkruimte.')
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const coverage = await externalApi.resolveCoverage({
|
||||
projectId: selectedProjectId,
|
||||
bbox,
|
||||
})
|
||||
setError(null)
|
||||
return coverage
|
||||
} catch (requestError) {
|
||||
setError(formatError(requestError, 'De regionale databronnen konden niet veilig worden bepaald.'))
|
||||
return null
|
||||
}
|
||||
},
|
||||
[selectedProjectId],
|
||||
)
|
||||
|
||||
return { products, loading, error, resolveCoverage }
|
||||
}
|
||||
|
||||
@@ -393,11 +393,11 @@ export interface OfficialVectorAcquireRequest {
|
||||
export interface OfficialVectorProductRead {
|
||||
key: string
|
||||
display_name: string
|
||||
theme: 'nature_value' | 'soil'
|
||||
theme: 'buildings' | 'roads' | 'water' | 'parcels' | 'nature_value' | 'soil'
|
||||
provider: string
|
||||
source_name: string
|
||||
reference_layer_name: string
|
||||
service_type: 'OGC API Features' | 'WFS 2.0'
|
||||
service_type: 'OGC API Features' | 'WFS 2.0' | 'ArcGIS REST'
|
||||
collection: string
|
||||
geometry_types: string[]
|
||||
source_crs: string
|
||||
@@ -408,6 +408,7 @@ export interface OfficialVectorProductRead {
|
||||
attribution: string
|
||||
license_note: string
|
||||
limitation_message: string
|
||||
coverage_zones: string[]
|
||||
}
|
||||
|
||||
export interface TerrainSelectionResponse {
|
||||
|
||||
Reference in New Issue
Block a user