feat(map): add USFS trails and roads layer

- Add USFS trails/roads as toggleable map layer via PMTiles
- Trails: dashed brown lines, roads: solid khaki lines
- Labels at zoom 12+ for trail and road names
- Click handler shows popup with trail/road info
- Feature-flag gated with has_usfs_trails (default false)
- Add Trails toggle to Layer Control panel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-04-30 16:43:30 +00:00
commit 2b90f8b17a
3 changed files with 526 additions and 307 deletions

View file

@ -1,306 +1,343 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef } from 'react'
import { Layers, Trees, Mountain } from 'lucide-react' import { Layers, Trees, Mountain } from 'lucide-react'
import { hasFeature, getConfig } from '../config' import { hasFeature, getConfig } from '../config'
const STORAGE_KEY = 'navi-layer-prefs' const STORAGE_KEY = 'navi-layer-prefs'
function loadPrefs() { function loadPrefs() {
try { try {
const raw = localStorage.getItem(STORAGE_KEY) const raw = localStorage.getItem(STORAGE_KEY)
if (raw) return JSON.parse(raw) if (raw) return JSON.parse(raw)
} catch {} } catch {}
return null return null
} }
function savePrefs(prefs) { function savePrefs(prefs) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)) localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
} }
export default function LayerControl({ mapRef }) { export default function LayerControl({ mapRef }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [hillshade, setHillshade] = useState(false) const [hillshade, setHillshade] = useState(false)
const [traffic, setTraffic] = useState(false) const [traffic, setTraffic] = useState(false)
const [publicLands, setPublicLands] = useState(false) const [publicLands, setPublicLands] = useState(false)
const [contours, setContours] = useState(false) const [contours, setContours] = useState(false)
const [contoursTest, setContoursTest] = useState(false) const [contoursTest, setContoursTest] = useState(false)
const [contoursTest10ft, setContoursTest10ft] = useState(false) const [contoursTest10ft, setContoursTest10ft] = useState(false)
const panelRef = useRef(null) const [usfsTrails, setUsfsTrails] = useState(false)
const panelRef = useRef(null)
// Initialize from localStorage or defaults on mount
useEffect(() => { // Initialize from localStorage or defaults on mount
const saved = loadPrefs() useEffect(() => {
const hsAvailable = hasFeature('has_hillshade') const saved = loadPrefs()
const trAvailable = hasFeature('has_traffic_overlay') const hsAvailable = hasFeature('has_hillshade')
const trAvailable = hasFeature('has_traffic_overlay')
const plAvailable = hasFeature('has_public_lands_layer') const plAvailable = hasFeature('has_public_lands_layer')
const ctAvailable = hasFeature('has_contours') const ctAvailable = hasFeature('has_contours')
const ctTestAvailable = hasFeature('has_contours_test') const ctTestAvailable = hasFeature('has_contours_test')
const ctTest10ftAvailable = hasFeature('has_contours_test_10ft') const ctTest10ftAvailable = hasFeature('has_contours_test_10ft')
const usfsAvailable = hasFeature('has_usfs_trails')
if (saved) {
setHillshade(hsAvailable && (saved.hillshade ?? true)) if (saved) {
setTraffic(trAvailable && (saved.traffic ?? false)) setHillshade(hsAvailable && (saved.hillshade ?? true))
setPublicLands(plAvailable && (saved.publicLands ?? false)) setTraffic(trAvailable && (saved.traffic ?? false))
setContours(ctAvailable && (saved.contours ?? false)) setPublicLands(plAvailable && (saved.publicLands ?? false))
setContoursTest(ctTestAvailable && (saved.contoursTest ?? false)) setContours(ctAvailable && (saved.contours ?? false))
setContoursTest10ft(ctTest10ftAvailable && (saved.contoursTest10ft ?? false)) setContoursTest(ctTestAvailable && (saved.contoursTest ?? false))
} else { setContoursTest10ft(ctTest10ftAvailable && (saved.contoursTest10ft ?? false))
// Defaults: hillshade ON if available, others OFF setUsfsTrails(usfsAvailable && (saved.usfsTrails ?? false))
setHillshade(hsAvailable) } else {
setTraffic(false) // Defaults: hillshade ON if available, others OFF
setPublicLands(false) setHillshade(hsAvailable)
setContours(false) setTraffic(false)
setContoursTest(false) setPublicLands(false)
setContoursTest10ft(false) setContours(false)
} setContoursTest(false)
}, []) setContoursTest10ft(false)
setUsfsTrails(false)
// Apply layers when prefs change }
useEffect(() => { }, [])
const mapView = mapRef?.current
if (!mapView) return // Apply layers when prefs change
const map = mapView.getMap?.() useEffect(() => {
if (!map) return const mapView = mapRef?.current
if (!mapView) return
const apply = () => { const map = mapView.getMap?.()
if (hillshade && hasFeature('has_hillshade')) { if (!map) return
mapView.addHillshadeLayer?.()
} else { const apply = () => {
mapView.removeHillshadeLayer?.() if (hillshade && hasFeature('has_hillshade')) {
} mapView.addHillshadeLayer?.()
} } else {
mapView.removeHillshadeLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft }) } else {
return () => map.off('style.load', apply) map.once('style.load', apply)
}, [hillshade, mapRef]) }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
useEffect(() => { return () => map.off('style.load', apply)
const mapView = mapRef?.current }, [hillshade, mapRef])
if (!mapView) return
const map = mapView.getMap?.() useEffect(() => {
if (!map) return const mapView = mapRef?.current
if (!mapView) return
const apply = () => { const map = mapView.getMap?.()
if (traffic && hasFeature('has_traffic_overlay')) { if (!map) return
mapView.addTrafficLayer?.()
} else { const apply = () => {
mapView.removeTrafficLayer?.() if (traffic && hasFeature('has_traffic_overlay')) {
} mapView.addTrafficLayer?.()
} } else {
mapView.removeTrafficLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft }) } else {
return () => map.off('style.load', apply) map.once('style.load', apply)
}, [traffic, mapRef]) }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
useEffect(() => { return () => map.off('style.load', apply)
const mapView = mapRef?.current }, [traffic, mapRef])
if (!mapView) return
const map = mapView.getMap?.() useEffect(() => {
if (!map) return const mapView = mapRef?.current
if (!mapView) return
const apply = () => { const map = mapView.getMap?.()
if (publicLands && hasFeature('has_public_lands_layer')) { if (!map) return
mapView.addPublicLandsLayer?.()
} else { const apply = () => {
mapView.removePublicLandsLayer?.() if (publicLands && hasFeature('has_public_lands_layer')) {
} mapView.addPublicLandsLayer?.()
} } else {
mapView.removePublicLandsLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft }) } else {
return () => map.off('style.load', apply) map.once('style.load', apply)
}, [publicLands, mapRef]) }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
useEffect(() => { return () => map.off('style.load', apply)
const mapView = mapRef?.current }, [publicLands, mapRef])
if (!mapView) return
const map = mapView.getMap?.() useEffect(() => {
if (!map) return const mapView = mapRef?.current
if (!mapView) return
const apply = () => { const map = mapView.getMap?.()
if (contours && hasFeature('has_contours')) { if (!map) return
mapView.addContoursLayer?.()
} else { const apply = () => {
mapView.removeContoursLayer?.() if (contours && hasFeature('has_contours')) {
} mapView.addContoursLayer?.()
} } else {
mapView.removeContoursLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft }) } else {
return () => map.off('style.load', apply) map.once('style.load', apply)
}, [contours, mapRef]) }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
useEffect(() => { return () => map.off('style.load', apply)
const mapView = mapRef?.current }, [contours, mapRef])
if (!mapView) return
const map = mapView.getMap?.() useEffect(() => {
if (!map) return const mapView = mapRef?.current
if (!mapView) return
const apply = () => { const map = mapView.getMap?.()
if (contoursTest && hasFeature('has_contours_test')) { if (!map) return
mapView.addContoursTestLayer?.()
} else { const apply = () => {
mapView.removeContoursTestLayer?.() if (contoursTest && hasFeature('has_contours_test')) {
} mapView.addContoursTestLayer?.()
} } else {
mapView.removeContoursTestLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft }) } else {
return () => map.off('style.load', apply) map.once('style.load', apply)
}, [contoursTest, mapRef]) }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
// Apply contoursTest10ft layer return () => map.off('style.load', apply)
useEffect(() => { }, [contoursTest, mapRef])
const map = mapRef.current?.getMap?.()
if (!map) return // Apply contoursTest10ft layer
useEffect(() => {
const apply = () => { const map = mapRef.current?.getMap?.()
if (contoursTest10ft && hasFeature('has_contours_test_10ft')) { if (!map) return
mapRef.current?.addContoursTest10ftLayer?.()
} else { const apply = () => {
mapRef.current?.removeContoursTest10ftLayer?.() if (contoursTest10ft && hasFeature('has_contours_test_10ft')) {
} mapRef.current?.addContoursTest10ftLayer?.()
} } else {
mapRef.current?.removeContoursTest10ftLayer?.()
if (map.isStyleLoaded()) { }
apply() }
} else {
map.once('style.load', apply) if (map.isStyleLoaded()) {
} apply()
}, [contoursTest10ft, mapRef]) } else {
map.once('style.load', apply)
// Close on outside click }
useEffect(() => { }, [contoursTest10ft, mapRef])
if (!open) return
function handleClick(e) { // Apply usfsTrails layer
if (panelRef.current && !panelRef.current.contains(e.target)) { useEffect(() => {
setOpen(false) const map = mapRef.current?.getMap?.()
} if (!map) return
}
document.addEventListener('pointerdown', handleClick) const apply = () => {
return () => document.removeEventListener('pointerdown', handleClick) if (usfsTrails && hasFeature('has_usfs_trails')) {
}, [open]) mapRef.current?.addUsfsTrailsLayer?.()
} else {
const showHillshade = hasFeature('has_hillshade') mapRef.current?.removeUsfsTrailsLayer?.()
const showTraffic = hasFeature('has_traffic_overlay') }
const showPublicLands = hasFeature('has_public_lands_layer') }
const showContours = hasFeature('has_contours')
const showContoursTest = hasFeature('has_contours_test') if (map.isStyleLoaded()) {
const showContoursTest10ft = hasFeature('has_contours_test_10ft') apply()
} else {
// Don't render if no overlay features available map.once('style.load', apply)
if (!showHillshade && !showTraffic && !showPublicLands && !showContours && !showContoursTest && !showContoursTest10ft) return null }
savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
return ( }, [usfsTrails, mapRef])
<div ref={panelRef} className="layer-control">
<button // Close on outside click
className="layer-control-btn" useEffect(() => {
onClick={() => setOpen((v) => !v)} if (!open) return
title="Map layers" function handleClick(e) {
aria-label="Toggle map layers" if (panelRef.current && !panelRef.current.contains(e.target)) {
> setOpen(false)
<Layers size={18} /> }
</button> }
document.addEventListener('pointerdown', handleClick)
{open && ( return () => document.removeEventListener('pointerdown', handleClick)
<div className="layer-control-popover"> }, [open])
<div className="layer-control-header">Layers</div>
const showHillshade = hasFeature('has_hillshade')
{showHillshade && ( const showTraffic = hasFeature('has_traffic_overlay')
<label className="layer-control-item"> const showPublicLands = hasFeature('has_public_lands_layer')
<span className="layer-control-label">Hillshade</span> const showContours = hasFeature('has_contours')
<input const showContoursTest = hasFeature('has_contours_test')
type="checkbox" const showContoursTest10ft = hasFeature('has_contours_test_10ft')
className="layer-control-toggle" const showUsfsTrails = hasFeature('has_usfs_trails')
checked={hillshade}
onChange={(e) => setHillshade(e.target.checked)} // Don't render if no overlay features available
/> if (!showHillshade && !showTraffic && !showPublicLands && !showContours && !showContoursTest && !showContoursTest10ft && !showUsfsTrails) return null
</label>
)} return (
<div ref={panelRef} className="layer-control">
{showTraffic && ( <button
<label className="layer-control-item"> className="layer-control-btn"
<span className="layer-control-label">Traffic</span> onClick={() => setOpen((v) => !v)}
<input title="Map layers"
type="checkbox" aria-label="Toggle map layers"
className="layer-control-toggle" >
checked={traffic} <Layers size={18} />
onChange={(e) => setTraffic(e.target.checked)} </button>
/>
</label> {open && (
)} <div className="layer-control-popover">
<div className="layer-control-header">Layers</div>
{showPublicLands && (
<label className="layer-control-item"> {showHillshade && (
<span className="layer-control-label">Public Lands</span> <label className="layer-control-item">
<input <span className="layer-control-label">Hillshade</span>
type="checkbox" <input
className="layer-control-toggle" type="checkbox"
checked={publicLands} className="layer-control-toggle"
onChange={(e) => setPublicLands(e.target.checked)} checked={hillshade}
/> onChange={(e) => setHillshade(e.target.checked)}
</label> />
)} </label>
)}
{showContours && (
<label className="layer-control-item"> {showTraffic && (
<span className="layer-control-label">Contours</span> <label className="layer-control-item">
<input <span className="layer-control-label">Traffic</span>
type="checkbox" <input
className="layer-control-toggle" type="checkbox"
checked={contours} className="layer-control-toggle"
onChange={(e) => setContours(e.target.checked)} checked={traffic}
/> onChange={(e) => setTraffic(e.target.checked)}
</label> />
)} </label>
)}
{showContoursTest && (
<label className="layer-control-item"> {showPublicLands && (
<span className="layer-control-label">Contours (Test)</span> <label className="layer-control-item">
<input <span className="layer-control-label">Public Lands</span>
type="checkbox" <input
className="layer-control-toggle" type="checkbox"
checked={contoursTest} className="layer-control-toggle"
onChange={(e) => setContoursTest(e.target.checked)} checked={publicLands}
/> onChange={(e) => setPublicLands(e.target.checked)}
</label> />
)} </label>
)}
{showContoursTest10ft && (
<label className="layer-control-item"> {showContours && (
<span className="layer-control-label">Contours (Test 10ft)</span> <label className="layer-control-item">
<input <span className="layer-control-label">Contours</span>
type="checkbox" <input
className="layer-control-toggle" type="checkbox"
checked={contoursTest10ft} className="layer-control-toggle"
onChange={(e) => setContoursTest10ft(e.target.checked)} checked={contours}
/> onChange={(e) => setContours(e.target.checked)}
</label> />
)} </label>
</div> )}
)}
</div> {showContoursTest && (
) <label className="layer-control-item">
} <span className="layer-control-label">Contours (Test)</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={contoursTest}
onChange={(e) => setContoursTest(e.target.checked)}
/>
</label>
)}
{showContoursTest10ft && (
<label className="layer-control-item">
<span className="layer-control-label">Contours (Test 10ft)</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={contoursTest10ft}
onChange={(e) => setContoursTest10ft(e.target.checked)}
/>
</label>
)}
{showUsfsTrails && (
<label className="layer-control-item">
<span className="layer-control-label">USFS Trails</span>
<input
type="checkbox"
className="layer-control-toggle"
checked={usfsTrails}
onChange={(e) => setUsfsTrails(e.target.checked)}
/>
</label>
)}
</div>
)}
</div>
)
}

