import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react' import { BoxSelect, ChevronLeft, ChevronRight, MapPinned, Play, Search, SlidersHorizontal, Trash2 } from 'lucide-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' import { useOfficialMapProducts } from '../../hooks/useOfficialMapProducts' import { useTemporalComparison } from '../../hooks/useTemporalComparison' import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay' import { TemporalTrendChart } from './TemporalTrendChart' import { MunicipalitySearch } from './MunicipalitySearch' import { LiveAnalysisJourney } from './LiveAnalysisJourney' import { SecondaryDisplayTarget } from '../shell/SecondaryDisplay' import { terrainImageUrl } from '../../lib/terrainImage' import { floodHazardImageUrl } from '../../lib/floodHazardImage' import { thematicRasterImageUrl, walousRasterImageUrl } from '../../lib/thematicRaster' import { bathymetryRasterImageUrl } from '../../lib/bathymetryRaster' import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus' import { MAP_ANALYSIS_BUDGET_MS, exceedsPerformanceBudget, formatPerformanceDuration, } from '../../lib/performanceBudget' import { bboxToInputState, bboxesEqual, copyText, datasetIntersectsSelection, downloadJsonFile, formatArea, formatBboxLabel, formatPercentage, formatTemporalMetric, getFeatureBBox, getFeatureCollectionBBox, getFeatureGeometrySummary, isMunicipalityAreaName, normalizeBboxFromCorners, operationalScopeProjectLabel, parseBboxInput, persistedDatasetSupportsSelection, productCoversZones, readablePropertyName, resultMetricLabel, safeFileStem, selectedAreaCoverageZones, selectedFeatureCollection, selectionAnalysisScale, selectionAreaSquareMetres, selectionDimensions, selectionFeatureLimit, selectionMetricLabel, splitSelectionBbox, } from './mapWorkspaceUtils' import { COVERAGE_THEME_BY_MAP_THEME, DATA_THEMES, DATA_THEME_MAP_STYLES, DEFAULT_AREA_SELECTION_FILENAME, DEFAULT_SELECTED_FEATURE_FILENAME, EMPTY_TEMPORAL_SERIES, coverageStatusLabel, coverageZoneLabel, datasetCoversSelectedArea, datasetProductKey, floodScenarioLabel, formatDatasetObservation, formatObservationDate, isPartitionedBathymetry, isPartitionedRaster, listThemeTemporalSeries, pickThemeDataset, productSupportsSelection, rasterPartitionsForDataset, themeIdForDataset, type DataTheme, type DataThemeId, type OnDemandMapProduct, type PlannedOnDemandMapProduct, type TemporalSeriesGroup, } from './mapWorkspaceThemes' interface MapWorkspaceProps { readOnly?: boolean secondaryResultsContainer?: HTMLElement | null selectedProjectId: string | null projects: ProjectRead[] areas: AreaRead[] selectedMapAreaId: string areaFeatureCollection: GeoJSON.FeatureCollection | null mapFeatureCollection: GeoJSON.FeatureCollection | null qualityEvidenceGeoJson?: GeoJSON.FeatureCollection | null qualityEvidenceFeatureCount?: number qualityEvidenceLoading?: boolean qualityEvidenceError?: string | null qualityEvidenceWarnings?: string[] mapLayerLabel: string mapLayerSourceLabel: string mapLayerProvenance: string mapLayerVisible: boolean mapLayerOpacity: number areaLayerVisible: boolean areaLayerOpacity: number mapFeatureCount: number areaFeatureCount: number viewportVectorEnabled: boolean viewportVectorStatus: string | null viewportVectorTone: 'ready' | 'pending' | 'warning' | 'error' fitMapDataOnChange: boolean mapContentMode: 'dataset' | 'analysis' analysisLayerAvailable: boolean selectedMapFeature: GeoJSON.Feature | null selectedFeature?: GeoJSON.Feature | null mapSelectionBbox: VectorSelectionBBox | null mapSelectionResult: VectorSelectionResponse | null mapSelectionLoading: boolean mapSelectionError: string | null coverage: CoverageResolveResponse | null coverageLoading: boolean coverageError: string | null coverageDurationMs: number | null coverageBudgetExceeded: boolean workspaceLoading: boolean workspaceError: string | null selectionExporting: boolean selectionExportError: string | null latestSelectionExportPath: string | null selectionDatasetSaving: boolean selectionDatasetError: string | null latestSelectionDataset: DatasetCreateResponse | null latestSelectionDatasetName: string | null mapQaReferenceDatasets: DatasetCreateResponse[] selectedMapQaReferenceDatasetId: string mapSelectionQaRunning: boolean mapSelectionQaError: string | null mapSelectionQaResult: QaComparisonResult | null latestMapSelectionQualityCheckId: string | null orthophotoAnalysisStage: 'idle' | 'acquiring' | 'detecting' | 'validating' | 'complete' | 'failed' orthophotoAnalysisStatus: string orthophotoAnalysisError: string | null orthophotoAnalysisRunning: boolean orthophotoAnalysisQuality: DetectionQaResult | null orthophotoAnalysisDetectionCount: number | null orthophotoProducts: OrthophotoProductRead[] selectedOrthophotoProductKey: string orthophotoResult: OrthophotoAcquisitionResult | null orthophotoImageUrl: string | null availableMapDatasets: DatasetCreateResponse[] selectedMapDatasetId: string onSelectMapArea: (areaId: string) => void onActivateMunicipality: (niscode: string) => Promise onSetContextSourceLabel: (label: string | null) => void onSetContextLayerLabel: (label: string | null) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onSetAreaLayerVisible: (visible: boolean) => void onSetAreaLayerOpacity: (opacity: number) => void onSetMapLayerVisible: (visible: boolean) => void onSetMapLayerOpacity: (opacity: number) => void onSetMapContentMode: (mode: 'dataset' | 'analysis') => void onSelectMapFeature: (feature: GeoJSON.Feature | null) => void onMapViewportChange: (viewport: MapViewportState) => void onSetMapSelectionBbox: (bbox: VectorSelectionBBox | null) => void onRunMapSelectionExtract: (bbox: VectorSelectionBBox, areaId?: string) => Promise onClearMapSelectionExtract: () => void onExportMapSelection: (bbox: VectorSelectionBBox, areaId?: string) => Promise onPersistMapResult: (payload: MapResultExportRequest) => Promise onDeriveMapSelectionDataset: (bbox: VectorSelectionBBox, areaId?: string) => Promise onSelectMapQaReferenceDataset: (datasetId: string) => void onRunMapSelectionQa: (candidateDataset?: DatasetCreateResponse | null) => Promise onOpenMapSelectionQualityEvidence: () => void onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise onSelectOrthophotoProduct: (productKey: string) => void onClearQualityEvidence?: () => void onRefreshProjectData: () => Promise onOpenAssistant: () => void onOpenExports: () => void } export function MapWorkspace({ readOnly = false, secondaryResultsContainer = null, selectedProjectId, projects, areas, selectedMapAreaId, areaFeatureCollection, mapFeatureCollection, qualityEvidenceGeoJson = null, qualityEvidenceFeatureCount = 0, qualityEvidenceLoading = false, qualityEvidenceError = null, qualityEvidenceWarnings = [], mapLayerLabel, mapLayerSourceLabel, mapLayerProvenance, mapLayerVisible, mapLayerOpacity, areaLayerVisible, areaLayerOpacity, mapFeatureCount, areaFeatureCount, viewportVectorEnabled, viewportVectorStatus, viewportVectorTone, fitMapDataOnChange, mapContentMode, analysisLayerAvailable, selectedMapFeature, selectedFeature = selectedMapFeature, mapSelectionBbox, mapSelectionResult, mapSelectionLoading, mapSelectionError, coverage, coverageLoading, coverageError, coverageDurationMs, coverageBudgetExceeded, workspaceLoading, workspaceError, selectionExporting, selectionExportError, latestSelectionExportPath, selectionDatasetSaving, selectionDatasetError, latestSelectionDataset, latestSelectionDatasetName, mapQaReferenceDatasets, selectedMapQaReferenceDatasetId, mapSelectionQaRunning, mapSelectionQaError, mapSelectionQaResult, latestMapSelectionQualityCheckId, orthophotoAnalysisStage, orthophotoAnalysisStatus, orthophotoAnalysisError, orthophotoAnalysisRunning, orthophotoAnalysisQuality, orthophotoAnalysisDetectionCount, orthophotoProducts, selectedOrthophotoProductKey, orthophotoResult, orthophotoImageUrl, availableMapDatasets, selectedMapDatasetId, onSelectMapArea, onActivateMunicipality, onSetContextSourceLabel, onSetContextLayerLabel, onOpenDatasetInMap, onSetAreaLayerVisible, onSetAreaLayerOpacity, onSetMapLayerVisible, onSetMapLayerOpacity, onSetMapContentMode, onSelectMapFeature, onMapViewportChange, onSetMapSelectionBbox, onRunMapSelectionExtract, onClearMapSelectionExtract, onExportMapSelection, onPersistMapResult, onDeriveMapSelectionDataset, onSelectMapQaReferenceDataset, onRunMapSelectionQa, onOpenMapSelectionQualityEvidence, onRunOrthophotoAnalysis, onSelectOrthophotoProduct, onClearQualityEvidence, onRefreshProjectData, onOpenAssistant, onOpenExports, }: MapWorkspaceProps): JSX.Element { const [advancedMode, setAdvancedMode] = useState(false) useEffect(() => { if (readOnly && advancedMode) setAdvancedMode(false) }, [advancedMode, readOnly]) const [activeThemeId, setActiveThemeId] = useState(() => { const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null return themeIdForDataset(selectedDataset) ?? 'buildings' }) const [selectedThemeIds, setSelectedThemeIds] = useState([]) const [themeFilter, setThemeFilter] = useState('') const [resultsPanelOpen, setResultsPanelOpen] = useState(false) const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied' 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 walloniaScopeSelected = Boolean(selectedCoverageZones?.includes('wallonia')) const { themeInsights, themeInsightsLoading: themeResultsLoading, themeInsightsError: themeResultsError, loadThemeInsights, clearThemeInsights, } = useMapThemeSelectionInsights(selectedProjectId, onRefreshProjectData) const { products: officialMapProducts, loading: officialMapProductsLoading, error: officialMapProductsError, resolveCoverage, resolveCoveragePartitions, } = useOfficialMapProducts(selectedProjectId) const { temporalComparison, temporalComparisonLoading, temporalComparisonError, compareTemporalSnapshots, clearTemporalComparison, } = useTemporalComparison(selectedProjectId) const [analysisMode, setAnalysisMode] = useState<'current' | 'evolution'>('current') const [selectedFloodHazardDatasetId, setSelectedFloodHazardDatasetId] = useState('') const [selectedDhmvProductKey, setSelectedDhmvProductKey] = useState<'dtm_1m' | 'dsm_1m'>('dtm_1m') const [selectedFloodHazardProductKey, setSelectedFloodHazardProductKey] = useState('pluviaal_current_t100') const [selectedTemporalSeriesKey, setSelectedTemporalSeriesKey] = useState('') const [earlierDatasetId, setEarlierDatasetId] = useState('') const [laterDatasetId, setLaterDatasetId] = useState('') const [bboxSelectionMode, setBboxSelectionMode] = useState(false) const [firstSelectionCorner, setFirstSelectionCorner] = useState<[number, number] | null>(null) const [bboxInput, setBboxInput] = useState(bboxToInputState(mapSelectionBbox)) const [fullWorkflowRunning, setFullWorkflowRunning] = useState(false) const [fullWorkflowStatus, setFullWorkflowStatus] = useState('Klaar om de volledige GIS-werkstroom uit te voeren.') const [fullWorkflowError, setFullWorkflowError] = useState(null) const [fullWorkflowMode, setFullWorkflowMode] = useState<'new' | 'reuse'>('new') const [mapAnalysisDurationMs, setMapAnalysisDurationMs] = useState(null) const mapAnalysisRequestSequence = useRef(0) 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') .slice(0, 6) : [] const featureExtractionEntries = featureProperties ? Object.entries(featureProperties).slice(0, 48) : [] const featureGeometrySummary = getFeatureGeometrySummary(selectedMapFeature) const selectedFeatureGeoJson = selectedMapFeature ? selectedFeatureCollection(selectedMapFeature) : null const selectedFeatureBbox = useMemo(() => getFeatureBBox(selectedMapFeature), [selectedMapFeature]) const activeLayerBbox = useMemo(() => getFeatureCollectionBBox(mapFeatureCollection), [mapFeatureCollection]) const selectedAreaBbox = useMemo(() => getFeatureCollectionBBox(areaFeatureCollection), [areaFeatureCollection]) const currentSelectionBbox = parseBboxInput(bboxInput) const areaSelectionFeatures = mapSelectionResult?.geojson.features ?? [] const areaSelectionPreviewFeatures = areaSelectionFeatures.slice(0, 12) const selectedFeatureStem = safeFileStem( featureProperties?.['name'] ?? featureProperties?.['id'] ?? featureProperties?.['source_feature_id'] ?? 'selected-feature', ) const selectedFeatureFilename = selectedFeatureStem === 'selected-feature' ? DEFAULT_SELECTED_FEATURE_FILENAME : `${selectedFeatureStem}.geojson` const selectedMapDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null const usesDefaultOsmBasemap = !import.meta.env.VITE_MAP_STYLE_URL const floodHazardDatasets = useMemo( () => { const scoped = availableMapDatasets .filter( (dataset) => dataset.source_name === 'vmm_flood_hazard' && datasetCoversSelectedArea(dataset, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected), ) .sort((left, right) => floodScenarioLabel(left).localeCompare(floodScenarioLabel(right), 'nl')) if (!regionalScopeSelected) { return scoped } const products = new Map() for (const dataset of scoped) { const key = datasetProductKey(dataset) if (key && !products.has(key)) { products.set(key, dataset) } } return Array.from(products.values()) }, [availableMapDatasets, regionalScopeSelected, selectedMapArea?.name, selectedMapAreaId], ) const themeDatasetMap = useMemo(() => { const result = Object.fromEntries( DATA_THEMES.map((theme) => [ theme.id, pickThemeDataset( availableMapDatasets, theme, selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected, ), ]), ) as Record const selectedFloodHazard = floodHazardDatasets.find( (dataset) => dataset.id === selectedFloodHazardDatasetId || (flandersScopeSelected && datasetProductKey(dataset) === selectedFloodHazardProductKey), ) if (selectedFloodHazard) { result.flood_hazard = selectedFloodHazard } else if (flandersScopeSelected && officialMapProducts.floodHazard.length > 0) { result.flood_hazard = null } if (flandersScopeSelected && officialMapProducts.dhmv.length > 0) { result.elevation = availableMapDatasets.find( (dataset) => dataset.source_name === 'digitaal_vlaanderen_dhmv' && datasetProductKey(dataset) === selectedDhmvProductKey && 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]) { result[product.theme] = null } } } if (flandersScopeSelected && officialMapProducts.grb.length > 0) { for (const product of officialMapProducts.grb) { if (!result[product.key]) { result[product.key] = null } } } if (officialMapProducts.officialVector.length > 0) { for (const product of officialMapProducts.officialVector.filter((item) => productCoversZones(item.coverage_zones, selectedCoverageZones), )) { if (!result[product.theme]) { result[product.theme] = null } } } return result }, [ availableMapDatasets, flandersScopeSelected, floodHazardDatasets, officialMapProducts.dhmv.length, officialMapProducts.spwTerrain, officialMapProducts.floodHazard.length, officialMapProducts.grb, officialMapProducts.officialVector, officialMapProducts.thematic, regionalScopeSelected, selectedDhmvProductKey, selectedFloodHazardDatasetId, selectedFloodHazardProductKey, selectedMapArea?.name, selectedMapAreaId, selectedCoverageZones, walloniaScopeSelected, ]) const themePartitionMap = useMemo( () => Object.fromEntries( DATA_THEMES.map((theme) => [ theme.id, rasterPartitionsForDataset( availableMapDatasets, themeDatasetMap[theme.id], selectedMapAreaId, selectedMapArea?.name, regionalScopeSelected, ), ]), ) as Record, [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] const activeCoverageItems = coverage?.items.filter((item) => item.theme === activeCoverageTheme) ?? [] const coverageCounts = useMemo( () => coverage?.items.reduce>( (counts, item) => ({ ...counts, [item.status]: counts[item.status] + 1 }), { operational: 0, partial: 0, not_configured: 0, unsupported: 0 }, ) ?? { operational: 0, partial: 0, not_configured: 0, unsupported: 0 }, [coverage], ) 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), ) const includesWallonia = Boolean(effectiveZones?.includes('wallonia')) if (includesFlanders) { for (const product of officialMapProducts.thematic) { result.push({ kind: 'thematic_raster', productKey: product.key, displayName: product.display_name, theme: product.theme, availabilityLabel: `${product.native_resolution_m} m · ${product.observation_year} · automatisch bij selectie`, attribution: product.attribution, limitationMessage: product.limitation_message, coverageZones: ['flanders'], }) } for (const product of officialMapProducts.grb) { result.push({ kind: 'grb', productKey: product.key, displayName: product.display_name, theme: product.key, availabilityLabel: 'officiële vectorbron · automatisch bij selectie', attribution: product.attribution, limitationMessage: product.limitation_message, coverageZones: ['flanders'], }) } for (const source of officialMapProducts.bathymetry.filter( (item) => item.key === 'vha_inland_profiles' && item.acquisition_supported && item.configured, )) { result.push({ kind: 'bathymetry_profiles', productKey: source.key, displayName: source.display_name, theme: 'bathymetry', availabilityLabel: 'historische profielpunten · automatisch bij selectie', attribution: source.attribution, limitationMessage: source.limitation_message, coverageZones: ['flanders'], }) } } 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), )) { result.push({ kind: 'official_vector', productKey: product.key, displayName: product.display_name, theme: product.theme, availabilityLabel: `${product.observation_label} · officiële vectorbron · automatisch bij selectie`, attribution: product.attribution, limitationMessage: product.limitation_message, coverageZones: product.coverage_zones, }) } 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} · automatisch bij selectie`, attribution: dhmvProduct.attribution, limitationMessage: dhmvProduct.limitation_message, 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, ) : 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} · automatisch bij selectie`, attribution: floodProduct.attribution, limitationMessage: floodProduct.limitation_message, coverageZones: ['flanders'], }) } return result }, [ activeScopeProject?.name, officialMapProducts, selectedCoverageZones, selectedDhmvProductKey, selectedFloodHazardProductKey, ]) const onDemandProductMap = useMemo(() => { const result = new Map() const productsByTheme = new Map() for (const product of onDemandProductsForZones(selectedCoverageZones)) { productsByTheme.set(product.theme, [...(productsByTheme.get(product.theme) ?? []), product]) } for (const [theme, products] of productsByTheme) { if (products.length === 1) { const product = products[0] const scopeZones = product.coverageZones.filter((zone) => selectedCoverageZones?.includes(zone) ?? true) result.set(theme, selectedCoverageZones && selectedCoverageZones.length > 1 ? { ...product, availabilityLabel: `${product.availabilityLabel} · alleen ${scopeZones.map(coverageZoneLabel).join(', ')}`, } : product) continue } const coverageZones = Array.from(new Set( products.flatMap((product) => product.coverageZones) .filter((zone) => selectedCoverageZones?.includes(zone) ?? true), )) result.set(theme, { ...products[0], displayName: 'Officiële bron per regio', availabilityLabel: `${coverageZones.map(coverageZoneLabel).join(', ')} · bron wordt na selectie bepaald`, attribution: 'Officiële Belgische en gewestelijke databronnen', limitationMessage: 'GeoIntel bepaalt na de getekende selectie welke regionale bron van toepassing is en voegt alleen semantisch gelijkwaardige resultaten samen.', coverageZones, }) } return result }, [onDemandProductsForZones, selectedCoverageZones]) const mapSelectionScale = mapSelectionBbox ? selectionAnalysisScale(mapSelectionBbox) : null const selectionRelevantThemes = useMemo(() => { if (!mapSelectionBbox || !coverage) { return DATA_THEMES } const boundedThemes = new Set( (mapSelectionBbox ? onDemandProductsForZones(coverage.intersected_zones).filter( (product) => productSupportsSelection(product, mapSelectionBbox), ) : []) .map((product) => product.theme), ) return DATA_THEMES.filter((theme) => { if (boundedThemes.has(theme.id)) { return true } const dataset = themeDatasetMap[theme.id] if (!dataset || !persistedDatasetSupportsSelection(dataset, mapSelectionBbox)) { return false } const coverageTheme = COVERAGE_THEME_BY_MAP_THEME[theme.id] return coverage.items.some( (item) => item.theme === coverageTheme && item.status === 'operational', ) }) }, [coverage, mapSelectionBbox, mapSelectionScale, onDemandProductsForZones, themeDatasetMap]) const activeThemeSupportsCurrentSelection = selectionRelevantThemes.some((theme) => theme.id === activeTheme.id) const activeOnDemandMapProduct = mapSelectionScale === 'overview' || themeDatasetMap[activeTheme.id] ? null : onDemandProductMap.get(activeTheme.id) ?? null const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id] const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection) const liveJourneyError = mapSelectionError ?? themeResultsError ?? temporalComparisonError ?? orthophotoAnalysisError ?? coverageError ?? workspaceError const liveJourneyHasResult = Boolean( mapSelectionResult || themeInsights.length > 0 || temporalComparison || orthophotoResult || analysisOverlayActive, ) const liveJourneyVerified = Boolean(mapSelectionQaResult || orthophotoAnalysisQuality) const liveJourneyProcessing = Boolean( mapSelectionLoading || themeResultsLoading || temporalComparisonLoading || orthophotoAnalysisRunning, ) const liveJourneyValidating = Boolean( mapSelectionQaRunning || orthophotoAnalysisStage === 'validating', ) const liveJourneyStatus = orthophotoAnalysisStatus || (temporalComparisonLoading ? 'Officiële meetmomenten vergelijken…' : '') || (themeResultsLoading ? 'Begrensde bronnen verwerken…' : '') || (mapSelectionLoading ? 'Objecten binnen de selectie ophalen…' : '') || (liveJourneyHasResult ? 'Resultaat op de kaart beschikbaar' : 'Klaar om de selectie te verwerken') const liveJourneyResultLabel = liveJourneyVerified ? 'Kwaliteitsbewijs beschikbaar' : latestMapSelectionQualityCheckId ? 'Bewaard kwaliteitsbewijs beschikbaar' : 'Controleerbaar resultaat' const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null const orthophotoImageOverlay = useMemo( () => orthophotoResult && orthophotoImageUrl && orthophotoResult.bbox_epsg4326.length === 4 ? { url: orthophotoImageUrl, bbox: orthophotoResult.bbox_epsg4326 as [number, number, number, number], label: orthophotoResult.display_name, opacity: 0.9, } : null, [orthophotoImageUrl, orthophotoResult], ) const activeThemeDataset = themeDatasetMap[activeTheme.id] const activeThemePartitions = themePartitionMap[activeTheme.id] const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset) const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset) const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive const onDemandThemeActive = analysisMode === 'current' && Boolean(activeOnDemandMapProduct) const regionalOnDemandThemeActive = regionalScopeSelected && onDemandThemeActive const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThemeActive const visibleThemes = useMemo(() => { const query = themeFilter.trim().toLocaleLowerCase('nl-BE') if (!query) return DATA_THEMES return DATA_THEMES.filter((theme) => theme.label.toLocaleLowerCase('nl-BE').includes(query)) }, [themeFilter]) const selectedThemes = useMemo( () => DATA_THEMES.filter((theme) => selectedThemeIds.includes(theme.id)), [selectedThemeIds], ) useEffect(() => { if ( analysisMode !== 'current' || activeThemeAvailable || ( selectedProjectId && officialMapProductsLoading && onDemandProductMap.size === 0 && !officialMapProductsError ) ) { return } const fallbackTheme = DATA_THEMES.find((theme) => 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)), ) if (!fallbackTheme) { return } setActiveThemeId(fallbackTheme.id) const fallbackDataset = themeDatasetMap[fallbackTheme.id] if (fallbackDataset) { onOpenDatasetInMap(fallbackDataset) } }, [ activeThemeAvailable, analysisMode, flandersScopeSelected, officialMapProductsError, officialMapProductsLoading, onDemandProductMap, onOpenDatasetInMap, selectedProjectId, themeDatasetMap, ]) const terrainImageOverlays = useMemo( () => activeTheme.id === 'elevation' && selectedProjectId ? activeThemePartitions.flatMap((dataset) => { const bounds = dataset.source_metadata?.['bbox_epsg4326'] return ['digitaal_vlaanderen_dhmv', 'spw_terrain'].includes(dataset.source_name ?? '') && Array.isArray(bounds) && bounds.length === 4 ? [{ url: terrainImageUrl(selectedProjectId, dataset.id), bbox: bounds.map(Number) as [number, number, number, number], label: getDatasetDisplayName(dataset), opacity: 0.82, }] : [] }) : [], [activeTheme.id, activeThemePartitions, selectedProjectId], ) const floodHazardImageOverlays = useMemo( () => activeTheme.id === 'flood_hazard' && selectedProjectId ? activeThemePartitions.flatMap((dataset) => { const bounds = dataset.source_metadata?.['bbox_epsg4326'] return dataset.source_name === 'vmm_flood_hazard' && Array.isArray(bounds) && bounds.length === 4 ? [{ url: floodHazardImageUrl(selectedProjectId, dataset.id), bbox: bounds.map(Number) as [number, number, number, number], label: floodScenarioLabel(dataset), opacity: 0.82, }] : [] }) : [], [activeTheme.id, activeThemePartitions, selectedProjectId], ) const thematicRasterBounds = activeThemeDataset?.source_name === 'department_omgeving_thematic_raster' ? activeThemeDataset.source_metadata?.['bbox_epsg4326'] : null const thematicRasterImageOverlays = useMemo( () => activeThemeDataset?.source_name === 'department_omgeving_thematic_raster' && selectedProjectId && Array.isArray(thematicRasterBounds) && thematicRasterBounds.length === 4 ? [{ url: thematicRasterImageUrl(selectedProjectId, activeThemeDataset.id), bbox: thematicRasterBounds.map(Number) as [number, number, number, number], label: getDatasetDisplayName(activeThemeDataset), opacity: 0.78, }] : [], [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 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( () => bathymetryRasterImageOverlays.length > 0 ? bathymetryRasterImageOverlays : walousRasterImageOverlays.length > 0 ? walousRasterImageOverlays : thematicRasterImageOverlays.length > 0 ? thematicRasterImageOverlays : floodHazardImageOverlays.length > 0 ? floodHazardImageOverlays : terrainImageOverlays.length > 0 ? terrainImageOverlays : orthophotoImageOverlay ? [orthophotoImageOverlay] : [], [bathymetryRasterImageOverlays, floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays, walousRasterImageOverlays], ) const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length const themeTemporalSeriesMap = useMemo( () => Object.fromEntries( DATA_THEMES.map((theme) => [theme.id, listThemeTemporalSeries(availableMapDatasets, theme, selectedMapAreaId)]), ) as Record, [availableMapDatasets, selectedMapAreaId], ) const activeTemporalSeriesGroups = themeTemporalSeriesMap[activeTheme.id] const availableEvolutionThemes = DATA_THEMES.filter((theme) => themeTemporalSeriesMap[theme.id].some((group) => group.items.length >= 2), ) const activeTemporalSeriesGroup = activeTemporalSeriesGroups.find((group) => group.key === selectedTemporalSeriesKey) ?? activeTemporalSeriesGroups[0] const activeTemporalSeries = activeTemporalSeriesGroup?.items ?? EMPTY_TEMPORAL_SERIES const earlierTemporalOptions = activeTemporalSeries.slice(0, -1) const selectedEarlierSnapshot = activeTemporalSeries.find((dataset) => dataset.id === earlierDatasetId) const selectedLaterSnapshot = activeTemporalSeries.find((dataset) => dataset.id === laterDatasetId) const selectedEarlierTime = new Date(selectedEarlierSnapshot?.observed_at ?? 0).getTime() const laterTemporalOptions = activeTemporalSeries.filter( (dataset) => new Date(dataset.observed_at ?? 0).getTime() > selectedEarlierTime, ) const temporalSelectionValid = Boolean( selectedEarlierSnapshot && selectedLaterSnapshot && selectedEarlierSnapshot.id !== selectedLaterSnapshot.id && selectedEarlierTime < new Date(selectedLaterSnapshot.observed_at ?? 0).getTime(), ) const activeSeriesIsDailyGrb = activeTemporalSeries.length >= 2 && activeTemporalSeries.every((dataset) => dataset.source_name === 'grb') && new Date(activeTemporalSeries[activeTemporalSeries.length - 1].observed_at ?? 0).getTime() - new Date(activeTemporalSeries[0].observed_at ?? 0).getTime() <= 7 * 24 * 60 * 60 * 1000 useEffect(() => { const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null : regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen' : activeOnDemandMapProduct?.displayName ?? null onSetContextSourceLabel(contextSourceLabel) return () => onSetContextSourceLabel(null) }, [ activeTemporalSeriesGroup?.label, activeOnDemandMapProduct?.displayName, analysisMode, onSetContextSourceLabel, regionalBathymetryThemeActive, ]) const themeResults = useMemo( () => themeInsights.flatMap((insight) => { const theme = DATA_THEMES.find((candidate) => candidate.id === insight.themeId) return theme ? [{ theme, dataset: insight.dataset, result: insight.result }] : [] }), [themeInsights], ) const activeThemeInsight = themeResults.find((item) => item.theme.id === activeThemeId) const activeResultDataset = activeThemeInsight?.dataset ?? activeThemeDataset const activeSelectionResult = activeThemeInsight?.result ?? (!regionalPartitionedThemeActive && selectedMapDataset?.id === activeThemeDataset?.id ? mapSelectionResult : null) useEffect(() => { const contextLayerLabel = analysisMode === 'current' && activeOnDemandMapProduct ? activeSelectionResult ? `${( activeSelectionResult.total_feature_count ?? activeSelectionResult.feature_count ).toLocaleString('nl-BE')} objecten gemeten` : 'Automatisch bij selectie' : null onSetContextLayerLabel(contextLayerLabel) return () => onSetContextLayerLabel(null) }, [ activeOnDemandMapProduct, activeSelectionResult, analysisMode, onSetContextLayerLabel, ]) const explorerMapFeatureCollection = regionalBathymetryThemeActive ? null : analysisMode === 'evolution' && temporalComparison?.geojson.features.length ? temporalComparison.geojson : onDemandThemeActive ? null : mapFeatureCollection const explorerSelectionFeatureCollection = analysisMode === 'current' ? activeSelectionResult?.geojson ?? null : null const selectedAreaSquareMetres = useMemo( () => bboxesEqual(mapSelectionBbox, selectedAreaBbox) && selectedMapArea?.area_m2 ? selectedMapArea.area_m2 : selectionAreaSquareMetres(mapSelectionBbox), [mapSelectionBbox, selectedAreaBbox, selectedMapArea?.area_m2], ) const selectionScaleNotice = useMemo(() => { if (!mapSelectionBbox || !mapSelectionScale) return null const dimensions = selectionDimensions(mapSelectionBbox) const widthKm = dimensions.widthMetres / 1000 const heightKm = dimensions.heightMetres / 1000 if (mapSelectionScale === 'regional') { const partitionCount = splitSelectionBbox(mapSelectionBbox).length return `Regionale selectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. De server verwerkt dit thema hervatbaar over ${partitionCount} of meer bronafhankelijke partities en presenteert één gezamenlijke status.` } if (mapSelectionScale === 'overview') { return `Overzichtsselectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. De server kiest automatisch bronlimieten, checkpoints en partities; resolutie en beschikbare dekking blijven die van de officiële bron.` } return null }, [mapSelectionBbox, mapSelectionScale]) const selectedResultTotal = activeSelectionResult?.total_feature_count ?? activeSelectionResult?.feature_count ?? 0 const selectedDensity = selectedAreaSquareMetres && selectedAreaSquareMetres > 0 ? selectedResultTotal / (selectedAreaSquareMetres / 1_000_000) : null const activeMetricValue = activeSelectionResult?.summary?.metric_value ?? selectedResultTotal const activeMetricUnit = activeSelectionResult?.summary?.metric_unit ?? 'objecten' const activeMetricLabel = activeSelectionResult?.summary?.metric_label ?? activeTheme.shortLabel const activeSupportingMetrics = (activeSelectionResult?.summary?.metrics ?? []).filter( (metric) => metric.metric_key !== activeSelectionResult?.summary?.primary_metric_key, ) 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' || activeMetricUnit === 'm DNG' ? terrainReliefMetric ? `${terrainReliefMetric.metric_value.toLocaleString('nl-BE', { maximumFractionDigits: 2 })} m reliëf` : null : activeMetricUnit === 'inwoners' ? populationDensityMetric ? selectionMetricLabel(populationDensityMetric) : null : activeMetricUnit.startsWith('score') ? scoreMedianMetric ? selectionMetricLabel(scoreMedianMetric) : null : selectedAreaSquareMetres && selectedAreaSquareMetres > 0 ? activeMetricUnit === 'ha' ? `${((activeMetricValue * 10_000) / selectedAreaSquareMetres * 100).toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% dekking` : `${(activeMetricValue / (selectedAreaSquareMetres / 1_000_000)).toLocaleString('nl-BE', { maximumFractionDigits: 1 })} ${activeMetricUnit} / km2` : null const activeSecondaryLabel = activeMetricUnit === 'ha' ? 'Aandeel selectie' : activeMetricUnit === 'm TAW' || activeMetricUnit === 'm DNG' ? 'Reliëf' : activeMetricUnit === 'inwoners' ? 'Gemiddelde dichtheid' : activeMetricUnit.startsWith('score') ? 'Mediaan' : 'Dichtheid' const selectedResultProperties = useMemo(() => { const keys = new Map>() for (const feature of activeSelectionResult?.geojson.features ?? []) { for (const [key, value] of Object.entries(feature.properties ?? {})) { if (value === null || value === undefined || typeof value === 'object' || key.endsWith('_id')) { continue } const values = keys.get(key) ?? new Set() if (values.size < 4) { values.add(String(value)) } keys.set(key, values) } } return Array.from(keys.entries()) .filter(([, values]) => values.size > 0) .slice(0, 8) .map(([key, values]) => ({ key, values: Array.from(values) })) }, [activeSelectionResult]) useEffect(() => { setBboxInput(bboxToInputState(mapSelectionBbox)) }, [mapSelectionBbox]) useEffect(() => { setSelectedTemporalSeriesKey((current) => activeTemporalSeriesGroups.some((group) => group.key === current) ? current : activeTemporalSeriesGroups[0]?.key ?? '', ) }, [activeTemporalSeriesGroups]) useEffect(() => { const selected = floodHazardDatasets.find( (dataset) => datasetProductKey(dataset) === selectedFloodHazardProductKey, ) if (selected) { if (selected.id !== selectedFloodHazardDatasetId) { setSelectedFloodHazardDatasetId(selected.id) } return } if (flandersScopeSelected && officialMapProducts.floodHazard.length > 0) { setSelectedFloodHazardDatasetId('') return } const preferred = floodHazardDatasets.find( (dataset) => dataset.source_metadata?.['product_key'] === 'pluviaal_current_t100', ) ?? floodHazardDatasets[0] setSelectedFloodHazardDatasetId(preferred?.id ?? '') if (preferred && datasetProductKey(preferred)) { setSelectedFloodHazardProductKey(datasetProductKey(preferred)) } }, [ flandersScopeSelected, floodHazardDatasets, officialMapProducts.floodHazard.length, selectedFloodHazardDatasetId, selectedFloodHazardProductKey, ]) useEffect(() => { const first = activeTemporalSeries[0] const last = activeTemporalSeries[activeTemporalSeries.length - 1] setEarlierDatasetId(first?.id ?? '') setLaterDatasetId(last?.id ?? '') clearTemporalComparison() }, [activeTemporalSeries]) useEffect(() => { if (advancedMode || !activeThemeDataset || selectedMapDataset?.id === activeThemeDataset.id) { return } onOpenDatasetInMap(activeThemeDataset) }, [activeTheme, activeThemeDataset, advancedMode, onOpenDatasetInMap, selectedMapDataset]) useEffect(() => { if (!selectedMapDataset) { return } const selectedProductKey = datasetProductKey(selectedMapDataset) if ( selectedMapDataset.source_name === 'digitaal_vlaanderen_dhmv' && (selectedProductKey === 'dtm_1m' || selectedProductKey === 'dsm_1m') ) { setSelectedDhmvProductKey(selectedProductKey) } if (selectedMapDataset.source_name === 'vmm_flood_hazard' && selectedProductKey) { setSelectedFloodHazardProductKey(selectedProductKey) setSelectedFloodHazardDatasetId(selectedMapDataset.id) } const matchingThemeId = themeIdForDataset(selectedMapDataset) if (matchingThemeId) { setActiveThemeId(matchingThemeId) } }, [selectedMapDataset]) const downloadSelectedMapFeature = () => { if (!selectedFeatureGeoJson) { return } downloadJsonFile(selectedFeatureFilename, selectedFeatureGeoJson, 'application/geo+json') } const copySelectedMapFeatureProperties = () => { copyText(JSON.stringify(featureProperties ?? {}, null, 2)) } const setSelectionBbox = (bbox: VectorSelectionBBox | null) => { onSetMapSelectionBbox(bbox) setBboxInput(bboxToInputState(bbox)) } const areaIdForSelection = (bbox: VectorSelectionBBox | null): string | undefined => ( bbox && selectedMapArea ? selectedMapArea.id : undefined ) const startBboxSelection = () => { setFirstSelectionCorner(null) clearThemeInsights() clearTemporalComparison() setResultsPanelOpen(false) setBboxSelectionMode(true) } const handleMapCoordinateSelect = (coordinate: [number, number]) => { if (!firstSelectionCorner) { setFirstSelectionCorner(coordinate) return } const bbox = normalizeBboxFromCorners(firstSelectionCorner, coordinate) setSelectionBbox(bbox) setFirstSelectionCorner(null) setBboxSelectionMode(false) clearThemeInsights() clearTemporalComparison() setResultsPanelOpen(false) } const runAreaExtract = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } void analyzeSelection(bbox, areaIdForSelection(bbox)) } const clearAreaSelection = () => { mapAnalysisRequestSequence.current += 1 setMapAnalysisDurationMs(null) setBboxSelectionMode(false) setFirstSelectionCorner(null) setBboxInput(bboxToInputState(null)) setResultsPanelOpen(false) clearThemeInsights() clearTemporalComparison() onClearMapSelectionExtract() } const handleSelectMapArea = (areaId: string) => { clearAreaSelection() onSelectMapArea(areaId) } const downloadAreaSelection = () => { if (!mapSelectionResult) { return } downloadJsonFile(DEFAULT_AREA_SELECTION_FILENAME, mapSelectionResult.geojson, 'application/geo+json') } const copyAreaSelection = () => { copyText(JSON.stringify(mapSelectionResult?.geojson ?? { type: 'FeatureCollection', features: [] }, null, 2)) } const downloadActiveThemeResult = () => { if (!activeSelectionResult) { return } if (activeResultDataset?.dataset_type === 'raster') { downloadJsonFile(`${activeTheme.id}-analysis.json`, { project_id: selectedProjectId, area_id: areaIdForSelection(mapSelectionBbox) ?? null, area_name: selectedMapArea?.name ?? null, theme: activeTheme, dataset_id: activeResultDataset.id, dataset_name: activeResultDataset.name, source_name: activeResultDataset.source_name, result: activeSelectionResult, }) return } downloadJsonFile(`${activeTheme.id}-selection.geojson`, activeSelectionResult.geojson, 'application/geo+json') } const copyActiveThemeResult = () => { copyText(JSON.stringify(activeSelectionResult ?? {}, null, 2)) } const downloadTemporalComparison = () => { if (!temporalComparison) { return } downloadJsonFile(`${activeTheme.id}-evolution-${temporalComparison.earlier.observed_at.slice(0, 10)}-${temporalComparison.later.observed_at.slice(0, 10)}.json`, temporalComparison) } const copyTemporalComparison = () => { copyText(JSON.stringify(temporalComparison ?? {}, null, 2)) } const persistActiveResultAndOpenDownloads = async () => { if (!selectedProjectId || !mapSelectionBbox) { onOpenExports() return } const areaId = areaIdForSelection(mapSelectionBbox) const payload: MapResultExportRequest | null = analysisMode === 'evolution' ? temporalComparison && earlierDatasetId && laterDatasetId ? { project_id: selectedProjectId, mode: 'evolution', bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' }, earlier_dataset_id: earlierDatasetId, later_dataset_id: laterDatasetId, area_id: areaId, theme_id: activeTheme.id, name: `${activeTheme.id}-evolution`, } : null : activeSelectionResult && activeResultDataset ? { project_id: selectedProjectId, mode: 'current', bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' }, dataset_id: activeResultDataset.id, area_id: areaId, partitioned: regionalPartitionedThemeActive, product_key: regionalRasterThemeActive ? String(activeResultDataset.source_metadata?.['product_key'] ?? '') || undefined : undefined, partition_scope_key: regionalBathymetryThemeActive ? String(activeResultDataset.source_metadata?.['partition_scope_key'] ?? 'flanders') : undefined, theme_id: activeTheme.id, name: `${activeTheme.id}-analysis`, } : null if (!payload) { onOpenExports() return } const persisted = await onPersistMapResult(payload) if (persisted) { onOpenExports() } } const saveAreaSelectionExport = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } onExportMapSelection(bbox, areaIdForSelection(bbox)) } const saveAreaSelectionDataset = () => { const bbox = parseBboxInput(bboxInput) if (!bbox) { return } onDeriveMapSelectionDataset(bbox, areaIdForSelection(bbox)) } const openSelectedDatabaseLayer = (datasetId: string) => { const dataset = availableMapDatasets.find((item) => item.id === datasetId) if (dataset) { onOpenDatasetInMap(dataset) } } const selectDataTheme = (theme: DataTheme) => { const temporalGroup = themeTemporalSeriesMap[theme.id][0] const dataset = analysisMode === 'evolution' ? temporalGroup?.items[temporalGroup.items.length - 1] ?? null : themeDatasetMap[theme.id] const onDemandProduct = analysisMode === 'current' ? onDemandProductMap.get(theme.id) : null if (!dataset && !onDemandProduct) { return } setSelectedThemeIds((current) => ( current.includes(theme.id) ? current.filter((themeId) => themeId !== theme.id) : [...current, theme.id] )) setActiveThemeId(theme.id) clearThemeInsights() clearTemporalComparison() if (dataset) { onOpenDatasetInMap(dataset) } } const setExplorerMode = (mode: 'current' | 'evolution') => { setAnalysisMode(mode) setSelectedThemeIds([]) setResultsPanelOpen(false) clearThemeInsights() clearTemporalComparison() if (mode !== 'evolution' || activeTemporalSeriesGroups.length > 0) { return } const fallbackTheme = availableEvolutionThemes[0] const fallbackGroup = fallbackTheme ? themeTemporalSeriesMap[fallbackTheme.id][0] : null const fallbackDataset = fallbackGroup?.items[fallbackGroup.items.length - 1] if (fallbackTheme && fallbackDataset) { setActiveThemeId(fallbackTheme.id) onOpenDatasetInMap(fallbackDataset) } } const handleAnalysisModeKeyDown = ( event: KeyboardEvent, mode: 'current' | 'evolution', ) => { if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return event.preventDefault() const nextMode = event.key === 'ArrowLeft' || event.key === 'Home' ? 'current' : event.key === 'ArrowRight' || event.key === 'End' ? 'evolution' : mode setExplorerMode(nextMode) window.requestAnimationFrame(() => { document.getElementById(`geo-analysis-tab-${nextMode}`)?.focus() }) } const loadSelectedThemeResult = async (bbox: VectorSelectionBBox, areaId?: string) => { let resolvedZones = selectedCoverageZones const scale = selectionAnalysisScale(bbox) 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 } let resolvedProducts: PlannedOnDemandMapProduct[] = [] if (analysisMode === 'current') { const zoneProducts = resolvedZones ? onDemandProductsForZones(resolvedZones) : [] if (scale === 'detail' || !selectedProjectId || scale === 'overview') { resolvedProducts = zoneProducts .filter((product) => productSupportsSelection(product, bbox)) .map((product) => ({ ...product, acquisitionBboxes: [bbox], })) } else { const detailTiles = splitSelectionBbox(bbox) const tileCoverage = await resolveCoveragePartitions(detailTiles) if (!tileCoverage) { clearThemeInsights() return } const grouped = new Map() for (const item of tileCoverage) { for (const product of onDemandProductsForZones(item.coverage.intersected_zones)) { if (product.kind === 'thematic_raster' || !productSupportsSelection(product, bbox)) continue const key = `${product.kind}:${product.productKey}` const existing = grouped.get(key) if (existing) { existing.acquisitionBboxes.push(item.bbox) } else { grouped.set(key, { ...product, acquisitionBboxes: [item.bbox] }) } } } for (const product of zoneProducts.filter( (candidate) => candidate.kind === 'thematic_raster' && productSupportsSelection(candidate, bbox), )) { grouped.set(`${product.kind}:${product.productKey}`, { ...product, acquisitionBboxes: [bbox], }) } resolvedProducts = [...grouped.values()] } } const availableThemes: Array> = [] const resultFeatureLimit = selectionFeatureLimit(bbox) for (const theme of selectedThemes) { const dataset = themeDatasetMap[theme.id] const partitioned = Boolean( dataset && regionalScopeSelected && (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)), ) const coveringPartitions = partitioned ? themePartitionMap[theme.id].filter((partition) => datasetIntersectsSelection(partition, bbox)) : [] const persistedCoverageAvailable = Boolean( dataset && persistedDatasetSupportsSelection(dataset, bbox) && (!partitioned || coveringPartitions.length > 0), ) if (dataset && persistedCoverageAvailable) { availableThemes.push({ themeId: theme.id, dataset, datasetIds: coveringPartitions.map((partition) => partition.id), featureLimit: resultFeatureLimit, partitioned, }) continue } 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, historyProductKeys: onDemandProduct.historyProductKeys, coverageZone: onDemandProduct.coverageZones.find((zone) => resolvedZones?.includes(zone)), serverOrchestrated: scale !== 'detail', }, acquisitionBboxes: onDemandProduct.acquisitionBboxes, featureLimit: resultFeatureLimit, }) } continue } } await loadThemeInsights(bbox, availableThemes, areaId) } const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => { if (analysisMode === 'current' && selectedThemes.length === 0) { return } setResultsPanelOpen(true) const requestId = mapAnalysisRequestSequence.current + 1 mapAnalysisRequestSequence.current = requestId const startedAt = Date.now() setMapAnalysisDurationMs(null) setSelectionBbox(bbox) const tasks: Array> = analysisMode === 'current' ? [loadSelectedThemeResult(bbox, areaId)] : [] const activeDatasetSupportsSelection = !activeThemeDataset || persistedDatasetSupportsSelection(activeThemeDataset, bbox) if ( analysisMode === 'current' && advancedMode && activeThemeAvailable && !regionalPartitionedThemeActive && !onDemandThemeActive && activeDatasetSupportsSelection ) { tasks.push(onRunMapSelectionExtract(bbox, areaId)) } if (analysisMode === 'evolution' && temporalSelectionValid) { tasks.push(compareTemporalSnapshots(earlierDatasetId, laterDatasetId, bbox, areaId)) } try { await Promise.all(tasks) } finally { if (mapAnalysisRequestSequence.current === requestId) { setMapAnalysisDurationMs(Date.now() - startedAt) } } } const runTemporalComparison = () => { if (!mapSelectionBbox || !temporalSelectionValid) { return } void compareTemporalSnapshots( earlierDatasetId, laterDatasetId, mapSelectionBbox, areaIdForSelection(mapSelectionBbox), ) } const handleMapBboxPreview = (bbox: VectorSelectionBBox) => { setSelectionBbox(bbox) } const handleMapBboxSelect = (bbox: VectorSelectionBBox) => { setFirstSelectionCorner(null) setBboxSelectionMode(false) clearThemeInsights() clearTemporalComparison() setResultsPanelOpen(false) setSelectionBbox(bbox) } const runQuickAoiExtract = () => { const bbox = selectedAreaBbox ?? activeLayerBbox if (!bbox) { return } setSelectionBbox(bbox) void analyzeSelection(bbox, areaIdForSelection(bbox)) } const runFullGisWorkflow = async () => { const bbox = currentSelectionBbox ?? selectedAreaBbox ?? activeLayerBbox if (fullWorkflowMode === 'reuse') { if (!latestSelectionDataset) { setFullWorkflowError('Bewaar eerst een kaartselectie voordat je het laatste resultaat opnieuw gebruikt.') return } if (!selectedMapQaReferenceDatasetId) { setFullWorkflowError('Kies eerst een referentielaag voor de kwaliteitscontrole.') return } setFullWorkflowRunning(true) setFullWorkflowError(null) try { setFullWorkflowStatus('Laatste bewaarde resultaatlaag opnieuw controleren...') const qaResult = await onRunMapSelectionQa(latestSelectionDataset) setFullWorkflowStatus(qaResult ? 'Het laatste bewaarde resultaat is opnieuw gebruikt en gecontroleerd.' : 'Het laatste resultaat is gebruikt, maar de kwaliteitscontrole is niet afgerond.') } catch (error) { setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.') setFullWorkflowStatus('De werkstroom is gestopt.') } finally { setFullWorkflowRunning(false) } return } if (!selectedMapDataset || !bbox) { setFullWorkflowError('Kies een kaartlaag en een werkgebied of laagbegrenzing.') return } setFullWorkflowRunning(true) setFullWorkflowError(null) try { setFullWorkflowStatus('1/4 Bewaarde kaartobjecten selecteren...') setSelectionBbox(bbox) const selectionAreaId = areaIdForSelection(bbox) const selection = await onRunMapSelectionExtract(bbox, selectionAreaId) if (!selection) { setFullWorkflowError('De ruimtelijke selectie kon niet worden afgerond.') setFullWorkflowStatus('Stopped at query.') return } setFullWorkflowStatus('2/4 Saving derived result dataset...') const derived = await onDeriveMapSelectionDataset(bbox, selectionAreaId) if (!derived) { setFullWorkflowError('De afgeleide resultaatlaag kon niet worden aangemaakt.') setFullWorkflowStatus('Stopped at dataset save.') return } setFullWorkflowStatus('3/4 Saving GeoJSON export artifact...') await onExportMapSelection(bbox, selectionAreaId) if (selectedMapQaReferenceDatasetId) { setFullWorkflowStatus('4/4 Kwaliteit vergelijken met de gekozen referentielaag...') const qaResult = await onRunMapSelectionQa(derived) setFullWorkflowStatus(qaResult ? 'De volledige GIS-werkstroom en kwaliteitscontrole zijn afgerond.' : 'Resultaat en download zijn gereed; de kwaliteitscontrole is niet afgerond.') } else { setFullWorkflowStatus('Resultaat en download zijn gereed. Kies een referentielaag om de kwaliteit te controleren.') } } catch (error) { setFullWorkflowError(error instanceof Error ? error.message : 'Full GIS workflow failed.') setFullWorkflowStatus('De werkstroom is gestopt.') } finally { setFullWorkflowRunning(false) } } if (!advancedMode) { return (

{activeScopeLabel} · geografische verkenner

Gebied analyseren

Verken vrij op de kaart, gebruik optioneel een officiële grens en vraag daarna inzichten op voor uw selectie.

{!readOnly ? ( ) : null}
{readOnly ? (
) : ( )} {workspaceLoading ? (
) : workspaceError ? (
De werkruimte kon niet volledig worden geladen {workspaceError}
) : null}

Baken uw onderzoeksvraag af

{bboxSelectionMode ? 'Sleep nu een rechthoek op de kaart.' : mapSelectionBbox ? 'Gebied gekozen. Selecteer links één of meer thema’s en start de analyse.' : 'Teken een rechthoek of gebruik het volledige werkgebied. Er wordt nog niets automatisch geladen.'}

0 && !workspaceLoading} processing={liveJourneyProcessing} validating={liveJourneyValidating} hasResult={liveJourneyHasResult} verified={liveJourneyVerified} error={liveJourneyError} selectionLabel={mapSelectionBbox ? 'Begrensde kaartselectie' : selectedMapArea?.name ?? 'Nog geen gebied geselecteerd'} sourceLabel={selectedThemes.length > 0 ? `${selectedThemes.length} gekozen` : 'Kies thema’s'} statusMessage={liveJourneyStatus} resultLabel={liveJourneyResultLabel} />
Werkgebied {thematicRasterImageOverlays.length > 0 || walousRasterImageOverlays.length > 0 ? ( {thematicLegendMin} → {thematicLegendMax} ) : activeImageOverlays.length > 0 ? ( {activeImageOverlays[0].label} {activeImageOverlays.length > 1 ? ` · ${activeImageOverlays.length} gemeenten` : ''} ) : null} {analysisOverlayActive ? ( <> AI-kandidaten Selectie ) : analysisMode === 'evolution' && temporalComparison?.object_changes.available ? ( <> Nieuw Verdwenen Gewijzigd ) : ( <> {activeTheme.shortLabel} Selectie )}
{bboxSelectionMode ? (
Rechthoek tekenen Houd de linkermuisknop ingedrukt, sleep over het gewenste gebied en laat los.
) : null} {viewportVectorEnabled && viewportVectorStatus ? (
{viewportVectorStatus}
) : null}
{secondaryResultsContainer ? null : ( )}
Werkgebied: {selectedMapArea?.name ?? 'Geen werkgebied geselecteerd'} Bron:{' '} {analysisOverlayActive ? `${mapLayerLabel} · ${mapLayerSourceLabel}` : analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks' : regionalBathymetryThemeActive ? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities` : activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : activeOnDemandMapProduct?.displayName ?? 'niet beschikbaar'} {usesDefaultOsmBasemap ? Ondergrond: OpenStreetMap : null}
) } return (

Ruimtelijke controle

Kaartwerkruimte

{mapFeatureCollection ? `${mapFeatureCount} objecten` : viewportVectorEnabled ? 'zoom in om te laden' : 'geen laag'}
{usesDefaultOsmBasemap ? (
Publieke kaartondergrond De publieke OpenStreetMap-ondergrond is actief. Configureer voor intensief gebruik een eigen kaartstijl.
) : null}
Kaartinhoud
onSetAreaLayerOpacity(Number(event.target.value))} data-testid="map-area-opacity" />
onSetMapLayerOpacity(Number(event.target.value))} data-testid="map-layer-opacity" />
{mapLayerLabel} {selectedMapDataset ? `Databaselaag: ${selectedMapDataset.name}` : 'Geen databaselaag gekozen'} {areaFeatureCollection ? `${areaFeatureCount} werkgebiedobjecten geladen` : 'Geen werkgebied geladen'} {mapFeatureCollection ? `${mapFeatureCount} objecten geladen` : viewportVectorEnabled ? 'Databaselaag gekozen; zichtbare objecten laden volgens de kaartuitsnede' : 'Geen vector- of resultaatlaag geladen'} {viewportVectorEnabled && viewportVectorStatus ? ( {viewportVectorStatus} ) : null}
Details van de kaartlagen {mapFeatureCollection ? `${mapFeatureCount} getekende objecten` : viewportVectorEnabled ? 'Kaartuitsnedelaag gekozen' : 'Geen actieve laag'}
Werkgebied {selectedMapArea?.name ?? 'Geen gebied geselecteerd'} {areaFeatureCollection ? `${areaFeatureCount} werkgebiedobjecten geladen` : 'Werkgebiedlaag uitgeschakeld'}
Actieve kaartlaag {mapLayerLabel} {mapLayerSourceLabel}
Status kaartobjecten {mapFeatureCollection ? `${mapFeatureCount} getekende objecten` : viewportVectorEnabled ? 'Wachten op detail van de kaartuitsnede' : 'Geen laag getekend'} {mapLayerProvenance}
Kaartbewijs kwaliteitscontrole {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} bewijsobjecten` : 'Geen bewijslaag'} {qualityEvidenceLoading ? 'Bewaard bewijs laden' : 'Overeenkomsten, onterecht gevonden en gemiste objecten'}
Bron van de kaartlaag {mapLayerSourceLabel}
Herkomst {mapLayerProvenance}
Weergavestatus {mapFeatureCollection ? `${mapFeatureCount} getekende objecten` : viewportVectorEnabled ? 'Laden volgens kaartuitsnede actief' : 'Geen actieve vector- of resultaatlaag'}
Kaartbewijs {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} getekend` : 'uitgeschakeld'}
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
Kaartbewijs kwaliteitscontrole {qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} bewaarde objecten` : 'Niet geladen'} {qualityEvidenceError ?

{qualityEvidenceError}

: null} {qualityEvidenceWarnings.length > 0 ? (

{qualityEvidenceWarnings.length} {qualityEvidenceWarnings.length === 1 ? 'bewijsverwijzing kon' : 'bewijsverwijzingen konden'} niet worden teruggevonden.

) : null}
Overeenkomst resultaat Overeenkomst referentie Onterecht gevonden Gemist
{onClearQualityEvidence ? ( ) : null}
) : null} {!mapFeatureCollection && !viewportVectorEnabled ? (
Geen actieve vector- of resultaatlaag

Open een databron, beeldanalyse, segmentatie of veranderingsresultaat om het hier te tekenen.

{availableMapDatasets.length > 0 ? ( <>

Open een beschikbare vectorlaag

{availableMapDatasets.map((dataset) => ( ))}
) : (

Nog geen gebruiksklare vectorlagen beschikbaar. Voeg eerst een vectorbestand toe.

)}
) : null} {mapSelectionBbox ? (

Dekking van deze selectie

{activeTheme.label}

{coverageLoading ? controleren : null}
{coverageError ?

{coverageError}

: null} {coverageDurationMs !== null ? (

Dekkingscontrole voltooid in {formatPerformanceDuration(coverageDurationMs)}. {coverageBudgetExceeded ? ' Dit overschrijdt het releasebudget van 4 seconden.' : ''}

) : null} {coverage ? ( <>
{coverage.intersected_zones.map((zone) => ( {coverageZoneLabel(zone)} ))} {coverage.outside_supported_scope ? deels buiten scope : null}
{activeCoverageItems.map((item) => (
{coverageZoneLabel(item.zone)} {coverageStatusLabel(item.status)} {item.source_names.join(', ') || 'Geen broncontract'}
))} {activeCoverageItems.length === 0 ? (

Deze selectie raakt geen bewaarde Belgische land- of zeezone.

) : null}
{coverageCounts.operational} beschikbaar {coverageCounts.partial} gedeeltelijk {coverageCounts.not_configured} niet gekoppeld {coverageCounts.unsupported} niet ondersteund
{coverage.warnings.map((warning) =>

{warning}

)} ) : !coverageLoading && !coverageError ? (

De dekkingsmatrix wordt bepaald zodra de selectie volledig is.

) : null}
) : null}

Operationele GIS-controle

Databaselaag doorzoeken

{mapSelectionResult ? `${mapSelectionResult.feature_count} resultaten` : 'gereed'}

Kies een bewaarde vectorlaag en doorzoek daarna de objecten in PostGIS binnen het werkgebied of de volledige laag.

Databaselaag {selectedMapDataset?.name ?? 'Kies een laag'}
Begrenzing werkgebied {selectedAreaBbox ? 'beschikbaar' : 'ontbreekt'}
Begrenzing kaartlaag {activeLayerBbox ? 'beschikbaar' : 'ontbreekt'}
Resultaat {mapSelectionResult ? `${mapSelectionResult.feature_count} objecten` : 'nog niet uitgevoerd'}
{mapSelectionError ?

{mapSelectionError}

: null}
1 Kaartlaag {selectedMapDataset ? selectedMapDataset.name : 'Kies een databaselaag'}
2 Begrenzing {currentSelectionBbox ? 'Gebiedsbegrenzing gereed' : 'Gebruik het werkgebied of de laagbegrenzing'}
3 Selectie {mapSelectionResult ? `${mapSelectionResult.feature_count} bewaarde objecten` : 'Voer de ruimtelijke selectie uit'}
4 Resultaatlaag {latestSelectionDatasetName ?? 'Bewaar het selectieresultaat'}
5 Kwaliteitscontrole {mapSelectionQaResult ? `F1 ${mapSelectionQaResult.f1_score ?? 'n.v.t.'}` : 'Vergelijk met een referentielaag'}
6 Download {latestSelectionExportPath ? 'GeoJSON-bestand gereed' : 'Bewaar een downloadbestand'}
Selecteren, bewaren, controleren en downloaden {fullWorkflowStatus}
{selectionDatasetError ?

{selectionDatasetError}

: null} {selectionExportError ?

{selectionExportError}

: null} {mapSelectionQaError ?

{mapSelectionQaError}

: null} {fullWorkflowError ?

{fullWorkflowError}

: null}
Geavanceerde selectie en inspectie Coördinaten, objectextractie en ruwe eigenschappen

Bewaarde vectorobjecten

Gebiedsselectie

{mapSelectionResult ? `${mapSelectionResult.feature_count} geselecteerd` : bboxSelectionMode ? 'selecteren' : 'gereed'}
{bboxSelectionMode ? (firstSelectionCorner ? 'Klik de tegenoverliggende hoek' : 'Klik de eerste hoek op de kaart') : 'Begrenzing EPSG:4326'} {formatBboxLabel(currentSelectionBbox)}
{mapSelectionError ?

{mapSelectionError}

: null} {mapSelectionResult ? (
Objecten {mapSelectionResult.feature_count}
Limiet {mapSelectionResult.limit}
Afgekapt {mapSelectionResult.truncated ? 'ja' : 'nee'}
Bron Bewaarde databankobjecten
{selectionExportError ?

{selectionExportError}

: null} {latestSelectionExportPath ? (

De geselecteerde download is bewaard.

) : null} {selectionDatasetError ?

{selectionDatasetError}

: null} {latestSelectionDatasetName ? (

Bewaarde afgeleide laag: {latestSelectionDatasetName}

) : null} {latestSelectionDatasetName ? (
{mapSelectionQaError ?

{mapSelectionQaError}

: null} {mapSelectionQaResult ? (

Kaartbewijs

Vergelijking van de bewaarde selectie

Precisie {mapSelectionQaResult.precision ?? 'n.v.t.'}
Herkenningsgraad {mapSelectionQaResult.recall ?? 'n.v.t.'}
F1 {mapSelectionQaResult.f1_score ?? 'n.v.t.'}
Gemiddelde overlap {mapSelectionQaResult.mean_iou ?? 'n.v.t.'}
Overeenkomsten {mapSelectionQaResult.matches}
Onterecht gevonden {mapSelectionQaResult.false_positives}
Gemist {mapSelectionQaResult.false_negatives}
Status bewijs {latestMapSelectionQualityCheckId ? 'bewaard' : 'niet bewaard'}
{mapSelectionQaResult.warnings.length > 0 ? (
Aandachtspunten
    {mapSelectionQaResult.warnings.map((warning) => (
  • {warning}
  • ))}
) : null}
) : null}
) : null} {areaSelectionPreviewFeatures.length > 0 ? (
{areaSelectionPreviewFeatures.map((feature, index) => ( ))}
Object Klasse Bronreferentie
{String(feature.properties?.['name'] ?? feature.properties?.['vector_feature_id'] ?? feature.id ?? index + 1)} {String(feature.properties?.['feature_class'] ?? 'n.v.t.')} {String(feature.properties?.['source_feature_id'] ?? 'n.v.t.')}
) : (

Geen bewaarde vectorobjecten kruisen deze selectie.

)}
) : null}

Geselecteerd object

Selectie en extractie

{selectedMapFeature ? 'gereed' : 'wachten'}
{selectedMapFeature ? ( <> {isBathymetryProfile ? (
Waterloop {String(featureProperties?.['watercourse_name'] ?? 'Onbekende waterloop')}
Profiel {String(featureProperties?.['profile_number'] ?? 'n.v.t.')}
Meetdatum {String(featureProperties?.['measurement_date'] ?? 'Niet geregistreerd')}
Geregistreerde diepte {typeof featureProperties?.['recorded_depth_m'] === 'number' ? `${featureProperties['recorded_depth_m'].toLocaleString('nl-BE')} m` : 'Niet als veld beschikbaar'}
{bathymetryDocumentUrl ? ( Officieel profielblad openen ) : ( Voor dit meetpunt is geen digitaal profielblad gekoppeld. )}

Historisch dwarsprofiel. Dit punt is geen continue actuele bodemkaart en levert zonder gelijktijdig waterpeil geen actueel watervolume.

) : null}
Geometrie {featureGeometrySummary.geometryType}
Coördinaten {featureGeometrySummary.coordinateCount}
Eigenschappen {featureExtractionEntries.length}
BBox EPSG:4326 {featureGeometrySummary.bboxLabel}
{featureExtractionEntries.length > 0 ? (
{featureExtractionEntries.map(([key, value]) => ( ))}
Eigenschap Waarde
{key} {typeof value === 'object' ? JSON.stringify(value) : String(value)}
) : (

Het geselecteerde object heeft geometrie maar geen bewaarde eigenschappen.

)} ) : (
Geen object geselecteerd

Klik op een zichtbaar kaartobject om de eigenschappen en GeoJSON te bekijken.

)}

Objectinspectie

{selectedMapFeature?.geometry?.type ?? 'geen'}
{selectedMapFeature ? ( <> {featureSummaryEntries.length > 0 ? (
{featureSummaryEntries.map(([key, value]) => (
{key} {String(value)}
))}
) : null}
{JSON.stringify(selectedMapFeature.properties ?? {}, null, 2)}
) : (

Klik op een zichtbaar kaartobject om de eigenschappen te bekijken.

)}
) }