Wire overlay layers to read styling from theme registry

All overlay layer add functions now read colors, opacities, and widths
from the theme registry instead of hardcoded dark/light branches.

registry.js changes:
- Add complete darkOverlay and lightOverlay config objects
- Each overlay layer has its own config section:
  - hillshade: exaggeration, illuminationDirection, shadowColor, highlightColor
  - traffic: opacity
  - contours: colors, opacities, widths (with opacityMod), label styling
  - contoursTest: cascades from contours, overrides colors
  - contoursTest10ft: cascades from contours, overrides colors
  - publicLands: per-category fill/outline colors and opacities, labels
  - usfsTrails: roads/trails colors by use type, labels
  - blmTrails: route colors by use class, labels
- Add getOverlayConfig(themeId, layerKey) function
- Contour variants cascade missing keys from same theme's contours
- Width values use self-documenting object format: { z11: 0.5, z14: 1.0 }

MapView.jsx changes:
- All 8 overlay add functions now take themeId parameter
- Functions call getOverlayConfig() to get merged config
- No hardcoded color/opacity/width values remain in overlay functions
- Theme switching re-adds all active overlays with new theme config

This is a refactor - light and dark themes render identically to before.
Custom themes can now override individual overlay styling values.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Matt 2026-05-01 15:48:12 +00:00
commit 9530fbbf76
2 changed files with 666 additions and 291 deletions

View file

