diff --git a/src/components/LayerControl.jsx b/src/components/LayerControl.jsx
index c929888..2e234cc 100644
--- a/src/components/LayerControl.jsx
+++ b/src/components/LayerControl.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react'
-import { Layers } from 'lucide-react'
+import { Layers, Trees } from 'lucide-react'
import { hasFeature, getConfig } from '../config'
const STORAGE_KEY = 'navi-layer-prefs'
@@ -20,6 +20,7 @@ export default function LayerControl({ mapRef }) {
const [open, setOpen] = useState(false)
const [hillshade, setHillshade] = useState(false)
const [traffic, setTraffic] = useState(false)
+ const [publicLands, setPublicLands] = useState(false)
const panelRef = useRef(null)
// Initialize from localStorage or defaults on mount
@@ -28,13 +29,17 @@ export default function LayerControl({ mapRef }) {
const hsAvailable = hasFeature('has_hillshade')
const trAvailable = hasFeature('has_traffic_overlay')
+ const plAvailable = hasFeature('has_public_lands_layer')
+
if (saved) {
setHillshade(hsAvailable && (saved.hillshade ?? true))
setTraffic(trAvailable && (saved.traffic ?? false))
+ setPublicLands(plAvailable && (saved.publicLands ?? false))
} else {
- // Defaults: hillshade ON if available, traffic OFF
+ // Defaults: hillshade ON if available, traffic + publicLands OFF
setHillshade(hsAvailable)
setTraffic(false)
+ setPublicLands(false)
}
}, [])
@@ -58,7 +63,7 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic })
+ savePrefs({ hillshade, traffic, publicLands })
return () => map.off('style.load', apply)
}, [hillshade, mapRef])
@@ -81,10 +86,33 @@ export default function LayerControl({ mapRef }) {
} else {
map.once('style.load', apply)
}
- savePrefs({ hillshade, traffic })
+ savePrefs({ hillshade, traffic, publicLands })
return () => map.off('style.load', apply)
}, [traffic, mapRef])
+ 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)
+ }
+ savePrefs({ hillshade, traffic, publicLands })
+ return () => map.off('style.load', apply)
+ }, [publicLands, mapRef])
+
// Close on outside click
useEffect(() => {
if (!open) return
@@ -99,9 +127,10 @@ export default function LayerControl({ mapRef }) {
const showHillshade = hasFeature('has_hillshade')
const showTraffic = hasFeature('has_traffic_overlay')
+ const showPublicLands = hasFeature('has_public_lands_layer')
// Don't render if no overlay features available
- if (!showHillshade && !showTraffic) return null
+ if (!showHillshade && !showTraffic && !showPublicLands) return null
return (
@@ -141,6 +170,18 @@ export default function LayerControl({ mapRef }) {
/>
)}
+
+ {showPublicLands && (
+
+ )}
)}
diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx
index 5818a1e..7da3865 100644
--- a/src/components/MapView.jsx
+++ b/src/components/MapView.jsx
@@ -14,6 +14,10 @@ const HILLSHADE_SOURCE = 'hillshade-dem'
const HILLSHADE_LAYER = 'hillshade-layer'
const TRAFFIC_SOURCE = 'traffic-tiles'
const TRAFFIC_LAYER = 'traffic-layer'
+const PUBLIC_LANDS_SOURCE = 'public-lands-tiles'
+const PUBLIC_LANDS_FILL = 'public-lands-fill'
+const PUBLIC_LANDS_LINE = 'public-lands-line'
+const PUBLIC_LANDS_LABEL = 'public-lands-label'
/** Build a full MapLibre style object for the given theme */
function buildStyle(themeName) {
@@ -118,6 +122,150 @@ function removeTraffic(map) {
if (map.getSource(TRAFFIC_SOURCE)) map.removeSource(TRAFFIC_SOURCE)
}
+/** Add public lands vector tile overlay (PAD-US) */
+function addPublicLands(map) {
+ if (!map || map.getSource(PUBLIC_LANDS_SOURCE)) return
+
+ map.addSource(PUBLIC_LANDS_SOURCE, {
+ type: 'vector',
+ url: 'pmtiles:///tiles/public-lands.pmtiles',
+ })
+
+ // Insert below symbol layers for proper z-ordering
+ 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 opacityMod = isDark ? 0.7 : 1.0
+
+ // Fill layer — data-driven color by agency + designation
+ map.addLayer({
+ id: PUBLIC_LANDS_FILL,
+ type: 'fill',
+ source: PUBLIC_LANDS_SOURCE,
+ 'source-layer': 'public_lands',
+ paint: {
+ 'fill-color': [
+ 'case',
+ ['==', ['get', 'designation'], 'WA'], '#7c6b2f',
+ ['==', ['get', 'designation'], 'WSA'], '#7c6b2f',
+ ['==', ['get', 'agency'], 'NPS'], '#3d6b1f',
+ ['==', ['get', 'agency'], 'USFS'], '#5a7c2f',
+ ['==', ['get', 'agency'], 'BLM'], '#c4a672',
+ ['==', ['get', 'agency'], 'FWS'], '#4a7a5a',
+ ['any',
+ ['==', ['get', 'manager_type'], 'STAT'],
+ ['==', ['get', 'agency'], 'SPR'],
+ ['==', ['get', 'agency'], 'SDC'],
+ ['==', ['get', 'agency'], 'SLB']
+ ], '#5a8c7c',
+ ['any',
+ ['==', ['get', 'manager_type'], 'LOC'],
+ ['==', ['get', 'manager_type'], 'DIST']
+ ], '#8ca694',
+ '#a0a0a0'
+ ],
+ 'fill-opacity': [
+ 'case',
+ ['==', ['get', 'designation'], 'WA'], 0.30 * opacityMod,
+ ['==', ['get', 'designation'], 'WSA'], 0.30 * opacityMod,
+ ['==', ['get', 'agency'], 'NPS'], 0.30 * opacityMod,
+ ['==', ['get', 'agency'], 'USFS'], 0.25 * opacityMod,
+ ['==', ['get', 'agency'], 'BLM'], 0.20 * opacityMod,
+ ['any',
+ ['==', ['get', 'manager_type'], 'STAT'],
+ ['==', ['get', 'agency'], 'SPR']
+ ], 0.25 * opacityMod,
+ ['any',
+ ['==', ['get', 'manager_type'], 'LOC'],
+ ['==', ['get', 'manager_type'], 'DIST']
+ ], 0.20 * opacityMod,
+ 0.15 * opacityMod
+ ],
+ },
+ }, beforeId)
+
+ // Outline layer
+ map.addLayer({
+ id: PUBLIC_LANDS_LINE,
+ type: 'line',
+ source: PUBLIC_LANDS_SOURCE,
+ 'source-layer': 'public_lands',
+ paint: {
+ 'line-color': [
+ 'case',
+ ['==', ['get', 'designation'], 'WA'], '#5a4d20',
+ ['==', ['get', 'designation'], 'WSA'], '#5a4d20',
+ ['==', ['get', 'agency'], 'NPS'], '#2a4a15',
+ ['==', ['get', 'agency'], 'USFS'], '#3d5520',
+ ['==', ['get', 'agency'], 'BLM'], '#8a7343',
+ ['==', ['get', 'agency'], 'FWS'], '#2d5a3a',
+ ['any',
+ ['==', ['get', 'manager_type'], 'STAT'],
+ ['==', ['get', 'agency'], 'SPR']
+ ], '#3d6055',
+ ['any',
+ ['==', ['get', 'manager_type'], 'LOC'],
+ ['==', ['get', 'manager_type'], 'DIST']
+ ], '#5c6e66',
+ '#707070'
+ ],
+ 'line-opacity': [
+ 'case',
+ ['==', ['get', 'agency'], 'NPS'], 0.7,
+ ['==', ['get', 'agency'], 'USFS'], 0.6,
+ ['==', ['get', 'agency'], 'BLM'], 0.5,
+ 0.5
+ ],
+ 'line-width': [
+ 'interpolate', ['linear'], ['zoom'],
+ 4, 0.3,
+ 8, 0.8,
+ 12, 1.2
+ ],
+ },
+ }, beforeId)
+
+ // Label layer — unit names at zoom 10+
+ map.addLayer({
+ id: PUBLIC_LANDS_LABEL,
+ type: 'symbol',
+ source: PUBLIC_LANDS_SOURCE,
+ 'source-layer': 'public_lands',
+ minzoom: 10,
+ layout: {
+ 'text-field': ['get', 'name'],
+ 'text-size': ['interpolate', ['linear'], ['zoom'], 10, 10, 14, 13],
+ 'text-font': ['Noto Sans Regular'],
+ 'symbol-placement': 'point',
+ 'text-anchor': 'center',
+ 'text-max-width': 8,
+ 'text-allow-overlap': false,
+ 'text-ignore-placement': false,
+ },
+ paint: {
+ 'text-color': isDark ? '#c0c8b8' : '#3a4a30',
+ 'text-halo-color': isDark ? '#1a1a1a' : '#ffffff',
+ 'text-halo-width': 1.5,
+ 'text-opacity': 0.85,
+ },
+ })
+}
+
+/** Remove public lands layers + source */
+function removePublicLands(map) {
+ if (!map) return
+ if (map.getLayer(PUBLIC_LANDS_LABEL)) map.removeLayer(PUBLIC_LANDS_LABEL)
+ if (map.getLayer(PUBLIC_LANDS_LINE)) map.removeLayer(PUBLIC_LANDS_LINE)
+ if (map.getLayer(PUBLIC_LANDS_FILL)) map.removeLayer(PUBLIC_LANDS_FILL)
+ if (map.getSource(PUBLIC_LANDS_SOURCE)) map.removeSource(PUBLIC_LANDS_SOURCE)
+}
+
const MapView = forwardRef(function MapView(_, ref) {
const mapRef = useRef(null)
const mapInstance = useRef(null)
@@ -172,6 +320,18 @@ const MapView = forwardRef(function MapView(_, ref) {
removeTraffic(map)
activeLayersRef.current.traffic = false
},
+ addPublicLandsLayer() {
+ const map = mapInstance.current
+ if (!map) return
+ addPublicLands(map)
+ activeLayersRef.current.publicLands = true
+ },
+ removePublicLandsLayer() {
+ const map = mapInstance.current
+ if (!map) return
+ removePublicLands(map)
+ activeLayersRef.current.publicLands = false
+ },
}))
// Initialize map
@@ -255,6 +415,10 @@ const MapView = forwardRef(function MapView(_, ref) {
addTraffic(map)
activeLayersRef.current.traffic = true
}
+ if (prefs.publicLands && hasFeature('has_public_lands_layer')) {
+ addPublicLands(map)
+ activeLayersRef.current.publicLands = true
+ }
} else if (hasFeature('has_hillshade')) {
// Default: hillshade ON if available
addHillshade(map)
@@ -355,6 +519,7 @@ const MapView = forwardRef(function MapView(_, ref) {
// Re-add active overlay layers
if (activeLayersRef.current.hillshade) addHillshade(map)
if (activeLayersRef.current.traffic) addTraffic(map)
+ if (activeLayersRef.current.publicLands) addPublicLands(map)
// Restore view
map.jumpTo({ center, zoom, bearing, pitch })
diff --git a/src/config.js b/src/config.js
index 87496fe..0d84fe4 100644
--- a/src/config.js
+++ b/src/config.js
@@ -28,6 +28,7 @@ const FALLBACK_CONFIG = {
has_3d_terrain: false,
has_traffic_overlay: false,
has_landclass: false,
+ has_public_lands_layer: false,
has_address_book_write: false,
has_contacts: false,
},