diff --git a/src/components/LayerControl.jsx b/src/components/LayerControl.jsx
index b62484b..cf2e34e 100644
--- a/src/components/LayerControl.jsx
+++ b/src/components/LayerControl.jsx
@@ -24,7 +24,8 @@ export default function LayerControl({ mapRef }) {
const [contours, setContours] = useState(false)
const [contoursTest, setContoursTest] = useState(false)
const [contoursTest10ft, setContoursTest10ft] = useState(false)
- const [usfsTrails, setUsfsTrails] = useState(false)
+ const [usfsTrails, setUsfsTrails] = useState(false)
+ const [blmTrails, setBlmTrails] = useState(false)
const panelRef = useRef(null)
// Initialize from localStorage or defaults on mount
@@ -36,7 +37,8 @@ export default function LayerControl({ mapRef }) {
const ctAvailable = hasFeature('has_contours')
const ctTestAvailable = hasFeature('has_contours_test')
const ctTest10ftAvailable = hasFeature('has_contours_test_10ft')
- const usfsAvailable = hasFeature('has_usfs_trails')
+ const usfsAvailable = hasFeature('has_usfs_trails')
+ const blmAvailable = hasFeature('has_blm_trails')
if (saved) {
setHillshade(hsAvailable && (saved.hillshade ?? true))
@@ -45,7 +47,8 @@ export default function LayerControl({ mapRef }) {
setContours(ctAvailable && (saved.contours ?? false))
setContoursTest(ctTestAvailable && (saved.contoursTest ?? false))
setContoursTest10ft(ctTest10ftAvailable && (saved.contoursTest10ft ?? false))
- setUsfsTrails(usfsAvailable && (saved.usfsTrails ?? false))
+ setUsfsTrails(usfsAvailable && (saved.usfsTrails ?? false))
+ setBlmTrails(blmAvailable && (saved.blmTrails ?? false))
} else {
// Defaults: hillshade ON if available, others OFF
setHillshade(hsAvailable)
@@ -78,7 +81,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [hillshade, mapRef])
@@ -101,7 +104,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [traffic, mapRef])
@@ -124,7 +127,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [publicLands, mapRef])
@@ -147,7 +150,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [contours, mapRef])
@@ -170,7 +173,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
return () => map.off('style.load', apply)
}, [contoursTest, mapRef])
@@ -212,8 +215,29 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails })
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
}, [usfsTrails, mapRef])
+
+ // Apply blmTrails layer
+ useEffect(() => {
+ const map = mapRef.current?.getMap?.()
+ if (!map) return
+
+ const apply = () => {
+ if (blmTrails && hasFeature("has_blm_trails")) {
+ mapRef.current?.addBlmTrailsLayer?.()
+ } else {
+ mapRef.current?.removeBlmTrailsLayer?.()
+ }
+ }
+
+ if (map.isStyleLoaded()) {
+ apply()
+ } else {
+ map.once("style.load", apply)
+ }
+ savePrefs({ hillshade, traffic, publicLands, contours, contoursTest, contoursTest10ft, usfsTrails, blmTrails })
+ }, [blmTrails, mapRef])
// Close on outside click
useEffect(() => {
@@ -233,10 +257,11 @@ export default function LayerControl({ mapRef }) {
const showContours = hasFeature('has_contours')
const showContoursTest = hasFeature('has_contours_test')
const showContoursTest10ft = hasFeature('has_contours_test_10ft')
- const showUsfsTrails = hasFeature('has_usfs_trails')
+ const showUsfsTrails = hasFeature('has_usfs_trails')
+ const showBlmTrails = hasFeature('has_blm_trails')
// Don't render if no overlay features available
- if (!showHillshade && !showTraffic && !showPublicLands && !showContours && !showContoursTest && !showContoursTest10ft && !showUsfsTrails) return null
+ if (!showHillshade && !showTraffic && !showPublicLands && !showContours && !showContoursTest && !showContoursTest10ft && !showUsfsTrails && !showBlmTrails) return null
return (
@@ -336,6 +361,18 @@ export default function LayerControl({ mapRef }) {
/>
)}
+
+ {showBlmTrails && (
+
+ )}
)}
diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index 4f2e9bc..42b51b9 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -49,6 +49,10 @@ const USFS_ROADS_LABEL = 'usfs-roads-label'
const USFS_TRAILS_LABEL = 'usfs-trails-label'
const USFS_ROADS_HIT = 'usfs-roads-hit'
const USFS_TRAILS_HIT = 'usfs-trails-hit'
+const BLM_SOURCE = 'blm-trails-source'
+const BLM_ROUTES_LAYER = 'blm-routes-layer'
+const BLM_ROUTES_LABEL = 'blm-routes-label'
+const BLM_ROUTES_HIT = 'blm-routes-hit'
// Highlight state - use data-driven expressions to target specific features
@@ -974,6 +978,100 @@ function removeUsfsTrails(map) {
if (map.getLayer(USFS_ROADS_HIT)) map.removeLayer(USFS_ROADS_HIT)
if (map.getSource(USFS_SOURCE)) map.removeSource(USFS_SOURCE)
}
+/** Add BLM trails/roads vector tile overlay */
+function addBlmTrails(map) {
+ if (!map || map.getSource(BLM_SOURCE)) return
+
+ map.addSource(BLM_SOURCE, {
+ type: "vector",
+ url: "pmtiles:///tiles/blm-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
+
+ // Invisible hit-area layer for easier clicking (wide transparent line)
+ map.addLayer({
+ id: BLM_ROUTES_HIT,
+ type: "line",
+ source: BLM_SOURCE,
+ "source-layer": "blm_routes",
+ minzoom: 10,
+ paint: {
+ "line-color": "#000000",
+ "line-opacity": 0,
+ "line-width": 14,
+ },
+ }, beforeId)
+
+ // Routes layer - dashed line, muted olive/sage color
+ map.addLayer({
+ id: BLM_ROUTES_LAYER,
+ type: "line",
+ source: BLM_SOURCE,
+ "source-layer": "blm_routes",
+ minzoom: 10,
+ paint: {
+ "line-color": isDark ? "#8a9a70" : "#6b7a55",
+ "line-opacity": 0.7 * opMod,
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 16, 3.5],
+ "line-dasharray": [3, 2],
+ },
+ }, beforeId)
+
+ // Route labels (zoom 12+)
+ map.addLayer({
+ id: BLM_ROUTES_LABEL,
+ type: "symbol",
+ source: BLM_SOURCE,
+ "source-layer": "blm_routes",
+ minzoom: 12,
+ filter: ["has", "ROUTE_PRMRY_NM"],
+ layout: {
+ "text-field": ["get", "ROUTE_PRMRY_NM"],
+ "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 ? "#a0b090" : "#4a5a40",
+ "text-halo-color": isDark ? "#1a1a1a" : "#ffffff",
+ "text-halo-width": 1.5,
+ "text-opacity": 0.85,
+ },
+ })
+
+ // Cursor pointer on hover for hit layer
+ map.on("mouseenter", BLM_ROUTES_HIT, () => {
+ map.getCanvas().style.cursor = "pointer"
+ })
+ map.on("mouseleave", BLM_ROUTES_HIT, () => {
+ map.getCanvas().style.cursor = ""
+ })
+}
+
+/** Remove BLM trails/roads layers and source */
+function removeBlmTrails(map) {
+ if (!map) return
+ if (map.getLayer(BLM_ROUTES_LABEL)) map.removeLayer(BLM_ROUTES_LABEL)
+ if (map.getLayer(BLM_ROUTES_LAYER)) map.removeLayer(BLM_ROUTES_LAYER)
+ if (map.getLayer(BLM_ROUTES_HIT)) map.removeLayer(BLM_ROUTES_HIT)
+ if (map.getSource(BLM_SOURCE)) map.removeSource(BLM_SOURCE)
+}
+
/** Add boundary polygon layers with computed accent color (MapLibre rejects CSS vars in paint) */
const BOUNDARY_FILL_LAYER = 'boundary-fill-layer'
@@ -1033,7 +1131,7 @@ const MapView = forwardRef(function MapView(_, ref) {
const watchIdRef = useRef(null)
const currentThemeRef = useRef('dark')
// 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, usfsTrails: false })
+ const activeLayersRef = useRef({ hillshade: false, traffic: false, contours: false, contoursTest: false, contoursTest10ft: false, usfsTrails: false, blmTrails: false })
// Flag to suppress map-click when a stop pin was clicked
const pinClickedRef = useRef(false)
const highlightedFeatureRef = useRef(null) // { source, sourceLayer, id } for setFeatureState
@@ -1491,6 +1589,18 @@ const MapView = forwardRef(function MapView(_, ref) {
removeUsfsTrails(map)
activeLayersRef.current.usfsTrails = false
},
+ addBlmTrailsLayer() {
+ const map = mapInstance.current
+ if (!map) return
+ addBlmTrails(map)
+ activeLayersRef.current.blmTrails = true
+ },
+ removeBlmTrailsLayer() {
+ const map = mapInstance.current
+ if (!map) return
+ removeBlmTrails(map)
+ activeLayersRef.current.blmTrails = false
+ },
}))
@@ -1668,6 +1778,37 @@ const MapView = forwardRef(function MapView(_, ref) {
return
}
+ // Check for BLM routes click (show info popup)
+ const blmLayers = [BLM_ROUTES_HIT]
+ const blmFeatures = map.queryRenderedFeatures(e.point, { layers: blmLayers })
+ const blmFeature = blmFeatures.find(f => f.properties)
+ if (blmFeature && hasFeature("has_blm_trails")) {
+ const props = blmFeature.properties
+ const name = props.ROUTE_PRMRY_NM || "Unnamed Route"
+
+ // Build popup content
+ let html = ""
+ html += "
" + name + ""
+ html += "
BLM Route
"
+
+ // Route info - handle potential field name variations across states
+ if (props.PLAN_ASSET_CLASS) html += "
Asset Class: " + props.PLAN_ASSET_CLASS + "
"
+ if (props.PLAN_MODE_TRNSPRT) html += "
Transport: " + props.PLAN_MODE_TRNSPRT + "
"
+ if (props.OBSRVE_SRFCE_TYPE) html += "
Surface: " + props.OBSRVE_SRFCE_TYPE + "
"
+ if (props.GIS_MILES) html += "
Length: " + parseFloat(props.GIS_MILES).toFixed(1) + " mi
"
+ html += "
"
+
+ // 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)
diff --git a/src/config.js b/src/config.js
index 3bd129c..274edba 100644
--- a/src/config.js
+++ b/src/config.js
@@ -34,6 +34,7 @@ const FALLBACK_CONFIG = {
has_contours_test_10ft: false,
has_address_book_write: false,
has_usfs_trails: false,
+ has_blm_trails: false,
has_contacts: false,
},
defaults: {