99 lines
2.6 KiB
TypeScript
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>
|
|
)
|
|
}
|