Files
geointel/frontend/src/hooks/useMapImageOverlays.ts
T
JensandClaude Opus 5 9c29577b82 collapse five duplicate overlay blocks into one tested builder
MapWorkspace held five near-identical useMemo blocks deciding which raster
image the map draws under the active theme — terrain, flood depth, thematic
raster, WALOUS land cover and bathymetry. Each filtered partitions by source
name, read bbox_epsg4326 and assembled the same overlay shape, so the parts
that genuinely differ per theme were buried in the repetition.

One builder makes the rule testable and leaves only the source, the label and
the opacity varying. A raster whose bounds are unusable is now skipped rather
than drawn from a partial bbox: an overlay in the wrong place is worse than no
overlay.

The legend asked "are these thematic or WALOUS overlays" by inspecting two of
the five lists. That is a property of the source, so it says so directly.

Two contract tests needed fixing rather than repointing. One asserted
`"api" not in source.lower()`, which the new hook name useMapImageOverlays
matches inside "useM-api-mageOverlays" — as would rapid, capital or therapy.
The contract is that this component talks to no API client, so it now says
that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:56:21 +02:00

158 lines
5.4 KiB
TypeScript

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,
])
}