import { useMemo } from 'react' import type { DatasetCreateResponse } from '../types' import { getDatasetDisplayName } from '../lib/datasetDisplay' import { terrainImageUrl } from '../lib/terrainImage' import { floodHazardImageUrl } from '../lib/floodHazardImage' import { thematicRasterImageUrl, walousRasterImageUrl } from '../lib/thematicRaster' import { bathymetryRasterImageUrl } from '../lib/bathymetryRaster' /** * Which raster image the map draws underneath the active theme. * * MapWorkspace held five near-identical useMemo blocks for this — terrain, * flood depth, thematic raster, WALOUS land cover and bathymetry — each * filtering partitions by source name, reading `bbox_epsg4326` and assembling * the same overlay shape. One builder makes the rule testable and leaves only * the parts that genuinely differ per theme: the source, the label and the * opacity. */ export interface MapImageOverlay { url: string bbox: [number, number, number, number] label: string opacity: number } interface RasterOverlayRequest { datasets: DatasetCreateResponse[] sourceNames: string[] opacity: number url: (datasetId: string) => string label: (dataset: DatasetCreateResponse) => string } /** A bbox is only usable if the source actually published four numbers for it. */ function overlayBounds(dataset: DatasetCreateResponse): [number, number, number, number] | null { const bounds = dataset.source_metadata?.['bbox_epsg4326'] if (!Array.isArray(bounds) || bounds.length !== 4) return null const numbers = bounds.map(Number) return numbers.some((value) => !Number.isFinite(value)) ? null : (numbers as [number, number, number, number]) } export function buildRasterOverlays({ datasets, sourceNames, opacity, url, label, }: RasterOverlayRequest): MapImageOverlay[] { return datasets.flatMap((dataset) => { if (!sourceNames.includes(dataset.source_name ?? '')) return [] const bbox = overlayBounds(dataset) // A raster we cannot place is not drawn at all: an overlay in the wrong // spot is worse than no overlay. if (!bbox) return [] return [{ url: url(dataset.id), bbox, label: label(dataset), opacity }] }) } /** The first non-empty raster wins; the orthophoto is the base layer. */ export function pickActiveOverlays({ candidates, fallback, }: { candidates: MapImageOverlay[][] fallback: MapImageOverlay | null }): MapImageOverlay[] { const active = candidates.find((item) => item.length > 0) if (active) return active return fallback ? [fallback] : [] } /** Sources drawn as a continuous value ramp, which is what the legend explains. */ const VALUE_RAMP_SOURCES = ['department_omgeving_thematic_raster', 'spw_walous_land_cover'] export function isValueRampRasterSource(dataset: DatasetCreateResponse | null | undefined): boolean { return VALUE_RAMP_SOURCES.includes(dataset?.source_name ?? '') } interface MapImageOverlayOptions { selectedProjectId: string | null activeThemeId: string activeThemeDataset: DatasetCreateResponse | null | undefined activeThemePartitions: DatasetCreateResponse[] orthophotoImageOverlay: MapImageOverlay | null floodScenarioLabel: (dataset: DatasetCreateResponse) => string } export function useMapImageOverlays({ selectedProjectId, activeThemeId, activeThemeDataset, activeThemePartitions, orthophotoImageOverlay, floodScenarioLabel, }: MapImageOverlayOptions): MapImageOverlay[] { return useMemo(() => { if (!selectedProjectId) return orthophotoImageOverlay ? [orthophotoImageOverlay] : [] const singleDataset = activeThemeDataset ? [activeThemeDataset] : [] // Ordered most specific first: a theme that resolved to a dedicated raster // product should not be drawn as generic terrain underneath it. const candidates = [ buildRasterOverlays({ datasets: singleDataset, sourceNames: ['spw_bathymetry'], opacity: 0.86, url: (id) => bathymetryRasterImageUrl(selectedProjectId, id), label: () => 'Waterbodemhoogte in mDNG', }), buildRasterOverlays({ datasets: singleDataset, sourceNames: ['spw_walous_land_cover'], opacity: 0.82, url: (id) => walousRasterImageUrl(selectedProjectId, id), label: getDatasetDisplayName, }), buildRasterOverlays({ datasets: singleDataset, sourceNames: ['department_omgeving_thematic_raster'], opacity: 0.78, url: (id) => thematicRasterImageUrl(selectedProjectId, id), label: getDatasetDisplayName, }), activeThemeId === 'flood_hazard' ? buildRasterOverlays({ datasets: activeThemePartitions, sourceNames: ['vmm_flood_hazard'], opacity: 0.82, url: (id) => floodHazardImageUrl(selectedProjectId, id), label: floodScenarioLabel, }) : [], activeThemeId === 'elevation' ? buildRasterOverlays({ datasets: activeThemePartitions, sourceNames: ['digitaal_vlaanderen_dhmv', 'spw_terrain'], opacity: 0.82, url: (id) => terrainImageUrl(selectedProjectId, id), label: getDatasetDisplayName, }) : [], ] return pickActiveOverlays({ candidates, fallback: orthophotoImageOverlay }) }, [ activeThemeDataset, activeThemeId, activeThemePartitions, floodScenarioLabel, orthophotoImageOverlay, selectedProjectId, ]) }