/** * Theme Registry for Navi * * Provides a centralized registry for map themes, supporting both built-in * protomaps themes (light/dark) and custom themes with full flavor objects. * * Theme config structure: * id: string - unique identifier (used in store, data-theme attr) * name: string - display name for UI * dark: boolean - true if dark theme (affects overlay styling, sprite fallback) * colors: object|null - null for built-in themes, full flavor object for custom * satellite: object|null - raster adjustments when satellite layer is present * overlay: object - overlay layer styling configuration */ 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 * * Built-in themes (light/dark) use colors: null to signal that namedTheme() * should be called at render time. Custom themes provide a full flavor object. */ const themes = { light: { id: 'light', name: 'Light', dark: false, colors: null, // Use namedTheme('light') satellite: null, overlay: lightOverlay, }, dark: { id: 'dark', name: 'Dark', dark: true, colors: null, // Use namedTheme('dark') satellite: null, overlay: darkOverlay, }, // Custom themes go here. Example: // 'midnight': { // id: 'midnight', // name: 'Midnight', // dark: true, // colors: { /* full flavor object matching dark-flavor-reference.json schema */ }, // satellite: { opacity: 0.8, brightnessMin: 0.1 }, // overlay: { /* partial overrides - missing keys fall back to dark overlay */ }, // }, } // ═══════════════════════════════════════════════════════════════════════════ // EXPORTED FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ /** * Get a theme configuration by ID * @param {string} id - Theme ID * @returns {object} Theme config, falls back to 'dark' if not found */ export function getTheme(id) { return themes[id] || themes.dark } /** * Get the color flavor for a theme * For built-in themes, calls namedTheme(). For custom themes, returns colors directly. * @param {string} id - Theme ID * @returns {object} Flavor object for use with protomaps layers() */ export function getThemeColors(id) { const theme = getTheme(id) if (theme.colors === null) { // Built-in theme - use namedTheme from protomaps-themes-base return namedTheme(id) } return theme.colors } /** * Get the sprite URL for a theme * Built-in themes use their own sprites. Custom themes fall back to * dark or light sprite based on the theme's dark flag. * @param {string} id - Theme ID * @returns {string} Full sprite URL */ export function getThemeSprite(id) { const theme = getTheme(id) // Custom themes don't have matching sprites on CDN - fall back based on dark flag const spriteTheme = theme.colors === null ? id : (theme.dark ? 'dark' : 'light') 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 * @returns {Array<{id: string, name: string, dark: boolean}>} */ export function themeList() { return Object.values(themes).map(({ id, name, dark }) => ({ id, name, dark })) } /** * Check if a theme ID is valid/registered * @param {string} id - Theme ID to check * @returns {boolean} */ export function isValidTheme(id) { return id in themes } export default themes