rcnn/web/src/ui/OverlayControls.tsx
2026-04-25 23:15:46 +02:00

99 lines
2.6 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import type { Map } from 'maplibre-gl'
import { useMapLibreMap } from '../maplibre/maplibreContext'
import { safeRemoveLayers } from '../maplibre/geoUtils'
import styles from './OverlayControls.module.css'
interface OverlayDef {
id: string
label: string
tiles: string[]
tileSize: 256 | 512
opacity: number
}
const OVERLAYS: OverlayDef[] = [
{
id: 'streets',
label: 'Streets',
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}'],
tileSize: 256,
opacity: 0.75,
},
{
id: 'labels',
label: 'City names',
tiles: ['https://a.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}.png'],
tileSize: 256,
opacity: 1.0,
},
{
id: 'borders',
label: 'Borders',
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}'],
tileSize: 256,
opacity: 0.85,
},
]
function addOverlay(map: Map, overlay: OverlayDef) {
const srcId = `overlay-src-${overlay.id}`
const lyrId = `overlay-lyr-${overlay.id}`
map.addSource(srcId, { type: 'raster', tiles: overlay.tiles, tileSize: overlay.tileSize })
map.addLayer({ id: lyrId, type: 'raster', source: srcId,
paint: { 'raster-opacity': overlay.opacity } })
}
function removeOverlay(map: Map, overlay: OverlayDef) {
safeRemoveLayers(map,
[`overlay-lyr-${overlay.id}`],
[`overlay-src-${overlay.id}`],
)
}
export function OverlayControls() {
const map = useMapLibreMap()
const [active, setActive] = useState<Set<string>>(new Set())
const activeRef = useRef(active)
activeRef.current = active
function toggle(overlay: OverlayDef) {
setActive((prev) => {
const next = new Set(prev)
if (next.has(overlay.id)) {
removeOverlay(map, overlay)
next.delete(overlay.id)
} else {
addOverlay(map, overlay)
next.add(overlay.id)
}
return next
})
}
// Clean up all active overlays on unmount
useEffect(() => {
return () => {
for (const id of activeRef.current) {
const overlay = OVERLAYS.find(o => o.id === id)
if (overlay) removeOverlay(map, overlay)
}
}
}, [map])
return (
<div className={styles.group}>
{OVERLAYS.map((o) => (
<button
key={o.id}
className={`${styles.chip} ${active.has(o.id) ? styles.on : ''}`}
onClick={() => toggle(o)}
title={`Toggle ${o.label} overlay`}
>
{o.label}
</button>
))}
</div>
)
}