diff --git a/README.md b/README.md index b7c2be4..2e6fbf9 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ docker run -p 8000:8000 -v "$(pwd)/gpx-files:/app/gpx-files" toposcout ## Changelog +- **v2.18.1:** **Tile-loading performance**, from a pass over the app against MapLibre's [large-data guide](https://maplibre.org/maplibre-gl-js/docs/guides/large-data/). Three fixes in the tile-request path. (1) **Contour layers now set `minzoom`** (`contour-lines` at 10, `contour-labels` at 11). `contour-lines` already faded to `line-opacity: 0` below native z11.5 via a paint expression, but a layer painted at zero opacity still counts as *using* its source — MapLibre only skips a source when every layer referencing it is hidden by `minzoom`/`maxzoom` at the current zoom. So panning at z5–z10 kept MapLibre asking `maplibre-contour` to produce contour vector tiles that then rendered invisibly. Measured with a headless MapLibre harness driving the same source and layer definitions: contour tile requests at z8/z9.5 go **6 and 4 → 0**, while z11 and z13 are unchanged at 4 and 7, confirming no regression where contours are actually drawn. Worth recording precisely, because the obvious guess is wrong: this saves **worker and protocol work, not DEM network traffic**. Mapterhorn requests below z11 were already zero either way — the mlcontour `thresholds` table only covers z11–15, and mlcontour declines to fetch a DEM tile when no threshold covers the zoom, so the wasted work was tile plumbing around an empty result rather than downloads. The `line-opacity` fade and the label-density `filter` are unchanged — `minzoom` gates *tile loading*, the expressions still govern appearance. (2) **Elevation tiles are now served cache-first with no revalidation.** `tiles.mapterhorn.com` is a static dataset addressed by `z/x/y`, but it shared the stale-while-revalidate branch with the mutable basemap/overlay hosts, so every cache *hit* still fired a background network fetch that could only return identical bytes. A new `IMMUTABLE_TILE_HOSTS` list (and a `matchesHost` helper factored out of `isTileRequest`, preserving its subdomain-suffix matching for `{s}` templates) short-circuits those to the cached response. This compounds, because three independent consumers request the same DEM URLs: MapLibre's `raster-dem` source, `maplibre-contour`'s own worker `DemSource`, and the canvas `Image` LRU behind Scan / Climbs / Slope map. Basemaps, Waymarkedtrails, OSM Paths and the Strava heatmap keep stale-while-revalidate — those *are* re-rendered upstream. (3) **Cache eviction is amortized.** `putTileAndTrim` called `cache.keys()` — enumerating the entire tile cache — after *every* `cache.put`, so a fast pan storing 30 tiles re-walked the Cache Storage index 30 times. Trimming now runs once per 50 puts (`TILE_TRIM_INTERVAL`), guarded by an in-flight flag since the worker services many tile fetches in parallel, and trims down to a low-water mark (`TILE_CACHE_LOW_WATER`, 1000) rather than exactly to the cap so the next sweep is far off. With the per-put scan gone, **`TILE_CACHE_MAX` rose from 400 to 1200** — a viewport can pull three tile families at once (basemap + elevation + a route overlay), so 400 entries was roughly 130 viewport-loads per family and ordinary panning evicted tiles the user was about to pan back onto. The cache may briefly exceed the cap between sweeps, which is fine for a best-effort cache. `TILE_CACHE` remains version-independent and in `KEEP_CACHES`, so the larger cap does not discard existing tiles. Investigated and deliberately left alone: `tileSize: 512` on the DEM source is correct (Mapterhorn really does serve 512-px tiles on the standard XYZ grid — `getElevationAtLatLng` scales 256-unit projected coordinates by 2 to index into them — which already halves DEM tile count versus a 256 declaration); adding the `raster-dem` source eagerly in `flushPendingStyleLayers` costs nothing, since MapLibre loads tiles only for sources that are `used || usedForTerrain`; `maxTileCacheSize` is applied as `Math.min(maxTileCacheSize, computedDefault)` and can therefore only *shrink* the in-memory tile cache, never grow it; `volatile` on sources does not exist in MapLibre GL JS v6.2.0 (it is Mapbox-only); and clustering does not apply, as the app's points are DOM `maplibregl.Marker`s rather than a GeoJSON point source. - **v2.18.0:** Added an **OSM Path layer** route overlay — OpenStreetMap paths and trails rendered from a Mapbox raster style (`knixen/ckqxhc3530wpx17lfdvkd1wdi`, 256-px tiles requested at `@2x` for retina sharpness, `maxZoom` 19). Because the Mapbox tile URL carries an `access_token`, the layer is served through the **existing Cloudflare worker** rather than fetched directly: `worker.js` — until now a single-upstream Lantmäteriet proxy that parsed its path positionally as `/{z}/{x}/{y}` — now recognises a named `/osmpaths/` prefix and shifts the coordinate segments accordingly, so the original URL shape keeps working byte-for-byte for deployed clients and already-cached tiles. The worker appends the token and the `@2x` suffix server-side, and the client only ever sees `lm.clackspark.workers.dev/osmpaths/{z}/{x}/{y}`. The upstream request differs per route: the Lantmäteriet branch keeps spoofing `Referer: minkarta.lantmateriet.se`, while the Mapbox branch **forwards the page's own (already validated) Referer**, so a URL-restricted Mapbox token still authorises through the proxy. Edge caching for the Mapbox branch uses `cacheTtlByStatus` instead of a flat `cacheTtl` so an error response (e.g. a `401` from a bad token) can't be pinned in Cloudflare's cache for a week, and the browser `Cache-Control` header is now conditional on `imageResponse.ok` for both routes. The tile-bounds check gained a per-route zoom cap (22 for Mapbox, the previous 20 for Lantmäteriet). Client-side the change is a single `OVERLAY_SOURCES` entry plus a dropdown `