Add bounded Flanders thematic analysis
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-17 18:28:48 +02:00
parent 21103441cf
commit ce597ee0b4
17 changed files with 389 additions and 55 deletions
+16 -1
View File
@@ -71,6 +71,15 @@ but is not required for the primary choose-theme, draw-area, read-result flow.
The primary workflow is deliberately short: choose a municipality or the complete region, choose a data theme, drag a rectangle on the MapLibre map and read the resulting PostGIS evidence. Releasing the drag runs the active theme query and every other available theme query for the same EPSG:4326 bbox. The active `area_id` constrains every drawn/manual selection to `bbox ∩ Area`; `Volledig werkgebied` uses a bbox enclosing the Area and therefore resolves to the exact persisted geometry. The result panel shows selection area, exact intersection totals, active-theme density, source identity and bounded feature properties. Map rendering remains capped at 1,000 features while `total_feature_count` reports the exact database count.
In the Flanders workbench, ruimtebeslag, open ruimte, population density,
node value and service level may appear as `Op aanvraag`. Drawing a rectangle
or choosing a municipality uses the existing backend Job/Dataset flow to
acquire and persist those five official rasters for the exact selection, then
shows all available semantic metrics together. Identical requests reuse the
persisted artifact. The complete Flanders Area is deliberately unavailable for
these rasters because it exceeds the backend safety ceiling; this does not
limit vector or partitioned bathymetry analysis of the complete region.
Detection Lab only lists ready imagery rasters. Governed height, flood-hazard
and thematic policy rasters remain available in the map explorer but are
excluded from the `Luchtbeeld` selector.
@@ -92,7 +101,13 @@ local YOLO model, Detection persistence and automatic QA against ready GRB
buildings. The panel shows all stages and errors; successful detections open as
an explicit AI-result overlay. Rectangles must be 128-1,024 m per side.
The theme catalog currently recognizes buildings, population, forest/green, water, roads and parcels from dataset names and canonical `reference_layer_name` metadata. A theme is enabled only when a ready persisted vector dataset exists; otherwise it states `Bron nog niet ingeladen`. This prevents missing population or land-cover sources from appearing as zero-valued observations. The previous technical Map workspace remains available through `Geavanceerde werkbank` for derived datasets, QA/QC evidence and export operations.
The theme catalog recognizes the governed vector, raster and partitioned
sources through canonical source metadata. A theme is enabled only when a
ready persisted Dataset exists, except for the five allowlisted Flanders
policy rasters that explicitly show `Op aanvraag`. Other missing themes state
`Bron nog niet ingeladen`; they never appear as zero-valued observations. The
technical Map workspace remains available through `Geavanceerde werkbank` for
derived datasets, QA/QC evidence and export operations.
The workbench uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but Map is the default product entry and Overview, Data, QA/QC, AI Labs, Exports and System remain secondary workspaces with a persistent top context bar and an optional selection-detail drawer.
+3
View File
@@ -972,6 +972,9 @@ function App(): JSX.Element {
onRunOrthophotoAnalysis={mapOrthophotoAnalysis.run}
onSelectOrthophotoProduct={mapOrthophotoAnalysis.setSelectedProductKey}
onClearQualityEvidence={clearQualityEvidenceGeoJson}
onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)}
onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')}
/>
+100 -37
View File
@@ -1,13 +1,15 @@
import { useEffect, useMemo, useState } from 'react'
import GeoMap from '../GeoMap'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
import type { AreaRead, DatasetCreateResponse, DetectionQaResult, MapResultExportRequest, MapViewportState, OrthophotoAcquisitionResult, OrthophotoProductRead, ProjectRead, QaComparisonResult, ThematicRasterProductRead, VectorSelectionBBox, VectorSelectionMetric, VectorSelectionResponse } from '../../types'
import { useMapThemeSelectionInsights, type MapThemeQuery } from '../../hooks/useMapThemeSelectionInsights'
import { useThematicRasterProducts } from '../../hooks/useThematicRasterProducts'
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart'
import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl } from '../../lib/thematicRaster'
import { FLANDERS_WORKSPACE_PROJECT_NAME } from '../../config/primaryFocus'
import {
bboxToInputState,
bboxesEqual,
@@ -548,6 +550,7 @@ interface MapWorkspaceProps {
onRunOrthophotoAnalysis: (bbox: VectorSelectionBBox) => Promise<boolean>
onSelectOrthophotoProduct: (productKey: string) => void
onClearQualityEvidence?: () => void
onRefreshProjectData: () => Promise<unknown>
onOpenAssistant: () => void
onOpenExports: () => void
}
@@ -632,6 +635,7 @@ export function MapWorkspace({
onRunOrthophotoAnalysis,
onSelectOrthophotoProduct,
onClearQualityEvidence,
onRefreshProjectData,
onOpenAssistant,
onOpenExports,
}: MapWorkspaceProps): JSX.Element {
@@ -640,13 +644,21 @@ export function MapWorkspace({
const selectedDataset = availableMapDatasets.find((dataset) => dataset.id === selectedMapDatasetId) ?? null
return themeIdForDataset(selectedDataset) ?? 'buildings'
})
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const flandersScopeSelected = activeScopeProject?.name === FLANDERS_WORKSPACE_PROJECT_NAME
const {
themeInsights,
themeInsightsLoading: themeResultsLoading,
themeInsightsError: themeResultsError,
loadThemeInsights,
clearThemeInsights,
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId)
} = useMapThemeSelectionInsights<DataThemeId>(selectedProjectId, onRefreshProjectData)
const {
products: thematicRasterProducts,
loading: thematicRasterProductsLoading,
error: thematicRasterProductsError,
} = useThematicRasterProducts(flandersScopeSelected ? selectedProjectId : null)
const {
temporalComparison,
temporalComparisonLoading,
@@ -747,6 +759,15 @@ export function MapWorkspace({
[availableMapDatasets, regionalScopeSelected, selectedMapAreaId, themeDatasetMap],
)
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
const onDemandThematicProductMap = useMemo(
() => flandersScopeSelected
? new Map<DataThemeId, ThematicRasterProductRead>(
thematicRasterProducts.map((product) => [product.theme, product]),
)
: new Map<DataThemeId, ThematicRasterProductRead>(),
[flandersScopeSelected, thematicRasterProducts],
)
const activeOnDemandThematicProduct = onDemandThematicProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
@@ -766,6 +787,9 @@ export function MapWorkspace({
const regionalRasterThemeActive = regionalScopeSelected && isPartitionedRaster(activeThemeDataset)
const regionalBathymetryThemeActive = regionalScopeSelected && isPartitionedBathymetry(activeThemeDataset)
const regionalPartitionedThemeActive = regionalRasterThemeActive || regionalBathymetryThemeActive
const onDemandThematicThemeActive = analysisMode === 'current' && Boolean(activeOnDemandThematicProduct)
const regionalOnDemandThematicThemeActive = regionalScopeSelected && onDemandThematicThemeActive
const activeThemeAvailable = Boolean(activeThemeDataset) || onDemandThematicThemeActive
const terrainImageOverlays = useMemo(
() =>
activeTheme.id === 'elevation' && selectedProjectId
@@ -830,8 +854,6 @@ export function MapWorkspace({
: orthophotoImageOverlay ? [orthophotoImageOverlay] : [],
[floodHazardImageOverlays, orthophotoImageOverlay, terrainImageOverlays, thematicRasterImageOverlays],
)
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
const themeTemporalSeriesMap = useMemo(
() =>
@@ -855,11 +877,12 @@ export function MapWorkspace({
useEffect(() => {
const contextSourceLabel = analysisMode === 'evolution' ? activeTemporalSeriesGroup?.label ?? null
: regionalBathymetryThemeActive ? 'VHA-dwarsprofielen Vlaanderen'
: null
: activeOnDemandThematicProduct?.display_name ?? null
onSetContextSourceLabel(contextSourceLabel)
return () => onSetContextSourceLabel(null)
}, [
activeTemporalSeriesGroup?.label,
activeOnDemandThematicProduct?.display_name,
analysisMode,
onSetContextSourceLabel,
regionalBathymetryThemeActive,
@@ -873,7 +896,9 @@ export function MapWorkspace({
}),
[themeInsights],
)
const activeSelectionResult = themeResults.find((item) => item.theme.id === activeThemeId)?.result
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)
const explorerMapFeatureCollection = regionalBathymetryThemeActive
? null
@@ -1066,15 +1091,15 @@ export function MapWorkspace({
if (!activeSelectionResult) {
return
}
if (activeThemeDataset?.dataset_type === 'raster') {
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: activeThemeDataset.id,
dataset_name: activeThemeDataset.name,
source_name: activeThemeDataset.source_name,
dataset_id: activeResultDataset.id,
dataset_name: activeResultDataset.name,
source_name: activeResultDataset.source_name,
result: activeSelectionResult,
})
return
@@ -1116,19 +1141,19 @@ export function MapWorkspace({
name: `${activeTheme.id}-evolution`,
}
: null
: activeSelectionResult && activeThemeDataset
: activeSelectionResult && activeResultDataset
? {
project_id: selectedProjectId,
mode: 'current',
bbox: { ...mapSelectionBbox, crs: 'EPSG:4326' },
dataset_id: activeThemeDataset.id,
dataset_id: activeResultDataset.id,
area_id: areaId,
partitioned: regionalPartitionedThemeActive,
product_key: regionalRasterThemeActive
? String(activeThemeDataset.source_metadata?.['product_key'] ?? '') || undefined
? String(activeResultDataset.source_metadata?.['product_key'] ?? '') || undefined
: undefined,
partition_scope_key: regionalBathymetryThemeActive
? String(activeThemeDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
? String(activeResultDataset.source_metadata?.['partition_scope_key'] ?? 'flanders')
: undefined,
theme_id: activeTheme.id,
name: `${activeTheme.id}-analysis`,
@@ -1172,12 +1197,17 @@ export function MapWorkspace({
const dataset = analysisMode === 'evolution'
? temporalGroup?.items[temporalGroup.items.length - 1] ?? null
: themeDatasetMap[theme.id]
if (!dataset) {
const onDemandProduct = analysisMode === 'current'
? onDemandThematicProductMap.get(theme.id)
: null
if (!dataset && !onDemandProduct) {
return
}
setActiveThemeId(theme.id)
clearTemporalComparison()
onOpenDatasetInMap(dataset)
if (dataset) {
onOpenDatasetInMap(dataset)
}
}
const setExplorerMode = (mode: 'current' | 'evolution') => {
@@ -1196,24 +1226,35 @@ export function MapWorkspace({
}
const loadAllThemeResults = async (bbox: VectorSelectionBBox, areaId?: string) => {
const availableThemes = DATA_THEMES.flatMap((theme) => {
const availableThemes: Array<MapThemeQuery<DataThemeId>> = []
for (const theme of DATA_THEMES) {
const thematicProduct = analysisMode === 'current'
? onDemandThematicProductMap.get(theme.id)
: null
if (thematicProduct) {
availableThemes.push({
themeId: theme.id,
thematicProductKey: thematicProduct.key,
})
continue
}
const dataset = themeDatasetMap[theme.id]
return dataset
? [{
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
}]
: []
})
if (dataset) {
availableThemes.push({
themeId: theme.id,
dataset,
partitioned: regionalScopeSelected
&& (isPartitionedRaster(dataset) || isPartitionedBathymetry(dataset)),
})
}
}
await loadThemeInsights(bbox, availableThemes, areaId)
}
const analyzeSelection = async (bbox: VectorSelectionBBox, areaId?: string) => {
setSelectionBbox(bbox)
const tasks: Array<Promise<unknown>> = [loadAllThemeResults(bbox, areaId)]
if (!regionalPartitionedThemeActive) {
if (!regionalPartitionedThemeActive && !onDemandThematicThemeActive) {
tasks.push(onRunMapSelectionExtract(bbox, areaId))
}
if (analysisMode === 'evolution' && earlierDatasetId && laterDatasetId) {
@@ -1374,11 +1415,14 @@ export function MapWorkspace({
<div className="geo-theme-list">
{DATA_THEMES.map((theme) => {
const dataset = themeDatasetMap[theme.id]
const onDemandProduct = onDemandThematicProductMap.get(theme.id)
const partitions = themePartitionMap[theme.id]
const temporalGroups = themeTemporalSeriesMap[theme.id]
const temporalGroup = temporalGroups[0]
const evolutionAvailable = temporalGroups.some((group) => group.items.length >= 2)
const available = Boolean(dataset) && (analysisMode === 'current' || evolutionAvailable)
const available = analysisMode === 'current'
? Boolean(dataset || onDemandProduct)
: Boolean(dataset) && evolutionAvailable
const active = activeThemeId === theme.id
const temporalRange = temporalGroup ? temporalRangeLabel(temporalGroup.items) : null
return (
@@ -1402,13 +1446,15 @@ export function MapWorkspace({
: 'Bron nog niet ingeladen'
: dataset
? datasetAvailabilityLabel(dataset, partitions)
: onDemandProduct
? `${onDemandProduct.native_resolution_m} m · ${onDemandProduct.observation_year} · laad bij selectie`
: 'Bron nog niet ingeladen'}
</small>
</span>
<i>
{analysisMode === 'evolution'
? evolutionAvailable ? 'Tijdreeks' : dataset ? 'Alleen huidig' : 'Ontbreekt'
: dataset ? 'Beschikbaar' : 'Ontbreekt'}
: dataset ? 'Beschikbaar' : onDemandProduct ? 'Op aanvraag' : 'Ontbreekt'}
</i>
</button>
)
@@ -1424,7 +1470,9 @@ export function MapWorkspace({
? activeTemporalSeriesGroup?.label ?? 'Nog geen historische reeks ingeladen'
: regionalBathymetryThemeActive
? 'VHA-dwarsprofielen Vlaanderen'
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: activeOnDemandThematicProduct?.display_name ?? 'Geen databron beschikbaar'}
</strong>
<small>
{analysisOverlayActive
@@ -1437,9 +1485,17 @@ export function MapWorkspace({
? `${activeThemePartitions.length} gecontroleerde gemeentepartities · selectie wordt ruimtelijk samengevoegd`
: activeThemeDataset
? `${getDatasetSourceDisplayName(activeThemeDataset)} · ${formatDatasetObservation(activeThemeDataset)}`
: activeOnDemandThematicProduct
? `${activeOnDemandThematicProduct.attribution} · wordt alleen voor de gekozen selectie ingeladen`
: activeTheme.description}
</small>
</div>
{thematicRasterProductsLoading && flandersScopeSelected ? (
<p className="geo-data-notice">Beschikbare Vlaamse beleidsbronnen worden gecontroleerd</p>
) : null}
{thematicRasterProductsError && flandersScopeSelected ? (
<p className="error">{thematicRasterProductsError}</p>
) : null}
{analysisMode === 'current' && activeTheme.id === 'flood_hazard' && floodHazardDatasets.length > 0 ? (
<label className="geo-scope-select">
@@ -1537,6 +1593,8 @@ export function MapWorkspace({
? 'Sleep nu een rechthoek op de kaart.'
: regionalRasterThemeActive
? 'Teken een rechthoek; de juiste gemeentelijke rasters worden automatisch gecombineerd.'
: onDemandThematicThemeActive
? 'Teken een rechthoek; officiële Vlaamse rasters worden begrensd opgehaald, bewaard en hergebruikt.'
: regionalBathymetryThemeActive
? 'Teken een rechthoek of analyseer Vlaanderen; alleen overlappende VHA-partities worden samengevoegd.'
: 'Sleep een rechthoek of analyseer het volledige werkgebied.'}
@@ -1546,7 +1604,7 @@ export function MapWorkspace({
<div className="geo-map-actions">
<button
className={bboxSelectionMode ? 'primary-action geo-draw-active' : 'primary-action'}
disabled={!activeThemeDataset || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
disabled={!activeThemeAvailable || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || mapSelectionLoading || themeResultsLoading}
type="button"
onClick={startBboxSelection}
>
@@ -1554,12 +1612,16 @@ export function MapWorkspace({
</button>
<button
className="secondary-action"
disabled={!activeThemeDataset || regionalRasterThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
disabled={!activeThemeAvailable || regionalRasterThemeActive || regionalOnDemandThematicThemeActive || (analysisMode === 'evolution' && activeTemporalSeries.length < 2) || !selectedAreaBbox || mapSelectionLoading || themeResultsLoading}
type="button"
title={regionalRasterThemeActive ? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.' : undefined}
title={
regionalRasterThemeActive || regionalOnDemandThematicThemeActive
? 'Teken een begrensde rechthoek voor een regionale rasteranalyse.'
: undefined
}
onClick={() => selectedAreaBbox && void analyzeSelection(selectedAreaBbox, selectedMapArea?.id)}
>
{regionalRasterThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
{regionalRasterThemeActive || regionalOnDemandThematicThemeActive ? 'Selecteer een deelgebied' : 'Volledig werkgebied'}
</button>
<button className="secondary-action" disabled={!mapSelectionBbox} type="button" onClick={clearAreaSelection}>
Wis selectie
@@ -1881,7 +1943,7 @@ export function MapWorkspace({
{analysisMode === 'current' ? (
<div className="geo-result-actions">
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={downloadActiveThemeResult}>
{activeThemeDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
{activeResultDataset?.dataset_type === 'raster' ? 'Download analyse' : 'Download GeoJSON'}
</button>
<button className="secondary-action" disabled={!activeSelectionResult} type="button" onClick={copyActiveThemeResult}>Kopieer gegevens</button>
</div>
@@ -1928,7 +1990,8 @@ export function MapWorkspace({
? `VHA-dwarsprofielen Vlaanderen · ${activeThemePartitions.length} gemeentepartities`
: activeThemeDataset
? getDatasetDisplayName(activeThemeDataset)
: 'niet beschikbaar'}
: activeOnDemandThematicProduct?.display_name
?? 'niet beschikbaar'}
</span>
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
</footer>
@@ -8,16 +8,22 @@ import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
export interface MapThemeQuery<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
dataset?: DatasetCreateResponse
partitioned?: boolean
thematicProductKey?: string
}
export interface MapThemeInsight<TThemeId extends string> extends MapThemeQuery<TThemeId> {
export interface MapThemeInsight<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
partitioned?: boolean
thematicProductKey?: string
result: VectorSelectionResponse
}
export function useMapThemeSelectionInsights<TThemeId extends string>(
selectedProjectId: string | null,
onDatasetsChanged?: () => Promise<unknown>,
) {
const [themeInsights, setThemeInsights] = useState<Array<MapThemeInsight<TThemeId>>>([])
const [themeInsightsLoading, setThemeInsightsLoading] = useState(false)
@@ -55,10 +61,32 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError(null)
try {
const settled = await Promise.allSettled(
queries.map(async ({ themeId, dataset, partitioned }) => ({
themeId,
dataset,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
queries.map(async ({ themeId, dataset: existingDataset, partitioned, thematicProductKey }) => {
let dataset = existingDataset
if (thematicProductKey) {
const acquisition = await datasetsApi.acquireThematicRaster(selectedProjectId, {
bbox,
area_id: areaId,
product_key: thematicProductKey,
force_refresh: false,
})
if (acquisition.status !== 'success' || !acquisition.output_dataset_id) {
throw new Error(
acquisition.error_message
|| `De officiële rasterbron ${thematicProductKey} kon niet worden ingeladen.`,
)
}
dataset = await datasetsApi.get(selectedProjectId, acquisition.output_dataset_id)
}
if (!dataset) {
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
}
return {
themeId,
dataset,
partitioned,
thematicProductKey,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
? terrainSelectionToMapSelection(
partitioned
? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
@@ -100,13 +128,18 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
area_id: areaId,
limit: 1000,
}),
})),
}
}),
)
const successful = settled.flatMap((item) => (item.status === 'fulfilled' ? [item.value] : []))
const failures = settled.flatMap((item, index) => (
item.status === 'rejected'
? [{
dataset: queries[index]?.dataset.name ?? queries[index]?.themeId ?? 'Onbekende bron',
dataset:
queries[index]?.dataset?.name
?? queries[index]?.thematicProductKey
?? queries[index]?.themeId
?? 'Onbekende bron',
reason: formatError(item.reason, 'Bron kon niet worden bevraagd.'),
}]
: []
@@ -116,6 +149,14 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
return []
}
setThemeInsights(successful)
let refreshFailure: string | null = null
if (successful.some((item) => item.thematicProductKey) && onDatasetsChanged) {
try {
await onDatasetsChanged()
} catch (error) {
refreshFailure = formatError(error, 'De datasetlijst kon na de analyse niet worden vernieuwd.')
}
}
if (failureCount > 0) {
const details = failures
.slice(0, 4)
@@ -125,6 +166,8 @@ export function useMapThemeSelectionInsights<TThemeId extends string>(
setThemeInsightsError(
`${failureCount} beschikbare databron${failureCount === 1 ? '' : 'nen'} kon niet worden bevraagd. ${details}${remainder}`,
)
} else if (refreshFailure) {
setThemeInsightsError(refreshFailure)
}
return successful
} catch (error) {
@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { datasetsApi } from '../services/api'
import { formatError } from '../lib/formatError'
import type { ThematicRasterProductRead } from '../types'
export function useThematicRasterProducts(selectedProjectId: string | null) {
const [products, setProducts] = useState<ThematicRasterProductRead[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
if (!selectedProjectId) {
setProducts([])
setLoading(false)
setError(null)
return () => {
cancelled = true
}
}
setLoading(true)
setError(null)
void datasetsApi.listThematicRasterProducts(selectedProjectId)
.then((response) => {
if (!cancelled) {
setProducts(response.items)
}
})
.catch((requestError) => {
if (!cancelled) {
setProducts([])
setError(formatError(requestError, 'De Vlaamse beleidsrasters konden niet worden geladen.'))
}
})
.finally(() => {
if (!cancelled) {
setLoading(false)
}
})
return () => {
cancelled = true
}
}, [selectedProjectId])
return { products, loading, error }
}
+2
View File
@@ -64,6 +64,8 @@ async function listProjectDatasets(projectId: string): Promise<DatasetListRespon
export const datasetsApi = {
list: listProjectDatasets,
get: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
apiGet<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}`),
sourceFreshness: (projectId: string): Promise<SourceFreshnessReport> =>
apiGet<SourceFreshnessReport>(`/api/v1/projects/${projectId}/datasets/source-freshness`),
sourceCatalogProbes: (projectId: string, refresh = false): Promise<SourceCatalogProbeReport> =>