2026-04-21 00:52:20 +00:00
|
|
|
import { useState, useEffect, useRef } from 'react'
|
2026-04-24 00:44:20 +00:00
|
|
|
import { Layers, Trees, Mountain } from 'lucide-react'
|
2026-04-21 00:52:20 +00:00
|
|
|
import { hasFeature, getConfig } from '../config'
|
|
|
|
|
|
|
|
|
|
const STORAGE_KEY = 'navi-layer-prefs'
|
|
|
|
|
|
|
|
|
|
function loadPrefs() {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
|
|
|
if (raw) return JSON.parse(raw)
|
|
|
|
|
} catch {}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function savePrefs(prefs) {
|
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function LayerControl({ mapRef }) {
|
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const [hillshade, setHillshade] = useState(false)
|
|
|
|
|
const [traffic, setTraffic] = useState(false)
|
2026-04-22 18:52:53 +00:00
|
|
|
const [publicLands, setPublicLands] = useState(false)
|
2026-04-24 00:44:20 +00:00
|
|
|
const [contours, setContours] = useState(false)
|
2026-04-21 00:52:20 +00:00
|
|
|
const panelRef = useRef(null)
|
|
|
|
|
|
|
|
|
|
// Initialize from localStorage or defaults on mount
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const saved = loadPrefs()
|
|
|
|
|
const hsAvailable = hasFeature('has_hillshade')
|
|
|
|
|
const trAvailable = hasFeature('has_traffic_overlay')
|
|
|
|
|
|
2026-04-22 18:52:53 +00:00
|
|
|
const plAvailable = hasFeature('has_public_lands_layer')
|
2026-04-24 00:44:20 +00:00
|
|
|
const ctAvailable = hasFeature('has_contours')
|
2026-04-22 18:52:53 +00:00
|
|
|
|
2026-04-21 00:52:20 +00:00
|
|
|
if (saved) {
|
|
|
|
|
setHillshade(hsAvailable && (saved.hillshade ?? true))
|
|
|
|
|
setTraffic(trAvailable && (saved.traffic ?? false))
|
2026-04-22 18:52:53 +00:00
|
|
|
setPublicLands(plAvailable && (saved.publicLands ?? false))
|
2026-04-24 00:44:20 +00:00
|
|
|
setContours(ctAvailable && (saved.contours ?? false))
|
2026-04-21 00:52:20 +00:00
|
|
|
} else {
|
2026-04-24 00:44:20 +00:00
|
|
|
// Defaults: hillshade ON if available, others OFF
|
2026-04-21 00:52:20 +00:00
|
|
|
setHillshade(hsAvailable)
|
|
|
|
|
setTraffic(false)
|
2026-04-22 18:52:53 +00:00
|
|
|
setPublicLands(false)
|
2026-04-24 00:44:20 +00:00
|
|
|
setContours(false)
|
2026-04-21 00:52:20 +00:00
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
// Apply layers when prefs change
|
|
|
|
|
useEffect(() => {
|
2026-04-22 03:27:21 +00:00
|
|
|
const mapView = mapRef?.current
|
|
|
|
|
if (!mapView) return
|
|
|
|
|
const map = mapView.getMap?.()
|
|
|
|
|
if (!map) return
|
2026-04-21 00:52:20 +00:00
|
|
|
|
2026-04-22 03:27:21 +00:00
|
|
|
const apply = () => {
|
|
|
|
|
if (hillshade && hasFeature('has_hillshade')) {
|
|
|
|
|
mapView.addHillshadeLayer?.()
|
|
|
|
|
} else {
|
|
|
|
|
mapView.removeHillshadeLayer?.()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (map.isStyleLoaded()) {
|
|
|
|
|
apply()
|
2026-04-21 00:52:20 +00:00
|
|
|
} else {
|
2026-04-22 03:27:21 +00:00
|
|
|
map.once('style.load', apply)
|
2026-04-21 00:52:20 +00:00
|
|
|
}
|
2026-04-24 00:44:20 +00:00
|
|
|
savePrefs({ hillshade, traffic, publicLands, contours })
|
2026-04-22 03:27:21 +00:00
|
|
|
return () => map.off('style.load', apply)
|
2026-04-21 00:52:20 +00:00
|
|
|
}, [hillshade, mapRef])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-22 03:27:21 +00:00
|
|
|
const mapView = mapRef?.current
|
|
|
|
|
if (!mapView) return
|
|
|
|
|
const map = mapView.getMap?.()
|
|
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
const apply = () => {
|
|
|
|
|
if (traffic && hasFeature('has_traffic_overlay')) {
|
|
|
|
|
mapView.addTrafficLayer?.()
|
|
|
|
|
} else {
|
|
|
|
|
mapView.removeTrafficLayer?.()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 00:52:20 +00:00
|
|
|
|
2026-04-22 03:27:21 +00:00
|
|
|
if (map.isStyleLoaded()) {
|
|
|
|
|
apply()
|
2026-04-21 00:52:20 +00:00
|
|
|
} else {
|
2026-04-22 03:27:21 +00:00
|
|
|
map.once('style.load', apply)
|
2026-04-21 00:52:20 +00:00
|
|
|
}
|
2026-04-24 00:44:20 +00:00
|
|
|
savePrefs({ hillshade, traffic, publicLands, contours })
|
2026-04-22 03:27:21 +00:00
|
|
|
return () => map.off('style.load', apply)
|
2026-04-21 00:52:20 +00:00
|
|
|
}, [traffic, mapRef])
|
|
|
|
|
|
2026-04-22 18:52:53 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const mapView = mapRef?.current
|
|
|
|
|
if (!mapView) return
|
|
|
|
|
const map = mapView.getMap?.()
|
|
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
const apply = () => {
|
|
|
|
|
if (publicLands && hasFeature('has_public_lands_layer')) {
|
|
|
|
|
mapView.addPublicLandsLayer?.()
|
|
|
|
|
} else {
|
|
|
|
|
mapView.removePublicLandsLayer?.()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (map.isStyleLoaded()) {
|
|
|
|
|
apply()
|
|
|
|
|
} else {
|
|
|
|
|
map.once('style.load', apply)
|
|
|
|
|
}
|
2026-04-24 00:44:20 +00:00
|
|
|
savePrefs({ hillshade, traffic, publicLands, contours })
|
2026-04-22 18:52:53 +00:00
|
|
|
return () => map.off('style.load', apply)
|
|
|
|
|
}, [publicLands, mapRef])
|
|
|
|
|
|
2026-04-24 00:44:20 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const mapView = mapRef?.current
|
|
|
|
|
if (!mapView) return
|
|
|
|
|
const map = mapView.getMap?.()
|
|
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
const apply = () => {
|
|
|
|
|
if (contours && hasFeature('has_contours')) {
|
|
|
|
|
mapView.addContoursLayer?.()
|
|
|
|
|
} else {
|
|
|
|
|
mapView.removeContoursLayer?.()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (map.isStyleLoaded()) {
|
|
|
|
|
apply()
|
|
|
|
|
} else {
|
|
|
|
|
map.once('style.load', apply)
|
|
|
|
|
}
|
|
|
|
|
savePrefs({ hillshade, traffic, publicLands, contours })
|
|
|
|
|
return () => map.off('style.load', apply)
|
|
|
|
|
}, [contours, mapRef])
|
|
|
|
|
|
2026-04-21 00:52:20 +00:00
|
|
|
// Close on outside click
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return
|
|
|
|
|
function handleClick(e) {
|
|
|
|
|
if (panelRef.current && !panelRef.current.contains(e.target)) {
|
|
|
|
|
setOpen(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-22 03:27:21 +00:00
|
|
|
document.addEventListener('pointerdown', handleClick)
|
|
|
|
|
return () => document.removeEventListener('pointerdown', handleClick)
|
2026-04-21 00:52:20 +00:00
|
|
|
}, [open])
|
|
|
|
|
|
|
|
|
|
const showHillshade = hasFeature('has_hillshade')
|
|
|
|
|
const showTraffic = hasFeature('has_traffic_overlay')
|
2026-04-22 18:52:53 +00:00
|
|
|
const showPublicLands = hasFeature('has_public_lands_layer')
|
2026-04-24 00:44:20 +00:00
|
|
|
const showContours = hasFeature('has_contours')
|
2026-04-21 00:52:20 +00:00
|
|
|
|
|
|
|
|
// Don't render if no overlay features available
|
2026-04-24 00:44:20 +00:00
|
|
|
if (!showHillshade && !showTraffic && !showPublicLands && !showContours) return null
|
2026-04-21 00:52:20 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div ref={panelRef} className="layer-control">
|
|
|
|
|
<button
|
|
|
|
|
className="layer-control-btn"
|
|
|
|
|
onClick={() => setOpen((v) => !v)}
|
|
|
|
|
title="Map layers"
|
|
|
|
|
aria-label="Toggle map layers"
|
|
|
|
|
>
|
|
|
|
|
<Layers size={18} />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{open && (
|
|
|
|
|
<div className="layer-control-popover">
|
|
|
|
|
<div className="layer-control-header">Layers</div>
|
|
|
|
|
|
|
|
|
|
{showHillshade && (
|
|
|
|
|
<label className="layer-control-item">
|
|
|
|
|
<span className="layer-control-label">Hillshade</span>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="layer-control-toggle"
|
|
|
|
|
checked={hillshade}
|
|
|
|
|
onChange={(e) => setHillshade(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showTraffic && (
|
|
|
|
|
<label className="layer-control-item">
|
|
|
|
|
<span className="layer-control-label">Traffic</span>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="layer-control-toggle"
|
|
|
|
|
checked={traffic}
|
|
|
|
|
onChange={(e) => setTraffic(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-04-22 18:52:53 +00:00
|
|
|
|
|
|
|
|
{showPublicLands && (
|
|
|
|
|
<label className="layer-control-item">
|
|
|
|
|
<span className="layer-control-label">Public Lands</span>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="layer-control-toggle"
|
|
|
|
|
checked={publicLands}
|
|
|
|
|
onChange={(e) => setPublicLands(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-04-24 00:44:20 +00:00
|
|
|
|
|
|
|
|
{showContours && (
|
|
|
|
|
<label className="layer-control-item">
|
|
|
|
|
<span className="layer-control-label">Contours</span>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
className="layer-control-toggle"
|
|
|
|
|
checked={contours}
|
|
|
|
|
onChange={(e) => setContours(e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
)}
|
2026-04-21 00:52:20 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|