From 8cc2b07b1fa1ed12c4a538be7dbcef7c75f678c5 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Thu, 4 Jun 2026 00:52:58 +0200 Subject: [PATCH] feat: multiple map tile styles switchable in Settings > Interface New src/mapStyles.ts defines five styles from bincio_planner's sources: - Liberty (OpenFreeMap vector, default) - CyclOSM (cycling infrastructure raster) - Topo (OpenTopoMap elevation raster) - Satellite (Esri World Imagery raster) - OSM (standard raster fallback) Raster sources are wrapped in a StyleSpecification so MapLibre handles them natively. Setting persisted via ThemeContext (AsyncStorage key mapTileStyle). RecordingScreen and ActivityDetailScreen both read from context so the style updates everywhere simultaneously. MAP_STRATEGY.md added (untracked) with full map roadmap. --- .gitignore | 1 + src/ThemeContext.tsx | 12 ++++- src/mapStyles.ts | 69 ++++++++++++++++++++++++++++ src/screens/ActivityDetailScreen.tsx | 8 ++-- src/screens/RecordingScreen.tsx | 8 ++-- src/screens/SettingsScreen.tsx | 22 ++++++++- 6 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 src/mapStyles.ts diff --git a/.gitignore b/.gitignore index d914c32..f9eb17a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.* # generated native folders /ios /android +MAP_STRATEGY.md diff --git a/src/ThemeContext.tsx b/src/ThemeContext.tsx index cb8ff4d..71891b3 100644 --- a/src/ThemeContext.tsx +++ b/src/ThemeContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { PALETTES, FONT_SCALE, type PaletteKey, type FontSizeKey } from './theme'; +import { MAP_STYLES, type MapTileStyle } from './mapStyles'; export type MapOrientation = 'north' | 'compass' | 'course'; @@ -16,6 +17,8 @@ interface ThemeValue { scale: number; mapOrientation: MapOrientation; setMapOrientation: (o: MapOrientation) => void; + mapTileStyle: MapTileStyle; + setMapTileStyle: (s: MapTileStyle) => void; } const MAP_ORIENTATIONS: MapOrientation[] = ['north', 'compass', 'course']; @@ -31,6 +34,8 @@ const ThemeContext = createContext({ scale: 1, mapOrientation: 'north', setMapOrientation: () => {}, + mapTileStyle: 'liberty', + setMapTileStyle: () => {}, }); export function ThemeProvider({ children }: { children: React.ReactNode }) { @@ -38,20 +43,23 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { const [fontSize, setFontSizeState] = useState('medium'); const [boldLabels, setBoldLabelsState] = useState(false); const [mapOrientation, setMapOrientationState] = useState('north'); + const [mapTileStyle, setMapTileStyleState] = useState('liberty'); useEffect(() => { (async () => { - const [p, f, b, m] = await Promise.all([ + const [p, f, b, m, t] = await Promise.all([ AsyncStorage.getItem('themePalette'), AsyncStorage.getItem('themeFontSize'), AsyncStorage.getItem('themeBoldLabels'), AsyncStorage.getItem('mapOrientation'), + AsyncStorage.getItem('mapTileStyle'), ]); if (p && p in PALETTES) setPaletteState(p as PaletteKey); if (f && f in FONT_SCALE) setFontSizeState(f as FontSizeKey); if (b !== null) setBoldLabelsState(b === 'true'); if (m && MAP_ORIENTATIONS.includes(m as MapOrientation)) setMapOrientationState(m as MapOrientation); + if (t && t in MAP_STYLES) setMapTileStyleState(t as MapTileStyle); })(); }, []); @@ -59,6 +67,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { function setFontSize(f: FontSizeKey) { setFontSizeState(f); AsyncStorage.setItem('themeFontSize', f); } function setBoldLabels(b: boolean) { setBoldLabelsState(b); AsyncStorage.setItem('themeBoldLabels', String(b)); } function setMapOrientation(o: MapOrientation) { setMapOrientationState(o); AsyncStorage.setItem('mapOrientation', o); } + function setMapTileStyle(s: MapTileStyle) { setMapTileStyleState(s); AsyncStorage.setItem('mapTileStyle', s); } return ( {children} diff --git a/src/mapStyles.ts b/src/mapStyles.ts new file mode 100644 index 0000000..0350716 --- /dev/null +++ b/src/mapStyles.ts @@ -0,0 +1,69 @@ +import type { StyleSpecification } from '@maplibre/maplibre-gl-style-spec'; + +export type MapTileStyle = 'liberty' | 'cyclosm' | 'topo' | 'satellite' | 'osm'; + +export interface MapStyleDef { + label: string; + description: string; + style: string | StyleSpecification; +} + +function rasterStyle(tiles: string[], attribution: string): StyleSpecification { + return { + version: 8, + sources: { + base: { type: 'raster', tiles, tileSize: 256, attribution }, + }, + layers: [{ id: 'base', type: 'raster', source: 'base' }], + } as StyleSpecification; +} + +export const MAP_STYLES: Record = { + liberty: { + label: 'Liberty', + description: 'Vector street map', + style: 'https://tiles.openfreemap.org/styles/liberty', + }, + cyclosm: { + label: 'CyclOSM', + description: 'Cycling infrastructure', + style: rasterStyle( + [ + 'https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + 'https://b.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + 'https://c.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + ], + '© OpenStreetMap contributors, CyclOSM', + ), + }, + topo: { + label: 'Topo', + description: 'Elevation & terrain', + style: rasterStyle( + [ + 'https://a.tile.opentopomap.org/{z}/{x}/{y}.png', + 'https://b.tile.opentopomap.org/{z}/{x}/{y}.png', + 'https://c.tile.opentopomap.org/{z}/{x}/{y}.png', + ], + '© OpenStreetMap contributors, OpenTopoMap', + ), + }, + satellite: { + label: 'Satellite', + description: 'Esri World Imagery', + style: rasterStyle( + ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], + '© Esri, Maxar, Earthstar Geographics', + ), + }, + osm: { + label: 'OSM', + description: 'Standard street map', + style: rasterStyle( + ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'], + '© OpenStreetMap contributors', + ), + }, +}; + +export const MAP_TILE_STYLE_ORDER: MapTileStyle[] = ['liberty', 'cyclosm', 'topo', 'satellite', 'osm']; diff --git a/src/screens/ActivityDetailScreen.tsx b/src/screens/ActivityDetailScreen.tsx index a929093..ef36f33 100644 --- a/src/screens/ActivityDetailScreen.tsx +++ b/src/screens/ActivityDetailScreen.tsx @@ -16,8 +16,7 @@ import { } from '../sports'; import { colors } from '../theme'; import { useTheme } from '../ThemeContext'; - -const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'; +import { MAP_STYLES } from '../mapStyles'; type Route = RouteProp; @@ -57,7 +56,8 @@ function formatDuration(secs: number) { export function ActivityDetailScreen() { const nav = useNavigation(); - const { accent, accentDim } = useTheme(); + const { accent, accentDim, mapTileStyle } = useTheme(); + const mapStyle = MAP_STYLES[mapTileStyle].style; const insets = useSafeAreaInsets(); const route = useRoute(); @@ -166,7 +166,7 @@ export function ActivityDetailScreen() { {points === null ? : ( - + {bounds && ( ; export function RecordingScreen() { const nav = useNavigation