diff --git a/src/components/MapView.jsx b/src/components/MapView.jsx index 77d4b63..c5154b9 100644 --- a/src/components/MapView.jsx +++ b/src/components/MapView.jsx @@ -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: - 'Protomaps | OSM', + 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 diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..ce0e0a8 --- /dev/null +++ b/src/config.js @@ -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]) +} diff --git a/src/hooks/useConfig.js b/src/hooks/useConfig.js new file mode 100644 index 0000000..006e37d --- /dev/null +++ b/src/hooks/useConfig.js @@ -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]) +} diff --git a/src/main.jsx b/src/main.jsx index ef272ce..d452a6d 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,23 +1,27 @@ 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( + + + + , + ) +})