@ -3,7 +3,7 @@ import maplibregl from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css' import 'maplibre-gl/dist/maplibre-gl.css'
import { Protocol } from 'pmtiles' import { Protocol } from 'pmtiles'
import { layers } from 'protomaps-themes-base' import { layers } from 'protomaps-themes-base'
import { getTheme, getThemeColors, getThemeSprite } from '../themes/registry' import { getTheme, getThemeColors, getThemeSprite, getOverlayConfig } from '../themes/registry'
import { useStore } from '../store' import { useStore } from '../store'
import { decodePolyline } from '../utils/decode' import { decodePolyline } from '../utils/decode'
import { fetchReverse } from '../api' import { fetchReverse } from '../api'
@ -311,12 +311,14 @@ const CHEVRON_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http
</svg>` </svg>`
/** Add hillshade raster-dem source + layer to the map */ /** Add hillshade raster-dem source + layer to the map */
function addHillshade(map) { function addHillshade(map, themeId) {
if (!map || map.getSource(HILLSHADE_SOURCE)) return if (!map || map.getSource(HILLSHADE_SOURCE)) return
const config = getConfig() const config = getConfig()
const hs = config?.tileset_hillshade const hs = config?.tileset_hillshade
if (!hs?.url) return if (!hs?.url) return
const c = getOverlayConfig(themeId, 'hillshade')
map.addSource(HILLSHADE_SOURCE, { map.addSource(HILLSHADE_SOURCE, {
type: 'raster-dem', type: 'raster-dem',
url: `pmtiles://${hs.url}`, url: `pmtiles://${hs.url}`,
@ -339,10 +341,10 @@ function addHillshade(map) {
type: 'hillshade', type: 'hillshade',
source: HILLSHADE_SOURCE, source: HILLSHADE_SOURCE,
paint: { paint: {
'hillshade-exaggeration': 0.5, 'hillshade-exaggeration': c.exaggeration,
'hillshade-illumination-direction': 315, 'hillshade-illumination-direction': c.illuminationDirection,
'hillshade-shadow-color': '#000000', 'hillshade-shadow-color': c.shadowColor,
'hillshade-highlight-color': '#ffffff', 'hillshade-highlight-color': c.highlightColor,
}, },
}, beforeId) }, beforeId)
} }
@ -355,12 +357,13 @@ function removeHillshade(map) {
} }
/** Add traffic raster tile source + layer */ /** Add traffic raster tile source + layer */
function addTraffic(map) { function addTraffic(map, themeId) {
if (!map || map.getSource(TRAFFIC_SOURCE)) return if (!map || map.getSource(TRAFFIC_SOURCE)) return
const config = getConfig() const config = getConfig()
const tr = config?.traffic const tr = config?.traffic
if (!tr?.proxy_url) return if (!tr?.proxy_url) return
const c = getOverlayConfig(themeId, 'traffic')
const tileUrl = tr.proxy_url.replace('{z}', '{z}').replace('{x}', '{x}').replace('{y}', '{y}') const tileUrl = tr.proxy_url.replace('{z}', '{z}').replace('{x}', '{x}').replace('{y}', '{y}')
map.addSource(TRAFFIC_SOURCE, { map.addSource(TRAFFIC_SOURCE, {
@ -375,7 +378,7 @@ function addTraffic(map) {
type: 'raster', type: 'raster',
source: TRAFFIC_SOURCE, source: TRAFFIC_SOURCE,
paint: { paint: {
'raster-opacity': 0.6, 'raster-opacity': c.opacity,
}, },
}) })
} }
@ -388,9 +391,11 @@ function removeTraffic(map) {
} }
/** Add public lands vector tile overlay (PAD-US) */ /** Add public lands vector tile overlay (PAD-US) */
function addPublicLands(map) { function addPublicLands(map, themeId) {
if (!map || map.getSource(PUBLIC_LANDS_SOURCE)) return if (!map || map.getSource(PUBLIC_LANDS_SOURCE)) return
const c = getOverlayConfig(themeId, 'publicLands')
map.addSource(PUBLIC_LANDS_SOURCE, { map.addSource(PUBLIC_LANDS_SOURCE, {
type: 'vector', type: 'vector',
url: 'pmtiles:///tiles/public-lands.pmtiles', url: 'pmtiles:///tiles/public-lands.pmtiles',
@ -405,9 +410,6 @@ function addPublicLands(map) {
} }
} }
const isDark = isCurrentThemeDark()
const opacityMod = isDark ? 0.7 : 1.0
// Fill layer data-driven color by agency + designation // Fill layer data-driven color by agency + designation
map.addLayer({ map.addLayer({
id: PUBLIC_LANDS_FILL, id: PUBLIC_LANDS_FILL,
@ -417,40 +419,40 @@ function addPublicLands(map) {
paint: { paint: {
'fill-color': [ 'fill-color': [
'case', 'case',
['==', ['get', 'designation'], 'WA'], '#7c6b2f', ['==', ['get', 'designation'], 'WA'], c.fillWA,
['==', ['get', 'designation'], 'WSA'], '#7c6b2f', ['==', ['get', 'designation'], 'WSA'], c.fillWA,
['==', ['get', 'agency'], 'NPS'], '#3d6b1f', ['==', ['get', 'agency'], 'NPS'], c.fillNPS,
['==', ['get', 'agency'], 'USFS'], '#5a7c2f', ['==', ['get', 'agency'], 'USFS'], c.fillUSFS,
['==', ['get', 'agency'], 'BLM'], '#c4a672', ['==', ['get', 'agency'], 'BLM'], c.fillBLM,
['==', ['get', 'agency'], 'FWS'], '#4a7a5a', ['==', ['get', 'agency'], 'FWS'], c.fillFWS,
['any', ['any',
['==', ['get', 'manager_type'], 'STAT'], ['==', ['get', 'manager_type'], 'STAT'],
['==', ['get', 'agency'], 'SPR'], ['==', ['get', 'agency'], 'SPR'],
['==', ['get', 'agency'], 'SDC'], ['==', ['get', 'agency'], 'SDC'],
['==', ['get', 'agency'], 'SLB'] ['==', ['get', 'agency'], 'SLB']
], '#5a8c7c', ], c.fillSTAT,
['any', ['any',
['==', ['get', 'manager_type'], 'LOC'], ['==', ['get', 'manager_type'], 'LOC'],
['==', ['get', 'manager_type'], 'DIST'] ['==', ['get', 'manager_type'], 'DIST']
], '#8ca694', ], c.fillLOC,
'#a0a0a0' c.fillDefault
], ],
'fill-opacity': [ 'fill-opacity': [
'case', 'case',
['==', ['get', 'designation'], 'WA'], 0.30 * opacityMod, ['==', ['get', 'designation'], 'WA'], c.fillOpacityWA * c.opacityMod,
['==', ['get', 'designation'], 'WSA'], 0.30 * opacityMod, ['==', ['get', 'designation'], 'WSA'], c.fillOpacityWA * c.opacityMod,
['==', ['get', 'agency'], 'NPS'], 0.30 * opacityMod, ['==', ['get', 'agency'], 'NPS'], c.fillOpacityNPS * c.opacityMod,
['==', ['get', 'agency'], 'USFS'], 0.25 * opacityMod, ['==', ['get', 'agency'], 'USFS'], c.fillOpacityUSFS * c.opacityMod,
['==', ['get', 'agency'], 'BLM'], 0.20 * opacityMod, ['==', ['get', 'agency'], 'BLM'], c.fillOpacityBLM * c.opacityMod,
['any', ['any',
['==', ['get', 'manager_type'], 'STAT'], ['==', ['get', 'manager_type'], 'STAT'],
['==', ['get', 'agency'], 'SPR'] ['==', ['get', 'agency'], 'SPR']
], 0.25 * opacityMod, ], c.fillOpacitySTAT * c.opacityMod,
['any', ['any',
['==', ['get', 'manager_type'], 'LOC'], ['==', ['get', 'manager_type'], 'LOC'],
['==', ['get', 'manager_type'], 'DIST'] ['==', ['get', 'manager_type'], 'DIST']
], 0.20 * opacityMod, ], c.fillOpacityLOC * c.opacityMod,
0.15 * opacityMod c.fillOpacityDefault * c.opacityMod
], ],
}, },
}, beforeId) }, beforeId)
@ -464,34 +466,34 @@ function addPublicLands(map) {
paint: { paint: {
'line-color': [ 'line-color': [
'case', 'case',
['==', ['get', 'designation'], 'WA'], '#5a4d20', ['==', ['get', 'designation'], 'WA'], c.outlineWA,
['==', ['get', 'designation'], 'WSA'], '#5a4d20', ['==', ['get', 'designation'], 'WSA'], c.outlineWA,
['==', ['get', 'agency'], 'NPS'], '#2a4a15', ['==', ['get', 'agency'], 'NPS'], c.outlineNPS,
['==', ['get', 'agency'], 'USFS'], '#3d5520', ['==', ['get', 'agency'], 'USFS'], c.outlineUSFS,
['==', ['get', 'agency'], 'BLM'], '#8a7343', ['==', ['get', 'agency'], 'BLM'], c.outlineBLM,
['==', ['get', 'agency'], 'FWS'], '#2d5a3a', ['==', ['get', 'agency'], 'FWS'], c.outlineFWS,
['any', ['any',
['==', ['get', 'manager_type'], 'STAT'], ['==', ['get', 'manager_type'], 'STAT'],
['==', ['get', 'agency'], 'SPR'] ['==', ['get', 'agency'], 'SPR']
], '#3d6055', ], c.outlineSTAT,
['any', ['any',
['==', ['get', 'manager_type'], 'LOC'], ['==', ['get', 'manager_type'], 'LOC'],
['==', ['get', 'manager_type'], 'DIST'] ['==', ['get', 'manager_type'], 'DIST']
], '#5c6e66', ], c.outlineLOC,
'#707070' c.outlineDefault
], ],
'line-opacity': [ 'line-opacity': [
'case', 'case',
['==', ['get', 'agency'], 'NPS'], 0.7, ['==', ['get', 'agency'], 'NPS'], c.outlineOpacityNPS,
['==', ['get', 'agency'], 'USFS'], 0.6, ['==', ['get', 'agency'], 'USFS'], c.outlineOpacityUSFS,
['==', ['get', 'agency'], 'BLM'], 0.5, ['==', ['get', 'agency'], 'BLM'], c.outlineOpacityDefault,
0.5 c.outlineOpacityDefault
], ],
'line-width': [ 'line-width': [
'interpolate', ['linear'], ['zoom'], 'interpolate', ['linear'], ['zoom'],
4, 0.3, 4, c.outlineWidth.z4,
8, 0.8, 8, c.outlineWidth.z8,
12, 1.2 12, c.outlineWidth.z12
], ],
}, },
}, beforeId) }, beforeId)
@ -505,8 +507,8 @@ function addPublicLands(map) {
minzoom: 10, minzoom: 10,
layout: { layout: {
'text-field': ['get', 'name'], 'text-field': ['get', 'name'],
'text-size': ['interpolate', ['linear'], ['zoom'], 10, 10, 14, 13], 'text-size': ['interpolate', ['linear'], ['zoom'], 10, c.labelSize.z10, 14, c.labelSize.z14],
'text-font': ['Noto Sans Regular'], 'text-font': c.labelFont,
'symbol-placement': 'point', 'symbol-placement': 'point',
'text-anchor': 'center', 'text-anchor': 'center',
'text-max-width': 8, 'text-max-width': 8,
@ -514,10 +516,10 @@ function addPublicLands(map) {
'text-ignore-placement': false, 'text-ignore-placement': false,
}, },
paint: { paint: {
'text-color': isDark ? '#c0c8b8' : '#3a4a30', 'text-color': c.labelColor,
'text-halo-color': isDark ? '#1a1a1a' : '#ffffff', 'text-halo-color': c.labelHaloColor,
'text-halo-width': 1.5, 'text-halo-width': c.labelHaloWidth,
'text-opacity': 0.85, 'text-opacity': c.labelOpacity,
}, },
}) })
} }
@ -532,9 +534,11 @@ function removePublicLands(map) {
} }
/** Add topographic contour vector tile overlay */ /** Add topographic contour vector tile overlay */
function addContours(map) { function addContours(map, themeId) {
if (!map || map.getSource(CONTOUR_SOURCE)) return if (!map || map.getSource(CONTOUR_SOURCE)) return
const c = getOverlayConfig(themeId, 'contours')
map.addSource(CONTOUR_SOURCE, { map.addSource(CONTOUR_SOURCE, {
type: 'vector', type: 'vector',
url: 'pmtiles:///tiles/contours-na.pmtiles', url: 'pmtiles:///tiles/contours-na.pmtiles',
@ -549,9 +553,6 @@ function addContours(map) {
} }
} }
const isDark = isCurrentThemeDark()
const opMod = isDark ? 0.8 : 1.0
// Minor contours (40ft) visible z11+ // Minor contours (40ft) visible z11+
map.addLayer({ map.addLayer({
id: CONTOUR_MINOR, id: CONTOUR_MINOR,
@ -561,9 +562,9 @@ function addContours(map) {
minzoom: 11, minzoom: 11,
filter: ['==', ['get', 'tier'], 'minor'], filter: ['==', ['get', 'tier'], 'minor'],
paint: { paint: {
'line-color': '#8b6f47', 'line-color': c.minorColor,
'line-opacity': 0.4 * opMod, 'line-opacity': c.minorOpacity * c.opacityMod,
'line-width': ['interpolate', ['linear'], ['zoom'], 11, 0.5, 14, 1.0], 'line-width': ['interpolate', ['linear'], ['zoom'], 11, c.minorWidth.z11, 14, c.minorWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -576,9 +577,9 @@ function addContours(map) {
minzoom: 8, minzoom: 8,
filter: ['==', ['get', 'tier'], 'intermediate'], filter: ['==', ['get', 'tier'], 'intermediate'],
paint: { paint: {
'line-color': '#8b6f47', 'line-color': c.intermediateColor,
'line-opacity': 0.7 * opMod, 'line-opacity': c.intermediateOpacity * c.opacityMod,
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.8, 14, 1.2], 'line-width': ['interpolate', ['linear'], ['zoom'], 8, c.intermediateWidth.z8, 14, c.intermediateWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -591,9 +592,9 @@ function addContours(map) {
minzoom: 4, minzoom: 4,
filter: ['==', ['get', 'tier'], 'index'], filter: ['==', ['get', 'tier'], 'index'],
paint: { paint: {
'line-color': '#6b4f2a', 'line-color': c.indexColor,
'line-opacity': 0.9 * opMod, 'line-opacity': c.indexOpacity * c.opacityMod,
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 1.2, 14, 1.8], 'line-width': ['interpolate', ['linear'], ['zoom'], 4, c.indexWidth.z4, 14, c.indexWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -607,8 +608,8 @@ function addContours(map) {
filter: ['==', ['get', 'tier'], 'index'], filter: ['==', ['get', 'tier'], 'index'],
layout: { layout: {
'text-field': ['concat', ['to-string', ['get', 'elevation_ft']], "'"], 'text-field': ['concat', ['to-string', ['get', 'elevation_ft']], "'"],
'text-size': 10, 'text-size': c.labelSize,
'text-font': ['Noto Sans Regular'], 'text-font': c.labelFont,
'symbol-placement': 'line', 'symbol-placement': 'line',
'text-anchor': 'center', 'text-anchor': 'center',
'symbol-spacing': 400, 'symbol-spacing': 400,
@ -616,10 +617,10 @@ function addContours(map) {
'text-allow-overlap': false, 'text-allow-overlap': false,
}, },
paint: { paint: {
'text-color': isDark ? '#c0b898' : '#5a4020', 'text-color': c.labelColor,
'text-halo-color': isDark ? '#1a1a1a' : '#ffffff', 'text-halo-color': c.labelHaloColor,
'text-halo-width': 1.5, 'text-halo-width': c.labelHaloWidth,
'text-opacity': 0.85, 'text-opacity': c.labelOpacity,
}, },
}) })
} }
@ -635,9 +636,11 @@ function removeContours(map) {
} }
/** Add TEST topographic contour overlay (blue color scheme) */ /** Add TEST topographic contour overlay (blue color scheme) */
function addContoursTest(map) { function addContoursTest(map, themeId) {
if (!map || map.getSource(CONTOUR_TEST_SOURCE)) return if (!map || map.getSource(CONTOUR_TEST_SOURCE)) return
const c = getOverlayConfig(themeId, 'contoursTest')
map.addSource(CONTOUR_TEST_SOURCE, { map.addSource(CONTOUR_TEST_SOURCE, {
type: "vector", type: "vector",
url: "pmtiles:///tiles/contours-test.pmtiles", url: "pmtiles:///tiles/contours-test.pmtiles",
@ -651,9 +654,6 @@ function addContoursTest(map) {
} }
} }
const isDark = isCurrentThemeDark()
const opMod = isDark ? 0.8 : 1.0
// Minor contours (40ft) blue scheme // Minor contours (40ft) blue scheme
map.addLayer({ map.addLayer({
id: CONTOUR_TEST_MINOR, id: CONTOUR_TEST_MINOR,
@ -663,9 +663,9 @@ function addContoursTest(map) {
minzoom: 11, minzoom: 11,
filter: ["==", ["get", "tier"], "minor"], filter: ["==", ["get", "tier"], "minor"],
paint: { paint: {
"line-color": "#4a7c9b", "line-color": c.minorColor,
"line-opacity": 0.4 * opMod, "line-opacity": c.minorOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 11, 0.5, 14, 1.0], "line-width": ["interpolate", ["linear"], ["zoom"], 11, c.minorWidth.z11, 14, c.minorWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -678,9 +678,9 @@ function addContoursTest(map) {
minzoom: 8, minzoom: 8,
filter: ["==", ["get", "tier"], "intermediate"], filter: ["==", ["get", "tier"], "intermediate"],
paint: { paint: {
"line-color": "#4a7c9b", "line-color": c.intermediateColor,
"line-opacity": 0.7 * opMod, "line-opacity": c.intermediateOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.8, 14, 1.2], "line-width": ["interpolate", ["linear"], ["zoom"], 8, c.intermediateWidth.z8, 14, c.intermediateWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -693,9 +693,9 @@ function addContoursTest(map) {
minzoom: 4, minzoom: 4,
filter: ["==", ["get", "tier"], "index"], filter: ["==", ["get", "tier"], "index"],
paint: { paint: {
"line-color": "#2a5a7c", "line-color": c.indexColor,
"line-opacity": 0.9 * opMod, "line-opacity": c.indexOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 1.2, 14, 1.8], "line-width": ["interpolate", ["linear"], ["zoom"], 4, c.indexWidth.z4, 14, c.indexWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -709,8 +709,8 @@ function addContoursTest(map) {
filter: ["==", ["get", "tier"], "index"], filter: ["==", ["get", "tier"], "index"],
layout: { layout: {
"text-field": ["concat", ["to-string", ["get", "elevation_ft"]], ""], "text-field": ["concat", ["to-string", ["get", "elevation_ft"]], ""],
"text-size": 10, "text-size": c.labelSize,
"text-font": ["Noto Sans Regular"], "text-font": c.labelFont,
"symbol-placement": "line", "symbol-placement": "line",
"text-anchor": "center", "text-anchor": "center",
"symbol-spacing": 400, "symbol-spacing": 400,
@ -718,10 +718,10 @@ function addContoursTest(map) {
"text-allow-overlap": false, "text-allow-overlap": false,
}, },
paint: { paint: {
"text-color": isDark ? "#98b8d0" : "#205080", "text-color": c.labelColor,
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff", "text-halo-color": c.labelHaloColor,
"text-halo-width": 1.5, "text-halo-width": c.labelHaloWidth,
"text-opacity": 0.85, "text-opacity": c.labelOpacity,
}, },
}) })
} }
@ -737,9 +737,11 @@ function removeContoursTest(map) {
} }
/** Add TEST 10ft topographic contour overlay (green color scheme) */ /** Add TEST 10ft topographic contour overlay (green color scheme) */
function addContoursTest10ft(map) { function addContoursTest10ft(map, themeId) {
if (!map || map.getSource(CONTOUR_TEST_10FT_SOURCE)) return if (!map || map.getSource(CONTOUR_TEST_10FT_SOURCE)) return
const c = getOverlayConfig(themeId, 'contoursTest10ft')
map.addSource(CONTOUR_TEST_10FT_SOURCE, { map.addSource(CONTOUR_TEST_10FT_SOURCE, {
type: "vector", type: "vector",
url: "pmtiles:///tiles/contours-test-10ft.pmtiles", url: "pmtiles:///tiles/contours-test-10ft.pmtiles",
@ -753,9 +755,6 @@ function addContoursTest10ft(map) {
} }
} }
const isDark = isCurrentThemeDark()
const opMod = isDark ? 0.8 : 1.0
// Minor contours (10ft) green scheme // Minor contours (10ft) green scheme
map.addLayer({ map.addLayer({
id: CONTOUR_TEST_10FT_MINOR, id: CONTOUR_TEST_10FT_MINOR,
@ -765,9 +764,9 @@ function addContoursTest10ft(map) {
minzoom: 11, minzoom: 11,
filter: ["==", ["get", "tier"], "minor"], filter: ["==", ["get", "tier"], "minor"],
paint: { paint: {
"line-color": "#3a7c4f", "line-color": c.minorColor,
"line-opacity": 0.4 * opMod, "line-opacity": c.minorOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 11, 0.5, 14, 1.0], "line-width": ["interpolate", ["linear"], ["zoom"], 11, c.minorWidth.z11, 14, c.minorWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -780,9 +779,9 @@ function addContoursTest10ft(map) {
minzoom: 8, minzoom: 8,
filter: ["==", ["get", "tier"], "intermediate"], filter: ["==", ["get", "tier"], "intermediate"],
paint: { paint: {
"line-color": "#3a7c4f", "line-color": c.intermediateColor,
"line-opacity": 0.7 * opMod, "line-opacity": c.intermediateOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 8, 0.8, 14, 1.2], "line-width": ["interpolate", ["linear"], ["zoom"], 8, c.intermediateWidth.z8, 14, c.intermediateWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -795,9 +794,9 @@ function addContoursTest10ft(map) {
minzoom: 4, minzoom: 4,
filter: ["==", ["get", "tier"], "index"], filter: ["==", ["get", "tier"], "index"],
paint: { paint: {
"line-color": "#2a5c3a", "line-color": c.indexColor,
"line-opacity": 0.9 * opMod, "line-opacity": c.indexOpacity * c.opacityMod,
"line-width": ["interpolate", ["linear"], ["zoom"], 4, 1.2, 14, 1.8], "line-width": ["interpolate", ["linear"], ["zoom"], 4, c.indexWidth.z4, 14, c.indexWidth.z14],
}, },
}, beforeId) }, beforeId)
@ -811,8 +810,8 @@ function addContoursTest10ft(map) {
filter: ["==", ["get", "tier"], "index"], filter: ["==", ["get", "tier"], "index"],
layout: { layout: {
"text-field": ["concat", ["to-string", ["get", "elevation_ft"]], "'"], "text-field": ["concat", ["to-string", ["get", "elevation_ft"]], "'"],
"text-size": 10, "text-size": c.labelSize,
"text-font": ["Noto Sans Regular"], "text-font": c.labelFont,
"symbol-placement": "line", "symbol-placement": "line",
"text-anchor": "center", "text-anchor": "center",
"symbol-spacing": 400, "symbol-spacing": 400,
@ -820,10 +819,10 @@ function addContoursTest10ft(map) {
"text-allow-overlap": false, "text-allow-overlap": false,
}, },
paint: { paint: {
"text-color": isDark ? "#98c0a8" : "#2a4030", "text-color": c.labelColor,
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff", "text-halo-color": c.labelHaloColor,
"text-halo-width": 1.5, "text-halo-width": c.labelHaloWidth,
"text-opacity": 0.85, "text-opacity": c.labelOpacity,
}, },
}) })
} }
@ -838,10 +837,11 @@ function removeContoursTest10ft(map) {
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 */ /** Add USFS trails and roads vector tile overlay */
/** Add USFS trails and roads vector tile overlay */ function addUsfsTrails(map, themeId) {
function addUsfsTrails(map) {
if (!map || map.getSource(USFS_SOURCE)) return if (!map || map.getSource(USFS_SOURCE)) return
const c = getOverlayConfig(themeId, 'usfsTrails')
map.addSource(USFS_SOURCE, { map.addSource(USFS_SOURCE, {
type: "vector", type: "vector",
url: "pmtiles:///tiles/usfs-trails-roads.pmtiles", url: "pmtiles:///tiles/usfs-trails-roads.pmtiles",
@ -856,8 +856,6 @@ function addUsfsTrails(map) {
} }
} }
const isDark = isCurrentThemeDark()
// Invisible hit-area layers for easier clicking // Invisible hit-area layers for easier clicking
map.addLayer({ map.addLayer({
id: USFS_ROADS_HIT, id: USFS_ROADS_HIT,
@ -868,7 +866,7 @@ function addUsfsTrails(map) {
paint: { paint: {
"line-color": "#000000", "line-color": "#000000",
"line-opacity": 0, "line-opacity": 0,
"line-width": 14, "line-width": c.hitWidth,
}, },
}, beforeId) }, beforeId)
@ -881,7 +879,7 @@ function addUsfsTrails(map) {
paint: { paint: {
"line-color": "#000000", "line-color": "#000000",
"line-opacity": 0, "line-opacity": 0,
"line-width": 14, "line-width": c.hitWidth,
}, },
}, beforeId) }, beforeId)
@ -893,9 +891,9 @@ function addUsfsTrails(map) {
"source-layer": "roads", "source-layer": "roads",
minzoom: 10, minzoom: 10,
paint: { paint: {
"line-color": isDark ? "#d0a060" : "#c09050", "line-color": c.roadsColor,
"line-opacity": 0.9, "line-opacity": c.roadsOpacity,
"line-width": ["interpolate", ["linear"], ["zoom"], 10, 1.5, 14, 2.5, 16, 3.5], "line-width": ["interpolate", ["linear"], ["zoom"], 10, c.roadsWidth.z10, 14, c.roadsWidth.z14, 16, c.roadsWidth.z16],
}, },
}, beforeId) }, beforeId)
@ -913,21 +911,21 @@ function addUsfsTrails(map) {
["any", ["any",
["==", ["slice", ["get", "MOTORCYCLE"], 0, 1], "0"], ["==", ["slice", ["get", "MOTORCYCLE"], 0, 1], "0"],
["==", ["slice", ["get", "ATV_MANAGE"], 0, 1], "0"] ["==", ["slice", ["get", "ATV_MANAGE"], 0, 1], "0"]
], isDark ? "#f08040" : "#e07030", ], c.trailsMotorized,
// Bike trails - amber // Bike trails - amber
["==", ["slice", ["get", "BICYCLE_MA"], 0, 1], "0"], ["==", ["slice", ["get", "BICYCLE_MA"], 0, 1], "0"],
isDark ? "#e0b040" : "#d0a030", c.trailsBicycle,
// Hiker/Horse only - green // Hiker/Horse only - green
["any", ["any",
["==", ["slice", ["get", "HIKER_PEDE"], 0, 1], "0"], ["==", ["slice", ["get", "HIKER_PEDE"], 0, 1], "0"],
["==", ["slice", ["get", "HORSE_MANA"], 0, 1], "0"] ["==", ["slice", ["get", "HORSE_MANA"], 0, 1], "0"]
], isDark ? "#60c050" : "#50b040", ], c.trailsHiker,
// Default - tan // Default - tan
isDark ? "#c0a060" : "#b09050" c.trailsDefault
], ],
"line-opacity": 0.9, "line-opacity": c.trailsOpacity,
"line-width": ["interpolate", ["linear"], ["zoom"], 10, 2.0, 14, 3.0, 16, 4.0], "line-width": ["interpolate", ["linear"], ["zoom"], 10, c.trailsWidth.z10, 14, c.trailsWidth.z14, 16, c.trailsWidth.z16],
"line-dasharray": [2, 1.5], "line-dasharray": c.trailsDash,
}, },
}, beforeId) }, beforeId)
@ -941,8 +939,8 @@ function addUsfsTrails(map) {
filter: ["has", "NAME"], filter: ["has", "NAME"],
layout: { layout: {
"text-field": ["get", "NAME"], "text-field": ["get", "NAME"],
"text-size": 11, "text-size": c.roadsLabelSize,
"text-font": ["Noto Sans Regular"], "text-font": c.labelFont,
"symbol-placement": "line", "symbol-placement": "line",
"text-anchor": "center", "text-anchor": "center",
"symbol-spacing": 300, "symbol-spacing": 300,
@ -950,10 +948,10 @@ function addUsfsTrails(map) {
"text-allow-overlap": false, "text-allow-overlap": false,
}, },
paint: { paint: {
"text-color": isDark ? "#d0c0a0" : "#6a5a40", "text-color": c.roadsLabelColor,
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff", "text-halo-color": c.roadsLabelHaloColor,
"text-halo-width": 1.5, "text-halo-width": c.roadsLabelHaloWidth,
"text-opacity": 0.9, "text-opacity": c.roadsLabelOpacity,
}, },
}) })
@ -967,8 +965,8 @@ function addUsfsTrails(map) {
filter: ["has", "TRAIL_NAME"], filter: ["has", "TRAIL_NAME"],
layout: { layout: {
"text-field": ["get", "TRAIL_NAME"], "text-field": ["get", "TRAIL_NAME"],
"text-size": 11, "text-size": c.trailsLabelSize,
"text-font": ["Noto Sans Regular"], "text-font": c.labelFont,
"symbol-placement": "line", "symbol-placement": "line",
"text-anchor": "center", "text-anchor": "center",
"symbol-spacing": 300, "symbol-spacing": 300,
@ -976,10 +974,10 @@ function addUsfsTrails(map) {
"text-allow-overlap": false, "text-allow-overlap": false,
}, },
paint: { paint: {
"text-color": isDark ? "#d0b090" : "#5a4a30", "text-color": c.trailsLabelColor,
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff", "text-halo-color": c.trailsLabelHaloColor,
"text-halo-width": 1.5, "text-halo-width": c.trailsLabelHaloWidth,
"text-opacity": 0.9, "text-opacity": c.trailsLabelOpacity,
}, },
}) })
@ -1003,12 +1001,12 @@ function removeUsfsTrails(map) {
if (map.getLayer(USFS_ROADS_HIT)) map.removeLayer(USFS_ROADS_HIT) if (map.getLayer(USFS_ROADS_HIT)) map.removeLayer(USFS_ROADS_HIT)
if (map.getSource(USFS_SOURCE)) map.removeSource(USFS_SOURCE) if (map.getSource(USFS_SOURCE)) map.removeSource(USFS_SOURCE)
} }
/** Add BLM trails/roads vector tile overlay */
/** Add BLM trails/roads vector tile overlay with surface-type styling */ /** Add BLM trails/roads vector tile overlay with surface-type styling */
/** Add BLM trails/roads vector tile overlay with surface-type styling */ function addBlmTrails(map, themeId) {
function addBlmTrails(map) {
if (!map || map.getSource(BLM_SOURCE)) return if (!map || map.getSource(BLM_SOURCE)) return
const c = getOverlayConfig(themeId, 'blmTrails')
map.addSource(BLM_SOURCE, { map.addSource(BLM_SOURCE, {
type: "vector", type: "vector",
url: "pmtiles:///tiles/blm-trails-roads.pmtiles", url: "pmtiles:///tiles/blm-trails-roads.pmtiles",
@ -1023,40 +1021,38 @@ function addBlmTrails(map) {
} }
} }
const isDark = isCurrentThemeDark() // Color expression based on route use class
// Color expression based on route use class - brighter palette
const colorExpr = [ const colorExpr = [
"case", "case",
["any", ["any",
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD HIGH CLEARANCE / SPECIALIZED"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD HIGH CLEARANCE / SPECIALIZED"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD High Clearance/Specialized"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD High Clearance/Specialized"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4wd High Clearance / Specialized"] ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4wd High Clearance / Specialized"]
], isDark ? "#f08040" : "#e07030", ], c.color4wdHigh,
["any", ["any",
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD LOW"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD LOW"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD Low"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4WD Low"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4wd Low"] ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "4wd Low"]
], isDark ? "#e0b040" : "#d0a030", ], c.color4wdLow,
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "ATV"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "ATV"],
isDark ? "#e04040" : "#d03030", c.colorAtv,
["any", ["any",
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "MOTORIZED SINGLE TRACK"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "MOTORIZED SINGLE TRACK"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "Motorized Single Track"] ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "Motorized Single Track"]
], isDark ? "#b070c0" : "#a060b0", ], c.colorMotoSingle,
["any", ["any",
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2WD LOW"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2WD LOW"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2WD Low"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2WD Low"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2wd Low"] ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "2wd Low"]
], isDark ? "#f0d070" : "#e0c060", ], c.color2wdLow,
["any", ["any",
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "NON-MECHANIZED"], ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "NON-MECHANIZED"],
["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "Non-Mechanized"] ["==", ["get", "OBSRVE_ROUTE_USE_CLASS"], "Non-Mechanized"]
], isDark ? "#60c050" : "#50b040", ], c.colorNonMech,
isDark ? "#c0a060" : "#b09050" c.colorDefault
] ]
const lineWidth = ["interpolate", ["linear"], ["zoom"], 10, 2.0, 14, 3.0, 16, 4.0] const lineWidth = ["interpolate", ["linear"], ["zoom"], 10, c.lineWidth.z10, 14, c.lineWidth.z14, 16, c.lineWidth.z16]
// Filter out paved, arterial, collector, local, and highways // Filter out paved, arterial, collector, local, and highways
const excludeUrban = [ const excludeUrban = [
@ -1088,7 +1084,7 @@ function addBlmTrails(map) {
paint: { paint: {
"line-color": "#000000", "line-color": "#000000",
"line-opacity": 0, "line-opacity": 0,
"line-width": 14, "line-width": c.hitWidth,
}, },
}, beforeId) }, beforeId)
@ -1107,7 +1103,7 @@ function addBlmTrails(map) {
], ],
paint: { paint: {
"line-color": colorExpr, "line-color": colorExpr,
"line-opacity": 0.9, "line-opacity": c.lineOpacity,
"line-width": lineWidth, "line-width": lineWidth,
}, },
}, beforeId) }, beforeId)
@ -1127,9 +1123,9 @@ function addBlmTrails(map) {
], ],
paint: { paint: {
"line-color": colorExpr, "line-color": colorExpr,
"line-opacity": 0.9, "line-opacity": c.lineOpacity,
"line-width": lineWidth, "line-width": lineWidth,
"line-dasharray": [4, 2], "line-dasharray": c.dashImproved,
}, },
}, beforeId) }, beforeId)
@ -1148,9 +1144,9 @@ function addBlmTrails(map) {
], ],
paint: { paint: {
"line-color": colorExpr, "line-color": colorExpr,
"line-opacity": 0.9, "line-opacity": c.lineOpacity,
"line-width": lineWidth, "line-width": lineWidth,
"line-dasharray": [1, 2], "line-dasharray": c.dashAggregate,
}, },
}, beforeId) }, beforeId)
@ -1168,10 +1164,10 @@ function addBlmTrails(map) {
] ]
], ],
paint: { paint: {
"line-color": isDark ? "#80b0e0" : "#6090c0", "line-color": c.colorSnow,
"line-opacity": 0.9, "line-opacity": c.lineOpacity,
"line-width": lineWidth, "line-width": lineWidth,
"line-dasharray": [4, 2, 1, 2], "line-dasharray": c.dashSnow,
}, },
}, beforeId) }, beforeId)
@ -1196,9 +1192,9 @@ function addBlmTrails(map) {
], ],
paint: { paint: {
"line-color": colorExpr, "line-color": colorExpr,
"line-opacity": 0.85, "line-opacity": c.lineOpacityOther,
"line-width": lineWidth, "line-width": lineWidth,
"line-dasharray": [4, 2, 1, 2, 1, 2], "line-dasharray": c.dashOther,
}, },
}, beforeId) }, beforeId)
@ -1212,8 +1208,8 @@ function addBlmTrails(map) {
filter: ["all", excludeUrban, ["has", "ROUTE_PRMRY_NM"]], filter: ["all", excludeUrban, ["has", "ROUTE_PRMRY_NM"]],
layout: { layout: {
"text-field": ["get", "ROUTE_PRMRY_NM"], "text-field": ["get", "ROUTE_PRMRY_NM"],
"text-size": 11, "text-size": c.labelSize,
"text-font": ["Noto Sans Regular"], "text-font": c.labelFont,
"symbol-placement": "line", "symbol-placement": "line",
"text-anchor": "center", "text-anchor": "center",
"symbol-spacing": 300, "symbol-spacing": 300,
@ -1221,10 +1217,10 @@ function addBlmTrails(map) {
"text-allow-overlap": false, "text-allow-overlap": false,
}, },
paint: { paint: {
"text-color": isDark ? "#d0c0a0" : "#5a4a30", "text-color": c.labelColor,
"text-halo-color": isDark ? "#1a1a1a" : "#ffffff", "text-halo-color": c.labelHaloColor,
"text-halo-width": 1.5, "text-halo-width": c.labelHaloWidth,
"text-opacity": 0.9, "text-opacity": c.labelOpacity,
}, },
}) })
@ -1236,6 +1232,7 @@ function addBlmTrails(map) {
map.getCanvas().style.cursor = "" map.getCanvas().style.cursor = ""
}) })
} }
/** Remove BLM trails/roads layers and source */ /** Remove BLM trails/roads layers and source */
function removeBlmTrails(map) { function removeBlmTrails(map) {
if (!map) return if (!map) return
@ -1685,7 +1682,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addHillshadeLayer() { addHillshadeLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addHillshade(map) addHillshade(map, currentThemeRef.current)
activeLayersRef.current.hillshade = true activeLayersRef.current.hillshade = true
}, },
removeHillshadeLayer() { removeHillshadeLayer() {
@ -1697,7 +1694,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addTrafficLayer() { addTrafficLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addTraffic(map) addTraffic(map, currentThemeRef.current)
activeLayersRef.current.traffic = true activeLayersRef.current.traffic = true
}, },
removeTrafficLayer() { removeTrafficLayer() {
@ -1709,7 +1706,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addPublicLandsLayer() { addPublicLandsLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addPublicLands(map) addPublicLands(map, currentThemeRef.current)
activeLayersRef.current.publicLands = true activeLayersRef.current.publicLands = true
}, },
removePublicLandsLayer() { removePublicLandsLayer() {
@ -1721,7 +1718,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addContoursLayer() { addContoursLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addContours(map) addContours(map, currentThemeRef.current)
activeLayersRef.current.contours = true activeLayersRef.current.contours = true
}, },
removeContoursLayer() { removeContoursLayer() {
@ -1733,7 +1730,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addContoursTestLayer() { addContoursTestLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addContoursTest(map) addContoursTest(map, currentThemeRef.current)
activeLayersRef.current.contoursTest = true activeLayersRef.current.contoursTest = true
}, },
removeContoursTestLayer() { removeContoursTestLayer() {
@ -1745,7 +1742,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addContoursTest10ftLayer() { addContoursTest10ftLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addContoursTest10ft(map) addContoursTest10ft(map, currentThemeRef.current)
activeLayersRef.current.contoursTest10ft = true activeLayersRef.current.contoursTest10ft = true
}, },
removeContoursTest10ftLayer() { removeContoursTest10ftLayer() {
@ -1757,7 +1754,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addUsfsTrailsLayer() { addUsfsTrailsLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addUsfsTrails(map) addUsfsTrails(map, currentThemeRef.current)
activeLayersRef.current.usfsTrails = true activeLayersRef.current.usfsTrails = true
}, },
removeUsfsTrailsLayer() { removeUsfsTrailsLayer() {
@ -1769,7 +1766,7 @@ const MapView = forwardRef(function MapView(_, ref) {
addBlmTrailsLayer() { addBlmTrailsLayer() {
const map = mapInstance.current const map = mapInstance.current
if (!map) return if (!map) return
addBlmTrails(map) addBlmTrails(map, currentThemeRef.current)
activeLayersRef.current.blmTrails = true activeLayersRef.current.blmTrails = true
}, },
removeBlmTrailsLayer() { removeBlmTrailsLayer() {
@ -2139,24 +2136,24 @@ const MapView = forwardRef(function MapView(_, ref) {
if (raw) { if (raw) {
const prefs = JSON.parse(raw) const prefs = JSON.parse(raw)
if (prefs.hillshade && hasFeature('has_hillshade')) { if (prefs.hillshade && hasFeature('has_hillshade')) {
addHillshade(map) addHillshade(map, currentThemeRef.current)
activeLayersRef.current.hillshade = true activeLayersRef.current.hillshade = true
} }
if (prefs.traffic && hasFeature('has_traffic_overlay')) { if (prefs.traffic && hasFeature('has_traffic_overlay')) {
addTraffic(map) addTraffic(map, currentThemeRef.current)
activeLayersRef.current.traffic = true activeLayersRef.current.traffic = true
} }
if (prefs.publicLands && hasFeature('has_public_lands_layer')) { if (prefs.publicLands && hasFeature('has_public_lands_layer')) {
addPublicLands(map) addPublicLands(map, currentThemeRef.current)
activeLayersRef.current.publicLands = true activeLayersRef.current.publicLands = true
} }
if (prefs.contours && hasFeature('has_contours')) { if (prefs.contours && hasFeature('has_contours')) {
addContours(map) addContours(map, currentThemeRef.current)
activeLayersRef.current.contours = true activeLayersRef.current.contours = true
} }
} else if (hasFeature('has_hillshade')) { } else if (hasFeature('has_hillshade')) {
// Default: hillshade ON if available // Default: hillshade ON if available
addHillshade(map) addHillshade(map, currentThemeRef.current)
activeLayersRef.current.hillshade = true activeLayersRef.current.hillshade = true
} }
} catch {} } catch {}
@ -2343,10 +2340,14 @@ const MapView = forwardRef(function MapView(_, ref) {
applyBaseLabelStyling(map) applyBaseLabelStyling(map)
// Re-add active overlay layers // Re-add active overlay layers
if (activeLayersRef.current.hillshade) addHillshade(map) if (activeLayersRef.current.hillshade) addHillshade(map, currentThemeRef.current)
if (activeLayersRef.current.traffic) addTraffic(map) if (activeLayersRef.current.traffic) addTraffic(map, currentThemeRef.current)
if (activeLayersRef.current.publicLands) addPublicLands(map) if (activeLayersRef.current.publicLands) addPublicLands(map, currentThemeRef.current)
if (activeLayersRef.current.contours) addContours(map) if (activeLayersRef.current.contours) addContours(map, currentThemeRef.current)
if (activeLayersRef.current.contoursTest) addContoursTest(map, currentThemeRef.current)
if (activeLayersRef.current.contoursTest10ft) addContoursTest10ft(map, currentThemeRef.current)
if (activeLayersRef.current.usfsTrails) addUsfsTrails(map, currentThemeRef.current)
if (activeLayersRef.current.blmTrails) addBlmTrails(map, currentThemeRef.current)
// Clear highlights on theme change (paint values will be re-stored on next interaction) // Clear highlights on theme change (paint values will be re-stored on next interaction)
clearAllHighlights(map) clearAllHighlights(map)

View file

@ -10,11 +10,343 @@
* dark: boolean - true if dark theme (affects overlay styling, sprite fallback) * dark: boolean - true if dark theme (affects overlay styling, sprite fallback)
* colors: object|null - null for built-in themes, full flavor object for custom * colors: object|null - null for built-in themes, full flavor object for custom
* satellite: object|null - raster adjustments when satellite layer is present * satellite: object|null - raster adjustments when satellite layer is present
* overlay: object|null - reserved for future overlay-specific customizations * overlay: object - overlay layer styling configuration
*/ */
import { namedTheme } from 'protomaps-themes-base' import { namedTheme } from 'protomaps-themes-base'
// ═══════════════════════════════════════════════════════════════════════════
// OVERLAY CONFIGURATIONS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Dark theme overlay configuration
* All hardcoded values from overlay add functions extracted here
*/
const darkOverlay = {
// ── Hillshade ─────────────────────────────────────────────────────────────
hillshade: {
exaggeration: 0.5,
illuminationDirection: 315,
shadowColor: '#000000',
highlightColor: '#ffffff',
},
// ── Traffic ───────────────────────────────────────────────────────────────
traffic: {
opacity: 0.6,
},
// ── Contours (main, brown/tan scheme) ─────────────────────────────────────
contours: {
opacityMod: 0.8,
minorColor: '#8b6f47',
minorOpacity: 0.4,
minorWidth: { z11: 0.5, z14: 1.0 },
intermediateColor: '#8b6f47',
intermediateOpacity: 0.7,
intermediateWidth: { z8: 0.8, z14: 1.2 },
indexColor: '#6b4f2a',
indexOpacity: 0.9,
indexWidth: { z4: 1.2, z14: 1.8 },
labelColor: '#c0b898',
labelHaloColor: '#1a1a1a',
labelHaloWidth: 1.5,
labelOpacity: 0.85,
labelSize: 10,
labelFont: ['Noto Sans Regular'],
},
// ── Contours Test (blue scheme) ───────────────────────────────────────────
// Missing keys cascade from contours
contoursTest: {
minorColor: '#4a7c9b',
intermediateColor: '#4a7c9b',
indexColor: '#2a5a7c',
labelColor: '#98b8d0',
},
// ── Contours Test 10ft (green scheme) ─────────────────────────────────────
// Missing keys cascade from contours
contoursTest10ft: {
minorColor: '#3a7c4f',
intermediateColor: '#3a7c4f',
indexColor: '#2a5c3a',
labelColor: '#98c0a8',
},
// ── Public Lands (PAD-US) ─────────────────────────────────────────────────
publicLands: {
opacityMod: 0.7,
// Fill colors per category
fillWA: '#7c6b2f',
fillNPS: '#3d6b1f',
fillUSFS: '#5a7c2f',
fillBLM: '#c4a672',
fillFWS: '#4a7a5a',
fillSTAT: '#5a8c7c',
fillLOC: '#8ca694',
fillDefault: '#a0a0a0',
// Fill base opacities (multiplied by opacityMod)
fillOpacityWA: 0.30,
fillOpacityNPS: 0.30,
fillOpacityUSFS: 0.25,
fillOpacityBLM: 0.20,
fillOpacitySTAT: 0.25,
fillOpacityLOC: 0.20,
fillOpacityDefault: 0.15,
// Outline colors per category
outlineWA: '#5a4d20',
outlineNPS: '#2a4a15',
outlineUSFS: '#3d5520',
outlineBLM: '#8a7343',
outlineFWS: '#2d5a3a',
outlineSTAT: '#3d6055',
outlineLOC: '#5c6e66',
outlineDefault: '#707070',
// Outline opacities
outlineOpacityNPS: 0.7,
outlineOpacityUSFS: 0.6,
outlineOpacityDefault: 0.5,
// Outline width
outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
// Labels
labelColor: '#c0c8b8',
labelHaloColor: '#1a1a1a',
labelHaloWidth: 1.5,
labelOpacity: 0.85,
labelSize: { z10: 10, z14: 13 },
labelFont: ['Noto Sans Regular'],
},
// ── USFS Trails ───────────────────────────────────────────────────────────
usfsTrails: {
// Roads
roadsColor: '#d0a060',
roadsOpacity: 0.9,
roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
// Trails colors by use type
trailsMotorized: '#f08040',
trailsBicycle: '#e0b040',
trailsHiker: '#60c050',
trailsDefault: '#c0a060',
trailsOpacity: 0.9,
trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
trailsDash: [2, 1.5],
// Road labels
roadsLabelColor: '#d0c0a0',
roadsLabelHaloColor: '#1a1a1a',
roadsLabelHaloWidth: 1.5,
roadsLabelOpacity: 0.9,
roadsLabelSize: 11,
// Trail labels
trailsLabelColor: '#d0b090',
trailsLabelHaloColor: '#1a1a1a',
trailsLabelHaloWidth: 1.5,
trailsLabelOpacity: 0.9,
trailsLabelSize: 11,
labelFont: ['Noto Sans Regular'],
// Hit layer
hitWidth: 14,
},
// ── BLM Trails / Roads ────────────────────────────────────────────────────
blmTrails: {
// Route colors by use class
color4wdHigh: '#f08040',
color4wdLow: '#e0b040',
colorAtv: '#e04040',
colorMotoSingle: '#b070c0',
color2wdLow: '#f0d070',
colorNonMech: '#60c050',
colorDefault: '#c0a060',
colorSnow: '#80b0e0',
lineOpacity: 0.9,
lineOpacityOther: 0.85,
lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
// Dash patterns by surface type
dashImproved: [4, 2],
dashAggregate: [1, 2],
dashSnow: [4, 2, 1, 2],
dashOther: [4, 2, 1, 2, 1, 2],
// Labels
labelColor: '#d0c0a0',
labelHaloColor: '#1a1a1a',
labelHaloWidth: 1.5,
labelOpacity: 0.9,
labelSize: 11,
labelFont: ['Noto Sans Regular'],
// Hit layer
hitWidth: 14,
},
}
/**
* Light theme overlay configuration
* All hardcoded values from overlay add functions extracted here
*/
const lightOverlay = {
// ── Hillshade ─────────────────────────────────────────────────────────────
hillshade: {
exaggeration: 0.5,
illuminationDirection: 315,
shadowColor: '#000000',
highlightColor: '#ffffff',
},
// ── Traffic ───────────────────────────────────────────────────────────────
traffic: {
opacity: 0.6,
},
// ── Contours (main, brown/tan scheme) ─────────────────────────────────────
contours: {
opacityMod: 1.0,
minorColor: '#8b6f47',
minorOpacity: 0.4,
minorWidth: { z11: 0.5, z14: 1.0 },
intermediateColor: '#8b6f47',
intermediateOpacity: 0.7,
intermediateWidth: { z8: 0.8, z14: 1.2 },
indexColor: '#6b4f2a',
indexOpacity: 0.9,
indexWidth: { z4: 1.2, z14: 1.8 },
labelColor: '#5a4020',
labelHaloColor: '#ffffff',
labelHaloWidth: 1.5,
labelOpacity: 0.85,
labelSize: 10,
labelFont: ['Noto Sans Regular'],
},
// ── Contours Test (blue scheme) ───────────────────────────────────────────
// Missing keys cascade from contours
contoursTest: {
minorColor: '#4a7c9b',
intermediateColor: '#4a7c9b',
indexColor: '#2a5a7c',
labelColor: '#205080',
},
// ── Contours Test 10ft (green scheme) ─────────────────────────────────────
// Missing keys cascade from contours
contoursTest10ft: {
minorColor: '#3a7c4f',
intermediateColor: '#3a7c4f',
indexColor: '#2a5c3a',
labelColor: '#2a4030',
},
// ── Public Lands (PAD-US) ─────────────────────────────────────────────────
publicLands: {
opacityMod: 1.0,
// Fill colors per category
fillWA: '#7c6b2f',
fillNPS: '#3d6b1f',
fillUSFS: '#5a7c2f',
fillBLM: '#c4a672',
fillFWS: '#4a7a5a',
fillSTAT: '#5a8c7c',
fillLOC: '#8ca694',
fillDefault: '#a0a0a0',
// Fill base opacities (multiplied by opacityMod)
fillOpacityWA: 0.30,
fillOpacityNPS: 0.30,
fillOpacityUSFS: 0.25,
fillOpacityBLM: 0.20,
fillOpacitySTAT: 0.25,
fillOpacityLOC: 0.20,
fillOpacityDefault: 0.15,
// Outline colors per category
outlineWA: '#5a4d20',
outlineNPS: '#2a4a15',
outlineUSFS: '#3d5520',
outlineBLM: '#8a7343',
outlineFWS: '#2d5a3a',
outlineSTAT: '#3d6055',
outlineLOC: '#5c6e66',
outlineDefault: '#707070',
// Outline opacities
outlineOpacityNPS: 0.7,
outlineOpacityUSFS: 0.6,
outlineOpacityDefault: 0.5,
// Outline width
outlineWidth: { z4: 0.3, z8: 0.8, z12: 1.2 },
// Labels
labelColor: '#3a4a30',
labelHaloColor: '#ffffff',
labelHaloWidth: 1.5,
labelOpacity: 0.85,
labelSize: { z10: 10, z14: 13 },
labelFont: ['Noto Sans Regular'],
},
// ── USFS Trails ───────────────────────────────────────────────────────────
usfsTrails: {
// Roads
roadsColor: '#c09050',
roadsOpacity: 0.9,
roadsWidth: { z10: 1.5, z14: 2.5, z16: 3.5 },
// Trails colors by use type
trailsMotorized: '#e07030',
trailsBicycle: '#d0a030',
trailsHiker: '#50b040',
trailsDefault: '#b09050',
trailsOpacity: 0.9,
trailsWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
trailsDash: [2, 1.5],
// Road labels
roadsLabelColor: '#6a5a40',
roadsLabelHaloColor: '#ffffff',
roadsLabelHaloWidth: 1.5,
roadsLabelOpacity: 0.9,
roadsLabelSize: 11,
// Trail labels
trailsLabelColor: '#5a4a30',
trailsLabelHaloColor: '#ffffff',
trailsLabelHaloWidth: 1.5,
trailsLabelOpacity: 0.9,
trailsLabelSize: 11,
labelFont: ['Noto Sans Regular'],
// Hit layer
hitWidth: 14,
},
// ── BLM Trails / Roads ────────────────────────────────────────────────────
blmTrails: {
// Route colors by use class
color4wdHigh: '#e07030',
color4wdLow: '#d0a030',
colorAtv: '#d03030',
colorMotoSingle: '#a060b0',
color2wdLow: '#e0c060',
colorNonMech: '#50b040',
colorDefault: '#b09050',
colorSnow: '#6090c0',
lineOpacity: 0.9,
lineOpacityOther: 0.85,
lineWidth: { z10: 2.0, z14: 3.0, z16: 4.0 },
// Dash patterns by surface type
dashImproved: [4, 2],
dashAggregate: [1, 2],
dashSnow: [4, 2, 1, 2],
dashOther: [4, 2, 1, 2, 1, 2],
// Labels
labelColor: '#5a4a30',
labelHaloColor: '#ffffff',
labelHaloWidth: 1.5,
labelOpacity: 0.9,
labelSize: 11,
labelFont: ['Noto Sans Regular'],
// Hit layer
hitWidth: 14,
},
}
// ═══════════════════════════════════════════════════════════════════════════
// THEME REGISTRY
// ═══════════════════════════════════════════════════════════════════════════
/** /**
* Theme registry - maps theme IDs to theme configurations * Theme registry - maps theme IDs to theme configurations
* *
@ -28,7 +360,7 @@ const themes = {
dark: false, dark: false,
colors: null, // Use namedTheme('light') colors: null, // Use namedTheme('light')
satellite: null, satellite: null,
overlay: null, overlay: lightOverlay,
}, },
dark: { dark: {
id: 'dark', id: 'dark',
@ -36,7 +368,7 @@ const themes = {
dark: true, dark: true,
colors: null, // Use namedTheme('dark') colors: null, // Use namedTheme('dark')
satellite: null, satellite: null,
overlay: null, overlay: darkOverlay,
}, },
// Custom themes go here. Example: // Custom themes go here. Example:
// 'midnight': { // 'midnight': {
@ -45,10 +377,14 @@ const themes = {
// dark: true, // dark: true,
// colors: { /* full flavor object matching dark-flavor-reference.json schema */ }, // colors: { /* full flavor object matching dark-flavor-reference.json schema */ },
// satellite: { opacity: 0.8, brightnessMin: 0.1 }, // satellite: { opacity: 0.8, brightnessMin: 0.1 },
// overlay: null, // overlay: { /* partial overrides - missing keys fall back to dark overlay */ },
// }, // },
} }
// ═══════════════════════════════════════════════════════════════════════════
// EXPORTED FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════
/** /**
* Get a theme configuration by ID * Get a theme configuration by ID
* @param {string} id - Theme ID * @param {string} id - Theme ID
@ -87,6 +423,44 @@ export function getThemeSprite(id) {
return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}` return `https://protomaps.github.io/basemaps-assets/sprites/v4/${spriteTheme}`
} }
/**
* Get overlay configuration for a specific layer
*
* For contour variants (contoursTest, contoursTest10ft), missing keys cascade
* from the same theme's contours config.
*
* For custom themes, missing keys fall back to the appropriate built-in theme
* (dark or light based on theme.dark flag).
*
* @param {string} themeId - Theme ID
* @param {string} layerKey - Overlay layer key (hillshade, contours, publicLands, etc.)
* @returns {object} Merged overlay config for the layer
*/
export function getOverlayConfig(themeId, layerKey) {
const theme = getTheme(themeId)
const builtinTheme = theme.dark ? themes.dark : themes.light
const builtinOverlay = builtinTheme.overlay[layerKey] || {}
// For contour variants, cascade from same theme's contours config
let baseConfig = builtinOverlay
if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
const contoursBase = builtinTheme.overlay.contours || {}
baseConfig = { ...contoursBase, ...builtinOverlay }
}
// If this is a custom theme with overlay overrides, merge them
if (theme.overlay && theme.overlay[layerKey]) {
// For contour variants in custom themes, also cascade from custom contours
if (layerKey === 'contoursTest' || layerKey === 'contoursTest10ft') {
const customContours = theme.overlay.contours || {}
return { ...baseConfig, ...customContours, ...theme.overlay[layerKey] }
}
return { ...baseConfig, ...theme.overlay[layerKey] }
}
return baseConfig
}
/** /**
* Get list of available themes for UI display * Get list of available themes for UI display
* @returns {Array<{id: string, name: string, dark: boolean}>} * @returns {Array<{id: string, name: string, dark: boolean}>}