fix: Kartenkacheln von OpenFreeMap statt CARTO

CARTO liefert die Kacheln inzwischen mit einem eingebrannten
"API KEY REQUIRED" quer ueber der Karte — und zwar mit HTTP 200,
weshalb es wie ein Darstellungsfehler aussah und nicht wie eine Sperre.
Der Schluessel aus PUBLIC_CARTO_API_KEY aenderte daran nichts: Die
Kachel kam byteidentisch zurueck wie ohne ihn, als key=, api_key= und
apikey=.

OpenFreeMap braucht keinen Schluessel und kennt kein Limit; die Stile
heissen dort positron und dark und sind genau die, die CARTO als
Positron und Dark Matter gepraegt hat.

Weil das Vektorkacheln sind, tauscht der Themenwechsel jetzt den ganzen
Stil statt nur der Kachel-URLs. Ein Stilwechsel wirft Quellen und Layer
weg, deshalb legt style.load die Trail-Linie neu an — die Marker nicht,
die haengen als DOM-Elemente an der Karte und nicht am Stil.

PUBLIC_CARTO_API_KEY wird nicht mehr gelesen und kann aus .env und der
Deployment-Konfiguration verschwinden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P32KoesVtABd6xWsqMKzhr
This commit is contained in:
Daniel Michelberger 2026-09-07 22:09:30 +02:00
parent 69588528e8
commit c817e208cd

View file

@ -25,7 +25,6 @@
import { nearestOnLine, distanceAlong, positionAtDistance } from '$lib/gpx' import { nearestOnLine, distanceAlong, positionAtDistance } from '$lib/gpx'
import FlagIcon from './FlagIcon.svelte' import FlagIcon from './FlagIcon.svelte'
import { trailStatusColor } from '$lib/trailStatus' import { trailStatusColor } from '$lib/trailStatus'
import { env } from '$env/dynamic/public'
import { mode } from 'mode-watcher' import { mode } from 'mode-watcher'
maplibregl.setWorkerUrl(workerUrl) maplibregl.setWorkerUrl(workerUrl)
@ -84,29 +83,24 @@
} = $props() } = $props()
/** /**
* Basiskarten von CARTO statt der Standard-OSM-Kacheln: Sie brauchen * Basiskarten von OpenFreeMap: zurückhaltend gezeichnet — die Trail-Linie
* keinen Schlüssel, sind zurückhaltend gezeichnet — die Trail-Linie liegt * liegt dadurch deutlich sichtbarer darüber — und in einer echten dunklen
* dadurch deutlich sichtbarer darüber — und es gibt sie in einer echten * Fassung. Ein nachträglich abgedunkeltes OSM-Bild wäre nur ein dunkleres
* dunklen Fassung. Ein nachträglich abgedunkeltes OSM-Bild wäre nur ein * Bild, keine dunkle Karte.
* 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 const STYLE_LIGHT = 'https://tiles.openfreemap.org/styles/positron'
// PUBLIC_-Variablen. Über $env/dynamic/public statt static, weil die const STYLE_DARK = 'https://tiles.openfreemap.org/styles/dark'
// 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 = '&copy; OpenStreetMap-Mitwirkende, &copy; CARTO'
const isDark = $derived(mode.current === 'dark') const isDark = $derived(mode.current === 'dark')
@ -143,18 +137,7 @@
function build() { function build() {
const instance = new maplibregl.Map({ const instance = new maplibregl.Map({
container, container,
style: { style: isDark ? STYLE_DARK : STYLE_LIGHT,
version: 8,
sources: {
osm: {
type: 'raster',
tiles: isDark ? TILES_DARK : TILES_LIGHT,
tileSize: 256,
attribution: ATTRIBUTION,
},
},
layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
},
center: FALLBACK_CENTER, center: FALLBACK_CENTER,
zoom: 11, zoom: 11,
attributionControl: controls ? undefined : false, attributionControl: controls ? undefined : false,
@ -166,8 +149,13 @@
instance.addControl(new maplibregl.ScaleControl(), 'bottom-left') 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 ready = true
drawTrail(instance)
}) })
cleanupMap = () => { cleanupMap = () => {
@ -252,18 +240,26 @@
}) })
} }
// Trail-Linie zeichnen bzw. aktualisieren /**
$effect(() => { * Die layerbezogenen Listener bleiben am Kartenobjekt haengen, auch wenn
if (!map || !ready) return * 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 data = geojson
const color = trailStatusColor(status) 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 (!data) {
if (src) { src?.setData({ type: 'FeatureCollection', features: [] })
src.setData({ type: 'FeatureCollection', features: [] })
}
return return
} }
@ -271,33 +267,46 @@
if (src) { if (src) {
src.setData(feature) src.setData(feature)
map.setPaintProperty('trail-line', 'line-color', color) instance.setPaintProperty('trail-line', 'line-color', color)
} else { return
map.addSource('trail', { type: 'geojson', data: feature }) }
map.addLayer({
instance.addSource('trail', { type: 'geojson', data: feature })
instance.addLayer({
id: 'trail-line', id: 'trail-line',
type: 'line', type: 'line',
source: 'trail', source: 'trail',
layout: { 'line-join': 'round', 'line-cap': 'round' }, layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': color, 'line-width': 4 }, paint: { 'line-color': color, 'line-width': 4 },
}) })
map.addLayer({ instance.addLayer({
id: 'trail-hit', id: 'trail-hit',
type: 'line', type: 'line',
source: 'trail', source: 'trail',
layout: { 'line-join': 'round', 'line-cap': 'round' }, layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: { 'line-color': color, 'line-width': 20, 'line-opacity': 0 }, paint: { 'line-color': color, 'line-width': 20, 'line-opacity': 0 },
}) })
bindTrailEvents(map)
}
})
/** Beim Themenwechsel die Kachelquelle austauschen. */ if (!trailEventsBound) {
bindTrailEvents(instance)
trailEventsBound = true
}
}
// Trail-Linie zeichnen bzw. aktualisieren
$effect(() => { $effect(() => {
if (!map || !ready) return 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 // Kartenausschnitt auf den Track setzen