Files
geointel/frontend/src/hooks/useMapThemeSelectionInsights.ts
T
Codex c787fb2184
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s
feat: operationalize Flemish land and nature themes
2026-07-17 22:38:25 +02:00

226 lines
8.7 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { formatError } from '../lib/formatError'
import { datasetsApi } from '../services/api/datasets'
import type { DatasetCreateResponse, VectorSelectionBBox, VectorSelectionResponse } from '../types'
import { terrainSelectionToMapSelection } from '../lib/terrainSelection'
import { floodHazardSelectionToMapSelection } from '../lib/floodHazardSelection'
import { thematicRasterSelectionToMapSelection } from '../lib/thematicRaster'
export type MapThemeAcquisitionKind = 'thematic_raster' | 'dhmv' | 'flood_hazard' | 'grb' | 'official_vector'
export interface MapThemeAcquisition {
kind: MapThemeAcquisitionKind
productKey: string
displayName: string
}
export interface MapThemeQuery<TThemeId extends string> {
themeId: TThemeId
dataset?: DatasetCreateResponse
partitioned?: boolean
acquisition?: MapThemeAcquisition
}
export interface MapThemeInsight<TThemeId extends string> {
themeId: TThemeId
dataset: DatasetCreateResponse
partitioned?: boolean
acquisition?: MapThemeAcquisition
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)
const [themeInsightsError, setThemeInsightsError] = useState<string | null>(null)
const requestSequence = useRef(0)
useEffect(() => {
requestSequence.current += 1
setThemeInsights([])
setThemeInsightsError(null)
setThemeInsightsLoading(false)
}, [selectedProjectId])
const clearThemeInsights = () => {
requestSequence.current += 1
setThemeInsights([])
setThemeInsightsError(null)
setThemeInsightsLoading(false)
}
const loadThemeInsights = async (
bbox: VectorSelectionBBox,
queries: Array<MapThemeQuery<TThemeId>>,
areaId?: string,
): Promise<Array<MapThemeInsight<TThemeId>>> => {
if (!selectedProjectId) {
setThemeInsights([])
setThemeInsightsError('Open eerst een project om de selectie te analyseren.')
return []
}
const sequence = requestSequence.current + 1
requestSequence.current = sequence
setThemeInsightsLoading(true)
setThemeInsightsError(null)
try {
const settled = await Promise.allSettled(
queries.map(async ({ themeId, dataset: existingDataset, partitioned, acquisition }) => {
let dataset = existingDataset
if (acquisition) {
const commonPayload = {
bbox,
area_id: areaId,
force_refresh: false,
}
const acquisitionJob = acquisition.kind === 'thematic_raster'
? await datasetsApi.acquireThematicRaster(selectedProjectId, {
...commonPayload,
product_key: acquisition.productKey,
})
: acquisition.kind === 'dhmv'
? await datasetsApi.acquireDhmv(selectedProjectId, {
...commonPayload,
product_key: acquisition.productKey as 'dtm_1m' | 'dsm_1m',
})
: acquisition.kind === 'flood_hazard'
? await datasetsApi.acquireFloodHazard(selectedProjectId, {
...commonPayload,
product_key: acquisition.productKey,
})
: acquisition.kind === 'grb'
? await datasetsApi.acquireGrb(selectedProjectId, {
...commonPayload,
product_key: acquisition.productKey as 'buildings' | 'roads' | 'water' | 'parcels',
})
: await datasetsApi.acquireOfficialVector(selectedProjectId, {
...commonPayload,
product_key: acquisition.productKey,
})
if (acquisitionJob.status !== 'success' || !acquisitionJob.output_dataset_id) {
throw new Error(
acquisitionJob.error_message
|| `De officiële kaartbron ${acquisition.displayName} kon niet worden ingeladen.`,
)
}
dataset = await datasetsApi.get(selectedProjectId, acquisitionJob.output_dataset_id)
}
if (!dataset) {
throw new Error(`Geen persistente databron beschikbaar voor thema ${themeId}.`)
}
return {
themeId,
dataset,
partitioned,
acquisition,
result: dataset.dataset_type === 'raster' && dataset.source_name === 'digitaal_vlaanderen_dhmv'
? terrainSelectionToMapSelection(
partitioned
? await datasetsApi.selectTerrainPartitions(selectedProjectId, {
bbox,
area_id: areaId,
product_key: String(dataset.source_metadata?.['product_key'] ?? 'dtm_1m'),
})
: await datasetsApi.selectTerrain(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}),
)
: dataset.dataset_type === 'raster' && dataset.source_name === 'vmm_flood_hazard'
? floodHazardSelectionToMapSelection(
partitioned
? await datasetsApi.selectFloodHazardPartitions(selectedProjectId, {
bbox,
area_id: areaId,
product_key: String(dataset.source_metadata?.['product_key'] ?? 'pluviaal_current_t100'),
})
: await datasetsApi.selectFloodHazard(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}),
)
: dataset.dataset_type === 'raster' && dataset.source_name === 'department_omgeving_thematic_raster'
? thematicRasterSelectionToMapSelection(await datasetsApi.selectThematicRaster(selectedProjectId, dataset.id, {
bbox,
area_id: areaId,
}))
: dataset.source_name === 'vmm_vha_bathymetry_profiles' && partitioned
? await datasetsApi.selectBathymetryProfilePartitions(selectedProjectId, {
bbox,
area_id: areaId,
limit: 1000,
})
: await datasetsApi.selectVectorFeatures(selectedProjectId, dataset.id, {
bbox,
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]?.acquisition?.displayName
?? queries[index]?.themeId
?? 'Onbekende bron',
reason: formatError(item.reason, 'Bron kon niet worden bevraagd.'),
}]
: []
))
const failureCount = failures.length
if (requestSequence.current !== sequence) {
return []
}
setThemeInsights(successful)
let refreshFailure: string | null = null
if (successful.some((item) => item.acquisition) && 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)
.map((failure) => `${failure.dataset}: ${failure.reason}`)
.join(' · ')
const remainder = failureCount > 4 ? ` · en ${failureCount - 4} andere` : ''
setThemeInsightsError(
`${failureCount} beschikbare databron${failureCount === 1 ? '' : 'nen'} kon niet worden bevraagd. ${details}${remainder}`,
)
} else if (refreshFailure) {
setThemeInsightsError(refreshFailure)
}
return successful
} catch (error) {
if (requestSequence.current !== sequence) {
return []
}
setThemeInsights([])
setThemeInsightsError(formatError(error, 'De gebiedsanalyse is mislukt.'))
return []
} finally {
if (requestSequence.current === sequence) {
setThemeInsightsLoading(false)
}
}
}
return {
themeInsights,
themeInsightsLoading,
themeInsightsError,
loadThemeInsights,
clearThemeInsights,
}
}