diff --git a/frontend/src/lib/components/TrailMap.svelte b/frontend/src/lib/components/TrailMap.svelte index c74da95..6329beb 100644 --- a/frontend/src/lib/components/TrailMap.svelte +++ b/frontend/src/lib/components/TrailMap.svelte @@ -25,7 +25,6 @@ import { nearestOnLine, distanceAlong, positionAtDistance } from '$lib/gpx' import FlagIcon from './FlagIcon.svelte' import { trailStatusColor } from '$lib/trailStatus' - import { env } from '$env/dynamic/public' import { mode } from 'mode-watcher' maplibregl.setWorkerUrl(workerUrl) @@ -84,29 +83,24 @@ } = $props() /** - * Basiskarten von CARTO statt der Standard-OSM-Kacheln: Sie brauchen - * keinen Schlüssel, sind zurückhaltend gezeichnet — die Trail-Linie liegt - * dadurch deutlich sichtbarer darüber — und es gibt sie in einer echten - * dunklen Fassung. Ein nachträglich abgedunkeltes OSM-Bild wäre nur ein - * dunkleres Bild, keine dunkle Karte. + * Basiskarten von OpenFreeMap: zurückhaltend gezeichnet — die Trail-Linie + * liegt dadurch deutlich sichtbarer darüber — und in einer echten dunklen + * Fassung. Ein nachträglich abgedunkeltes OSM-Bild wäre nur ein dunkleres + * Bild, keine dunkle Karte. * - * Mehrere Subdomains, weil Browser die Verbindungen pro Host begrenzen. + * Vorher lagen hier die Rasterkacheln von CARTO. Die kommen inzwischen + * mit einem eingebrannten „API KEY REQUIRED" quer über der Karte — mit + * HTTP 200, weshalb es wie ein Darstellungsfehler aussah und nicht wie + * eine Sperre. Der Schlüssel aus PUBLIC_CARTO_API_KEY änderte daran + * nichts: Die Kachel kam byteidentisch zurück wie ohne ihn. + * + * OpenFreeMap braucht keinen Schlüssel und kennt kein Limit; die Stile + * heißen dort positron und dark und sind genau die, die CARTO als + * Positron und Dark Matter geprägt hat. Die Quellenangabe steckt in der + * Stildatei, sie muss hier nicht mehr von Hand gesetzt werden. */ - // Der Schlüssel muss den Browser erreichen — er kommt deshalb aus den - // PUBLIC_-Variablen. Über $env/dynamic/public statt static, weil die - // Kacheln auch ohne Schlüssel ausgeliefert werden: Fehlt die Variable, - // soll der Build nicht abbrechen. - const CARTO_KEY = env.PUBLIC_CARTO_API_KEY ?? '' - const KEY_PARAM = CARTO_KEY ? `?key=${CARTO_KEY}` : '' - - const cartoTiles = (style: 'light_all' | 'dark_all') => - ['a', 'b', 'c'].map( - (sub) => `https://${sub}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}.png${KEY_PARAM}`, - ) - - const TILES_LIGHT = cartoTiles('light_all') - const TILES_DARK = cartoTiles('dark_all') - const ATTRIBUTION = '© OpenStreetMap-Mitwirkende, © CARTO' + const STYLE_LIGHT = 'https://tiles.openfreemap.org/styles/positron' + const STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark' const isDark = $derived(mode.current === 'dark') @@ -143,18 +137,7 @@ function build() { const instance = new maplibregl.Map({ container, - style: { - version: 8, - sources: { - osm: { - type: 'raster', - tiles: isDark ? TILES_DARK : TILES_LIGHT, - tileSize: 256, - attribution: ATTRIBUTION, - }, - }, - layers: [{ id: 'osm', type: 'raster', source: 'osm' }], - }, + style: isDark ? STYLE_DARK : STYLE_LIGHT, center: FALLBACK_CENTER, zoom: 11, attributionControl: controls ? undefined : false, @@ -166,8 +149,13 @@ instance.addControl(new maplibregl.ScaleControl(), 'bottom-left') } - instance.on('load', () => { + // `style.load` statt `load`: Es feuert auch nach jedem + // setStyle. Ein Stilwechsel wirft Quellen und Layer weg, die + // Trail-Linie muss danach neu entstehen — die Marker nicht, die + // haengen als DOM-Elemente an der Karte und nicht am Stil. + instance.on('style.load', () => { ready = true + drawTrail(instance) }) cleanupMap = () => { @@ -252,18 +240,26 @@ }) } - // Trail-Linie zeichnen bzw. aktualisieren - $effect(() => { - if (!map || !ready) return + /** + * Die layerbezogenen Listener bleiben am Kartenobjekt haengen, auch wenn + * ihr Layer beim Stilwechsel verschwindet und gleich darauf wieder + * entsteht. Ein zweites bindTrailEvents wuerde sie deshalb nur + * verdoppeln. + */ + let trailEventsBound = false + /** + * Trail-Linie zeichnen bzw. aktualisieren. Zwei Aufrufer: der Effekt, wenn + * sich Strecke oder Zustand aendern, und `style.load` nach jedem + * Stilwechsel — dann ist die Quelle fort und muss neu angelegt werden. + */ + function drawTrail(instance: MapLibreMap) { const data = geojson const color = trailStatusColor(status) - const src = map.getSource('trail') as maplibregl.GeoJSONSource | undefined + const src = instance.getSource('trail') as maplibregl.GeoJSONSource | undefined if (!data) { - if (src) { - src.setData({ type: 'FeatureCollection', features: [] }) - } + src?.setData({ type: 'FeatureCollection', features: [] }) return } @@ -271,33 +267,46 @@ if (src) { src.setData(feature) - map.setPaintProperty('trail-line', 'line-color', color) - } else { - map.addSource('trail', { type: 'geojson', data: feature }) - map.addLayer({ - id: 'trail-line', - type: 'line', - source: 'trail', - layout: { 'line-join': 'round', 'line-cap': 'round' }, - paint: { 'line-color': color, 'line-width': 4 }, - }) - map.addLayer({ - id: 'trail-hit', - type: 'line', - source: 'trail', - layout: { 'line-join': 'round', 'line-cap': 'round' }, - paint: { 'line-color': color, 'line-width': 20, 'line-opacity': 0 }, - }) - bindTrailEvents(map) + instance.setPaintProperty('trail-line', 'line-color', color) + return } - }) - /** Beim Themenwechsel die Kachelquelle austauschen. */ + instance.addSource('trail', { type: 'geojson', data: feature }) + instance.addLayer({ + id: 'trail-line', + type: 'line', + source: 'trail', + layout: { 'line-join': 'round', 'line-cap': 'round' }, + paint: { 'line-color': color, 'line-width': 4 }, + }) + instance.addLayer({ + id: 'trail-hit', + type: 'line', + source: 'trail', + layout: { 'line-join': 'round', 'line-cap': 'round' }, + paint: { 'line-color': color, 'line-width': 20, 'line-opacity': 0 }, + }) + + if (!trailEventsBound) { + bindTrailEvents(instance) + trailEventsBound = true + } + } + + // Trail-Linie zeichnen bzw. aktualisieren $effect(() => { if (!map || !ready) return + drawTrail(map) + }) - const src = map.getSource('osm') as maplibregl.RasterTileSource | undefined - src?.setTiles(isDark ? TILES_DARK : TILES_LIGHT) + /** + * Beim Themenwechsel den ganzen Stil tauschen. Kacheln allein tun es + * nicht: Hell und dunkel unterscheiden sich hier nicht im Bild, sondern + * in der Zeichenvorschrift. + */ + $effect(() => { + if (!map || !ready) return + map.setStyle(isDark ? STYLE_DARK : STYLE_LIGHT) }) // Kartenausschnitt auf den Track setzen