Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<option>`: the overlay is deliberately left out of `OVERLAY_WMT_ACTIVITY`, so `handleExtraLayerChange` takes the non-Waymarkedtrails branch (no "Routes in view" legend, an isolated trail is preserved and re-lifted) exactly as the Strava heatmap does, and share links pick the new key up for free since `&route=` is validated against `OVERLAY_SOURCES`. No service-worker or CSP change was needed — `lm.clackspark.workers.dev` is already in both allowlists, so the new tiles join the offline tile cache automatically.
- **v2.17.1:** Fixed the **GPX track being hidden underneath other map layers**. The track's `gpx-line-0` line layer was added without a `beforeId`, so it only sat on top of the style *at the moment it was added* — anything drawn afterwards covered it. The most visible case was the extra-overlay picker: selecting **Waymarkedtrails** or the **Strava heatmap** appended an opaque raster over the loaded track (`applyExtraOverlay` → `L.tileOverlay(...).addTo(map)`), but the same happened with the **slope map** (whose *opacity slider* re-added the image on every tick, since the Leaflet-compat shim's `_renderOverlay` removes and re-adds a layer to re-render it), **climb result / manual climb lines**, the **isolated trail** (`liftIsolatedTrailToTop` explicitly moved it to the very top) and the **GPS accuracy circle**. Rather than re-lifting the track from each of those call sites, the ordering is now enforced at the single choke point where overlays are inserted: a new `getGpxTopBeforeId()` helper returns `GPX_LINE_LAYER_ID` while a track is loaded (and `undefined` otherwise), and all six `addLayer` calls in `_renderOverlay` — circle fill/line, circleMarker, polyline, image and tileOverlay — pass it as `beforeId`, so every overlay slots *underneath* the track automatically, including on re-render. `liftIsolatedTrailToTop` now moves its layers to directly below the track instead of to the absolute top (casing/line relative order is unchanged), and `updateGpxTrackLine` re-raises the track with `moveLayer` on its repaint path as a safety net. A newly loaded track is still appended on top, so it also wins when an overlay was enabled first. Basemap, hillshade and contours already used a `beforeId` and are unaffected; GPX waypoint/start-end/min-max labels are DOM markers and were never subject to layer order.
- **v2.17.0:** Upgraded the map engine from **MapLibre GL JS 5.24.0 to 6.2.0**. v6 ships as **ES modules only** — the UMD bundle and the separate CSP build are gone — so the loading path changed: the vendored set under `vendor/` is now `maplibre-gl.mjs`, `maplibre-gl-shared.mjs` (imported by *both* the main bundle and the worker) and `maplibre-gl-worker.mjs`, all three precached by the service worker so the installed PWA still boots fully offline. Because `script.js` is a classic script whose API lives in global scope (`index.html` drives it from inline handlers), a small first-party ES-module shim, **`maplibre-boot.mjs`**, imports the v6 namespace, pins the worker URL and republishes it as the `maplibregl` global; module scripts and `defer` scripts share one execution queue, so it is guaranteed to run before `maplibre-contour.min.js` and `script.js`. It joins `style.css` / `script.js` / `lang/*.js` as the fifth `?v=`-stamped shell asset. The backend now registers `text/javascript` for `.mjs` explicitly (`mimetypes.add_type`, since the interpreter's table is version-dependent and the Windows registry can override it) and includes `.mjs` in `STATIC_ASSET_SUFFIXES` so the modules get the same immutable cache header as `.js`. No CSP change was needed: a self-hosted, same-origin ESM build constructs its worker directly from the URL rather than laundering it through a blob, so `worker-src 'self'` remains valid. The rest of the app needed no changes — the v5→v6 CSS class names are identical (v6 only *adds* `.maplibregl-marker-draggable`), so `style.css` and the mobile control-corner relocation are untouched; every `Map` / camera / style method and every `MapOptions` key the app passes survives; `Evented.fire(string)` still works for the synthetic `zoomend` / `moveend`; and `addProtocol`'s signature is unchanged, so **maplibre-contour 0.1.0** keeps working as a UMD classic script. v6 also adopts `zoomLevelsToOverscale: 4` by default, slicing vector tiles instead of overscaling them — MapLibre reports this fixes a range of labelling issues, and the new default is taken as-is rather than pinned back to the v5 behaviour, so the contour overlay is the part of this release worth a visual pass. Separately fixed a latent bug this migration surfaced: the off-screen **Print map** map passed `preserveDrawingBuffer: true` as a top-level option, but MapLibre groups the WebGL context attributes under `canvasContextAttributes`, so it had been silently ignored and `getCanvas()` could read an empty buffer; it is now nested correctly. **Note:** v6 removes WebGL 1 support — the map now requires a WebGL2-capable browser.
Expand Down
14 changes: 7 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<!-- ?v= MUST equal CACHE_NAME in service-worker.js: the service worker precaches and
matches these URLs search-sensitively, so a normal refresh fetches the new build and
offline still hits. Bump ?v= (all five tags), CACHE_NAME and BUILD_NUMBER every release. -->
<link rel="stylesheet" href="style.css?v=3012">
<link rel="stylesheet" href="style.css?v=3013">

<!-- Google Identity Services (optional: enables cross-device GPX upload history) -->
<script src="https://accounts.google.com/gsi/client" async defer></script>
Expand Down Expand Up @@ -71,19 +71,19 @@ <h4 id="info-title">About TopoScout</h4>
<p><strong><a href="https://github.com/droidgren/TopoScout">GitHub Project</a></strong> / <strong><a href="http://droidgren.github.io/" target="_blank">droidgren.github.io</a></strong></p>

<p style="font-size: 12px; color: #666; margin-top: 5px; margin-bottom: 0;">
<span id="lbl-version">Version</span>: <span id="app-version">2.18.0</span> (Build <span id="app-build">3012</span>)
<span id="lbl-version">Version</span>: <span id="app-version">2.18.1</span> (Build <span id="app-build">3013</span>)
</p>

<details
style="text-align: left; background: #f9f9f9; padding: 10px; border-radius: 8px; margin: 10px 0; font-size: 12px; color: #555;">
<summary style="font-weight: 700; color: #333; cursor: pointer; outline: none;"
id="info-changelog-title">Changelog</summary>
<ul style="padding-left: 20px; margin: 8px 0 0 0; line-height: 1.5;">
<li><strong>v2.18.1:</strong> <strong>Faster tile loading.</strong> Contour lines no longer build tiles while zoomed too far out to show them, elevation tiles now serve straight from the offline cache instead of being re-checked over the network on every view, and the tile cache holds three times as many tiles so panning back over familiar ground is instant.</li>
<li><strong>v2.18.0:</strong> New <strong>OSM Path layer</strong> route overlay — OpenStreetMap paths and trails from a Mapbox raster style, served through the same Cloudflare proxy as the Lantmäteriet map so no API key is exposed.</li>
<li><strong>v2.17.1:</strong> The <strong>GPX track now always draws on top</strong> — enabling Waymarkedtrails or the Strava heatmap (or a slope map, climb line or isolated trail) no longer hides the loaded track behind it.</li>
<li><strong>v2.17.0:</strong> Upgraded the map engine to <strong>MapLibre GL JS 6.2.0</strong> — still self-hosted and precached, so the installed app keeps working offline. The Print map export now captures the map reliably. Requires a WebGL2-capable browser.</li>
<li><strong>v2.16.0:</strong> Mobile: while the route legend is shown, the map controls (GPS + zoom) now <strong>move to the bottom-left corner</strong> instead of hiding — the attribution banner is tucked away meanwhile — and tapping the minimized control panel now <strong>expands it</strong>.</li>
<li><strong>v2.15.3:</strong> Contour labels now scale with zoom — sparse major-line labels when zoomed out, denser labels (including minor lines) when zoomed in, like classic topo maps.</li>
</ul>
<p style="margin: 8px 0 0 0;"><a href="https://github.com/droidgren/TopoScout#changelog"
target="_blank" rel="noopener noreferrer">Full changelog →</a></p>
Expand Down Expand Up @@ -704,13 +704,13 @@ <h4 id="tutorial-title"></h4>

<!-- MapLibre v6 is ESM-only; this module shim publishes it as the `maplibregl` global the
classic scripts below use. Module scripts and defer scripts run in document order. -->
<script type="module" src="maplibre-boot.mjs?v=3012"></script>
<script type="module" src="maplibre-boot.mjs?v=3013"></script>
<script defer src="vendor/maplibre-contour.min.js"></script>
<!-- jsPDF, vendored locally (not build-versioned) so Print map works offline. -->
<script defer src="vendor/jspdf.umd.min.js"></script>
<script defer src="lang/en.js?v=3012"></script>
<script defer src="lang/sv.js?v=3012"></script>
<script defer src="script.js?v=3012"></script>
<script defer src="lang/en.js?v=3013"></script>
<script defer src="lang/sv.js?v=3013"></script>
<script defer src="script.js?v=3013"></script>
</body>

</html>
14 changes: 12 additions & 2 deletions script.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// ==========================================
// 1. CONFIGURATION & CONSTANTS
// ==========================================
const APP_VERSION = "2.18.0";
const BUILD_NUMBER = "3012";
const APP_VERSION = "2.18.1";
const BUILD_NUMBER = "3013";
const ANALYSIS_SECTION_IDS = ['section-points', 'section-climbs', 'section-slope'];
const ALL_SECTION_IDS = ['section-points', 'section-climbs', 'section-slope', 'section-routes'];
const APP_REFRESH_PARAM = 'app-refresh';
Expand Down Expand Up @@ -1362,6 +1362,13 @@ function createMapAdapter(containerId, options) {
type: 'line',
source: CONTOUR_SOURCE_ID,
'source-layer': 'contours',
// Native zoom, matching the line-opacity stops below. A layer merely
// painted at opacity 0 still counts as "using" its source, so without
// this MapLibre keeps asking maplibre-contour to build tiles at every
// zoom just to render them invisibly. (No DEM downloads are saved -
// mlcontour skips the fetch when no threshold covers the zoom, and the
// thresholds above only cover z11-15 - but the tile plumbing goes away.)
minzoom: 10,
paint: {
'line-color': 'rgba(120, 72, 48, 0.6)',
'line-width': ['match', ['get', 'level'], 1, 1.4, 0.6],
Expand All @@ -1379,6 +1386,9 @@ function createMapAdapter(containerId, options) {
type: 'symbol',
source: CONTOUR_SOURCE_ID,
'source-layer': 'contours',
// Labels only become legible once the lines are fully faded in; keeping
// the layer off below that also keeps it from pulling contour tiles.
minzoom: 11,
// Majors only when zoomed out; from native z14 (20 m / 40 ft minors)
// every line is labelled, so density grows with zoom like the
// interval thresholds above. Zoom in filters snaps to integers.
Expand Down
Loading