View file

@ -42,6 +42,12 @@ const CONTOUR_TEST_10FT_LABEL = 'contour-test-10ft-label'
const MEASURE_SOURCE = 'measure-source' const MEASURE_SOURCE = 'measure-source'
const MEASURE_LINE_LAYER = 'measure-line-layer' const MEASURE_LINE_LAYER = 'measure-line-layer'
const MEASURE_POINT_LAYER = 'measure-point-layer' const MEASURE_POINT_LAYER = 'measure-point-layer'
const USFS_SOURCE = 'usfs-trails-source'
const USFS_ROADS_LAYER = 'usfs-roads-layer'
const USFS_TRAILS_LAYER = 'usfs-trails-layer'
const USFS_ROADS_LABEL = 'usfs-roads-label'
const USFS_TRAILS_LABEL = 'usfs-trails-label'
// Highlight state - use data-driven expressions to target specific features // Highlight state - use data-driven expressions to target specific features
const INTERACTIVE_LABEL_LAYERS = ['pois', 'places_subplace', 'places_locality', 'places_region', 'places_country'] const INTERACTIVE_LABEL_LAYERS = ['pois', 'places_subplace', 'places_locality', 'places_region', 'places_country']
@ -813,6 +819,119 @@ function removeContoursTest10ft(map) {
if (map.getLayer(CONTOUR_TEST_10FT_MINOR)) map.removeLayer(CONTOUR_TEST_10FT_MINOR) if (map.getLayer(CONTOUR_TEST_10FT_MINOR)) map.removeLayer(CONTOUR_TEST_10FT_MINOR)
if (map.getSource(CONTOUR_TEST_10FT_SOURCE)) map.removeSource(CONTOUR_TEST_10FT_SOURCE) if (map.getSource(CONTOUR_TEST_10FT_SOURCE)) map.removeSource(CONTOUR_TEST_10FT_SOURCE)
} }
/** Add USFS trails and roads vector tile overlay */
function addUsfsTrails(map) {
if (!map || map.getSource(USFS_SOURCE)) return
map.addSource(USFS_SOURCE, {
type: "vector",
url: "pmtiles:///tiles/usfs-trails-roads.pmtiles",
})
// Insert below first symbol layer (above other overlays, below labels)
let beforeId = undefined
for (const layer of map.getStyle().layers) {
if (layer.type === "symbol") {
beforeId = layer.id
break
}
}
const isDark = document.documentElement.getAttribute("data-theme") === "dark"
const opMod = isDark ? 0.8 : 1.0
// Roads layer - solid khaki/tan line
map.addLayer({
id: USFS_ROADS_LAYER,
type: "line",
source: USFS_SOURCE,
"source-layer": "roads",
minzoom: 10,
paint: {
"line-color": isDark ? "#9a8b70" : "#b8a87a",
"line-opacity": 0.65 * opMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 10, 1.0, 14, 2.0, 16, 3.0],
},
}, beforeId)
// Trails layer - dashed earth-tone brown
map.addLayer({
id: USFS_TRAILS_LAYER,
type: "line",
source: USFS_SOURCE,
"source-layer": "trails",
minzoom: 10,
paint: {
"line-color": isDark ? "#a88960" : "#8b7355",
"line-opacity": 0.7 * opMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 16, 3.5],
"line-dasharray": [2, 1.5],
},
}, beforeId)
// Road labels (zoom 12+)
map.addLayer({
id: USFS_ROADS_LABEL,
type: "symbol",
source: USFS_SOURCE,
"source-layer": "roads",
minzoom: 12,
filter: ["has", "NAME"],
layout: {
"text-field": ["get", "NAME"],
"text-size": 10,
"text-font": ["Noto Sans Regular"],
"symbol-placement": "line",
"text-anchor": "center",
"symbol-spacing": 300,
"text-max-angle": 25,
"text-allow-overlap": false,
},
paint: {
"text-color": isDark ? "#c0b090" : "#6a5a40",
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff",
"text-halo-width": 1.5,
"text-opacity": 0.85,
},
})
// Trail labels (zoom 12+)
map.addLayer({
id: USFS_TRAILS_LABEL,
type: "symbol",
source: USFS_SOURCE,
"source-layer": "trails",
minzoom: 12,
filter: ["has", "TRAIL_NAME"],
layout: {
"text-field": ["get", "TRAIL_NAME"],
"text-size": 10,
"text-font": ["Noto Sans Regular"],
"symbol-placement": "line",
"text-anchor": "center",
"symbol-spacing": 300,
"text-max-angle": 25,
"text-allow-overlap": false,
},
paint: {
"text-color": isDark ? "#c8a878" : "#5a4530",
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff",
"text-halo-width": 1.5,
"text-opacity": 0.85,
},
})
}
/** Remove USFS trails/roads layers and source */
function removeUsfsTrails(map) {
if (!map) return
if (map.getLayer(USFS_TRAILS_LABEL)) map.removeLayer(USFS_TRAILS_LABEL)
if (map.getLayer(USFS_ROADS_LABEL)) map.removeLayer(USFS_ROADS_LABEL)
if (map.getLayer(USFS_TRAILS_LAYER)) map.removeLayer(USFS_TRAILS_LAYER)
if (map.getLayer(USFS_ROADS_LAYER)) map.removeLayer(USFS_ROADS_LAYER)
if (map.getSource(USFS_SOURCE)) map.removeSource(USFS_SOURCE)
}
/** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */ /** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */
const BOUNDARY_FILL_LAYER = 'boundary-fill-layer' const BOUNDARY_FILL_LAYER = 'boundary-fill-layer'
@ -871,7 +990,7 @@ const MapView = forwardRef(function MapView(_, ref) {
const watchIdRef = useRef(null) const watchIdRef = useRef(null)
const currentThemeRef = useRef('dark') const currentThemeRef = useRef('dark')
// Track which overlay layers are currently active (for theme swap re-add) // Track which overlay layers are currently active (for theme swap re-add)
const activeLayersRef = useRef({ hillshade: false, traffic: false, contours: false, contoursTest: false, contoursTest10ft: false }) const activeLayersRef = useRef({ hillshade: false, traffic: false, contours: false, contoursTest: false, contoursTest10ft: false, usfsTrails: false })
// Flag to suppress map-click when a stop pin was clicked // Flag to suppress map-click when a stop pin was clicked
const pinClickedRef = useRef(false) const pinClickedRef = useRef(false)
const highlightedFeatureRef = useRef(null) // { source, sourceLayer, id } for setFeatureState const highlightedFeatureRef = useRef(null) // { source, sourceLayer, id } for setFeatureState
@ -1317,6 +1436,19 @@ const MapView = forwardRef(function MapView(_, ref) {
removeContoursTest10ft(map) removeContoursTest10ft(map)
activeLayersRef.current.contoursTest10ft = false activeLayersRef.current.contoursTest10ft = false
}, },
addUsfsTrailsLayer() {
const map = mapInstance.current
if (!map) return
addUsfsTrails(map)
activeLayersRef.current.usfsTrails = true
},
removeUsfsTrailsLayer() {
const map = mapInstance.current
if (!map) return
removeUsfsTrails(map)
activeLayersRef.current.usfsTrails = false
},
})) }))
// Initialize map // Initialize map
@ -1446,6 +1578,55 @@ const MapView = forwardRef(function MapView(_, ref) {
const { lng, lat } = e.lngLat const { lng, lat } = e.lngLat
const MARKER_RADIUS_PX = 14 // half of 28px preview marker const MARKER_RADIUS_PX = 14 // half of 28px preview marker
// Check for USFS trails/roads click (show info popup)
const usfsLayers = [USFS_TRAILS_LAYER, USFS_ROADS_LAYER]
const usfsFeatures = map.queryRenderedFeatures(e.point, { layers: usfsLayers })
const usfsFeature = usfsFeatures.find(f => f.properties)
if (usfsFeature && hasFeature('has_usfs_trails')) {
const props = usfsFeature.properties
const isTrail = usfsFeature.layer?.id === USFS_TRAILS_LAYER
const name = isTrail ? (props.TRAIL_NAME || 'Unnamed Trail') : (props.NAME || 'Unnamed Road')
const typeLabel = isTrail ? 'USFS Trail' : 'USFS Road'
// Build popup content
let html = '<div style="font-size:12px;max-width:240px;line-height:1.4">'
html += '<strong style="font-size:13px">' + name + '</strong>'
html += '<div style="color:var(--text-secondary);font-size:11px;margin-bottom:4px">' + typeLabel + '</div>'
if (isTrail) {
// Trail-specific info
if (props.TRAIL_TYPE) html += '<div><b>Type:</b> ' + props.TRAIL_TYPE + '</div>'
if (props.TRAIL_SURF) html += '<div><b>Surface:</b> ' + props.TRAIL_SURF + '</div>'
if (props.GIS_MILES) html += '<div><b>Length:</b> ' + parseFloat(props.GIS_MILES).toFixed(1) + ' mi</div>'
// Allowed uses
const uses = []
if (props.HIKER_PEDE === 'Y') uses.push('Hiking')
if (props.BICYCLE_MA === 'Y') uses.push('Biking')
if (props.MOTORCYCLE === 'Y') uses.push('Motorcycle')
if (props.ATV_MANAGE === 'Y') uses.push('ATV')
if (props.HORSE_MANA === 'Y') uses.push('Horse')
if (uses.length > 0) html += '<div><b>Allowed:</b> ' + uses.join(', ') + '</div>'
} else {
// Road-specific info
if (props.OPER_MAINT) html += '<div><b>Maintenance:</b> ' + props.OPER_MAINT + '</div>'
if (props.SURFACE_TY) html += '<div><b>Surface:</b> ' + props.SURFACE_TY + '</div>'
if (props.ROUTE_STAT) html += '<div><b>Status:</b> ' + props.ROUTE_STAT + '</div>'
}
html += '</div>'
// Remove existing popup
if (popupRef.current) popupRef.current.remove()
const popup = new maplibregl.Popup({ offset: 10, closeButton: true })
.setLngLat([lng, lat])
.setHTML(html)
.addTo(map)
popupRef.current = popup
return
}
// Query rendered features at click point (label/POI priority) // Query rendered features at click point (label/POI priority)
const labelLayers = ['pois', 'places_subplace', 'places_locality', 'places_region', 'places_country'] const labelLayers = ['pois', 'places_subplace', 'places_locality', 'places_region', 'places_country']
const features = map.queryRenderedFeatures(e.point, { layers: labelLayers }) const features = map.queryRenderedFeatures(e.point, { layers: labelLayers })

View file

@ -33,6 +33,7 @@ const FALLBACK_CONFIG = {
has_contours_test: true, has_contours_test: true,
has_contours_test_10ft: false, has_contours_test_10ft: false,
has_address_book_write: false, has_address_book_write: false,
has_usfs_trails: false,
has_contacts: false, has_contacts: false,
}, },
defaults: { defaults: {