From 0f8015d23306b215ff1f27e4b63cfde77872d9e8 Mon Sep 17 00:00:00 2001 From: Rich Fecher Date: Mon, 22 Sep 2025 17:15:19 -0400 Subject: [PATCH] allow the basemaps to be configurable --- config.js | 46 +++++++++++++++++++++++ src/custom.d.ts | 17 +++++++++ src/stores/MapOptionsStore.ts | 70 ++++++++++++++++++++++++++++++++++- 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/config.js b/config.js index cda8ba12..d10ffdd4 100644 --- a/config.js +++ b/config.js @@ -68,6 +68,52 @@ const config = { // ] // } // } + + // Configure custom basemaps. You have three options: + // 1. Replace all default basemaps with your own + // 2. Add custom basemaps to the existing defaults + // 3. Disable specific default basemaps + // + // basemaps: { + // // Option 1: Replace all default basemaps with custom ones + // basemaps: [ + // { + // name: 'My Custom Raster', + // type: 'raster', + // url: ['https://example.com/tiles/{z}/{x}/{y}.png'], + // attribution: '© My Custom Provider', + // maxZoom: 18 + // }, + // { + // name: 'My Custom Vector', + // type: 'vector', + // url: 'https://example.com/style.json?key={maptiler_key}', + // attribution: '© My Vector Provider' + // } + // ], + // + // // Option 2: Add custom basemaps to defaults (commented out when using Option 1) + // // customBasemaps: [ + // // { + // // name: 'Additional Custom Map', + // // type: 'raster', + // // url: ['https://another-provider.com/{z}/{x}/{y}{retina_suffix}.png?key={thunderforest_key}'], + // // attribution: '© Another Provider', + // // maxZoom: 19, + // // tilePixelRatio: 2 // Optional: override retina detection + // // } + // // ], + // + // // Option 3: Disable specific default basemaps by name (commented out when using Option 1) + // // disabledBasemaps: ['TF Transport', 'TF Cycle', 'TF Outdoors'] + // } + // + // Available API key placeholders for URLs: + // - {omniscale_key} - replaced with keys.omniscale + // - {maptiler_key} - replaced with keys.maptiler + // - {thunderforest_key} - replaced with keys.thunderforest + // - {kurviger_key} - replaced with keys.kurviger + // - {retina_suffix} - replaced with '@2x' on retina displays, empty otherwise } // this is needed for jest (with our current setup at least) diff --git a/src/custom.d.ts b/src/custom.d.ts index d3374601..725d20ba 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -9,6 +9,15 @@ declare module 'config' { readonly options: { profile: string }[] } + interface BasemapConfig { + name: string + type: 'raster' | 'vector' + url: string[] | string + attribution: string + maxZoom?: number + tilePixelRatio?: number + } + const routingApi: string const geocodingApi: string const defaultTiles: string @@ -37,6 +46,14 @@ declare module 'config' { } const profile_group_mapping: Record const profiles: object + const basemaps: { + // Replace all default basemaps with custom ones + basemaps?: BasemapConfig[] + // Add custom basemaps to the default ones + customBasemaps?: BasemapConfig[] + // Disable specific default basemaps by name + disabledBasemaps?: string[] + } | undefined } declare module 'react-responsive' { diff --git a/src/stores/MapOptionsStore.ts b/src/stores/MapOptionsStore.ts index 17a5fd98..f31a982f 100644 --- a/src/stores/MapOptionsStore.ts +++ b/src/stores/MapOptionsStore.ts @@ -156,7 +156,8 @@ const wanderreitkarte: RasterStyle = { maxZoom: 18, } -const styleOptions: StyleOption[] = [ +// Default basemaps - these will be used as fallback when no custom configuration is provided +const defaultBasemaps: StyleOption[] = [ omniscale, osmOrg, osmCycl, @@ -169,6 +170,71 @@ const styleOptions: StyleOption[] = [ wanderreitkarte, ] +// Function to create basemap configurations from config +function createBasemapsFromConfig(): StyleOption[] { + // If custom basemaps are completely defined, use them instead of defaults + if (config.basemaps?.basemaps) { + return config.basemaps.basemaps.map(convertConfigBasemapToStyleOption) + } + + // Start with default basemaps + let basemaps = [...defaultBasemaps] + + // Remove disabled basemaps if specified + if (config.basemaps?.disabledBasemaps) { + basemaps = basemaps.filter(basemap => !config.basemaps!.disabledBasemaps!.includes(basemap.name)) + } + + // Add custom basemaps if specified + if (config.basemaps?.customBasemaps) { + const customBasemaps = config.basemaps.customBasemaps.map(convertConfigBasemapToStyleOption) + basemaps = [...basemaps, ...customBasemaps] + } + + return basemaps +} + +// Convert config basemap to StyleOption, handling API key interpolation and retina detection +function convertConfigBasemapToStyleOption(configBasemap: any): StyleOption { + let processedUrl = configBasemap.url + + // Handle API key interpolation for URLs + if (typeof processedUrl === 'string') { + processedUrl = interpolateApiKeys(processedUrl) + } else if (Array.isArray(processedUrl)) { + processedUrl = processedUrl.map(interpolateApiKeys) + } + + const styleOption: StyleOption = { + name: configBasemap.name, + type: configBasemap.type, + url: processedUrl, + attribution: configBasemap.attribution, + maxZoom: configBasemap.maxZoom, + } + + // Add tilePixelRatio for raster styles if specified or use default retina detection + if (configBasemap.type === 'raster') { + const rasterStyle = styleOption as RasterStyle + rasterStyle.tilePixelRatio = configBasemap.tilePixelRatio ?? tilePixelRatio + } + + return styleOption +} + +// Helper function to interpolate API keys in URLs +function interpolateApiKeys(url: string): string { + return url + .replace('{omniscale_key}', osApiKey) + .replace('{maptiler_key}', mapTilerKey) + .replace('{thunderforest_key}', thunderforestApiKey) + .replace('{kurviger_key}', kurvigerApiKey) + .replace('{retina_suffix}', retina2x) +} + +// Create the final styleOptions array +const styleOptions: StyleOption[] = createBasemapsFromConfig() + export default class MapOptionsStore extends Store { constructor() { super(MapOptionsStore.getInitialState()) @@ -181,7 +247,7 @@ export default class MapOptionsStore extends Store { `Could not find tile layer specified in config: '${config.defaultTiles}', using default instead`, ) return { - selectedStyle: selectedStyle ? selectedStyle : omniscale, + selectedStyle: selectedStyle ? selectedStyle : styleOptions[0], styleOptions, routingGraphEnabled: false, urbanDensityEnabled: false,