mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
feat(navi): config-driven tile source, defaults, and feature flags
Load deployment config from /api/config on startup: - src/config.js: loader with 3s timeout + hardcoded fallback - src/hooks/useConfig.js: useConfig() and useFeature() hooks - MapView.jsx: tile URL, attribution, center, zoom from config - main.jsx: loads config before first render Falls back to home profile defaults if backend unavailable. No visible behavior change — infrastructure for future features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a819458865
commit
edc5a9788d
4 changed files with 146 additions and 22 deletions
|
|
@ -6,12 +6,17 @@ import { layers, namedTheme } from 'protomaps-themes-base'
|
|||
import { useStore } from '../store'
|
||||
import { decodePolyline } from '../utils/decode'
|
||||
import { fetchReverse } from '../api'
|
||||
import { getConfig } from '../config'
|
||||
|
||||
const ROUTE_SOURCE = 'route-source'
|
||||
const ROUTE_LAYER_PREFIX = 'route-layer-'
|
||||
|
||||
/** Build a full MapLibre style object for the given theme */
|
||||
function buildStyle(themeName) {
|
||||
const config = getConfig()
|
||||
const tileUrl = config?.tileset?.url || '/tiles/na.pmtiles'
|
||||
const attribution = config?.tileset?.attribution || 'Protomaps \u00a9 OSM'
|
||||
|
||||
return {
|
||||
version: 8,
|
||||
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
|
||||
|
|
@ -19,9 +24,8 @@ function buildStyle(themeName) {
|
|||
sources: {
|
||||
protomaps: {
|
||||
type: 'vector',
|
||||
url: 'pmtiles:///tiles/na.pmtiles',
|
||||
attribution:
|
||||
'<a href="https://protomaps.com">Protomaps</a> | <a href="https://openstreetmap.org">OSM</a>',
|
||||
url: `pmtiles://${tileUrl}`,
|
||||
attribution,
|
||||
},
|
||||
},
|
||||
layers: layers('protomaps', namedTheme(themeName), { lang: 'en' }),
|
||||
|
|
@ -68,8 +72,11 @@ const MapView = forwardRef(function MapView(_, ref) {
|
|||
const protocol = new Protocol()
|
||||
maplibregl.addProtocol('pmtiles', protocol.tile)
|
||||
|
||||
const DEFAULT_CENTER = [-114.6066, 42.5736]
|
||||
const DEFAULT_ZOOM = 10
|
||||
const config = getConfig()
|
||||
const DEFAULT_CENTER = config?.defaults?.center
|
||||
? [config.defaults.center[1], config.defaults.center[0]] // config is [lat,lon], MapLibre wants [lon,lat]
|
||||
: [-114.6066, 42.5736]
|
||||
const DEFAULT_ZOOM = config?.defaults?.zoom || 10
|
||||
|
||||
const initialTheme = document.documentElement.getAttribute('data-theme') || 'dark'
|
||||
currentThemeRef.current = initialTheme
|
||||
|
|
|
|||
84
src/config.js
Normal file
84
src/config.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Deployment config loader.
|
||||
*
|
||||
* Fetches /api/config on startup and caches the result.
|
||||
* Falls back to hardcoded defaults matching the home profile if the
|
||||
* API is unavailable (backend restart, network issue).
|
||||
*/
|
||||
|
||||
const FALLBACK_CONFIG = {
|
||||
profile: 'home',
|
||||
region_name: 'North America',
|
||||
tileset: {
|
||||
url: '/tiles/na.pmtiles',
|
||||
bounds: [-168, 14, -52, 72],
|
||||
max_zoom: 15,
|
||||
attribution: 'Protomaps © OSM',
|
||||
},
|
||||
services: {
|
||||
geocode: '/api/geocode',
|
||||
reverse: '/api/reverse',
|
||||
address_book: '/api/address_book',
|
||||
valhalla: '/valhalla',
|
||||
},
|
||||
features: {
|
||||
has_nominatim_details: false,
|
||||
has_kiwix_wiki: false,
|
||||
has_hillshade: false,
|
||||
has_3d_terrain: false,
|
||||
has_traffic_overlay: false,
|
||||
has_landclass: false,
|
||||
has_address_book_write: false,
|
||||
},
|
||||
defaults: {
|
||||
center: [42.5736, -114.6066],
|
||||
zoom: 10,
|
||||
},
|
||||
}
|
||||
|
||||
let _config = null
|
||||
let _configPromise = null
|
||||
|
||||
/**
|
||||
* Fetch config from backend. Returns cached config on subsequent calls.
|
||||
* Falls back to FALLBACK_CONFIG if API fails.
|
||||
*/
|
||||
export function loadConfig() {
|
||||
if (_configPromise) return _configPromise
|
||||
|
||||
_configPromise = fetch('/api/config', { signal: AbortSignal.timeout(3000) })
|
||||
.then((resp) => {
|
||||
if (!resp.ok) throw new Error(`Config API returned ${resp.status}`)
|
||||
return resp.json()
|
||||
})
|
||||
.then((data) => {
|
||||
_config = data
|
||||
console.log('[navi] Config loaded:', data.profile, `(${data.region_name})`)
|
||||
console.log('[navi] Feature flags:', data.features)
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn('[navi] Config API unavailable, using fallback:', err.message)
|
||||
_config = FALLBACK_CONFIG
|
||||
return FALLBACK_CONFIG
|
||||
})
|
||||
|
||||
return _configPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current config synchronously. Returns null if not yet loaded.
|
||||
*/
|
||||
export function getConfig() {
|
||||
return _config
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a feature flag from the loaded config.
|
||||
* @param {string} flag - Feature flag name (e.g. 'has_hillshade')
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasFeature(flag) {
|
||||
if (!_config) return false
|
||||
return Boolean(_config.features?.[flag])
|
||||
}
|
||||
29
src/hooks/useConfig.js
Normal file
29
src/hooks/useConfig.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { loadConfig, getConfig, hasFeature } from '../config'
|
||||
|
||||
/**
|
||||
* Hook that returns the deployment config, loading it if needed.
|
||||
* Components using this will re-render once config is loaded.
|
||||
*/
|
||||
export function useConfig() {
|
||||
const [config, setConfig] = useState(getConfig)
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
loadConfig().then(setConfig)
|
||||
}
|
||||
}, [config])
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check a single feature flag.
|
||||
* @param {string} flag - e.g. 'has_hillshade'
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function useFeature(flag) {
|
||||
const config = useConfig()
|
||||
if (!config) return false
|
||||
return Boolean(config.features?.[flag])
|
||||
}
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { loadConfig } from './config'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
// Load deployment config before rendering — non-blocking (fallback kicks in on failure)
|
||||
loadConfig().then(() => {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Toaster
|
||||
|
|
@ -20,4 +23,5 @@ createRoot(document.getElementById('root')).render(
|
|||
}}
|
||||
/>
|
||||
</StrictMode>,
|
||||
)
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue