Files
TREK/client/src/components/Map/useDawarichTrailGL.ts
T
Maurice c1d55c463c fix(dawarich): three things a tester on dev1 ran into
The Atlas country check timed out. It asked Dawarich for a year of visited
cities in one request, and Dawarich computes that over every point in the
window with nothing cached, so a dense recorder never got under the 15 s the
server allows. The year is now asked for a month at a time, one request after
another, and folded back together. The windows sit a second apart because
Dawarich reads the bounds as inclusive.

The recorded route only appeared after a reload. Every Dawarich call from the
browser carried the shared 8 s timeout, so the browser gave up while the server
carried on and cached the answer for the next page load. Calls that make the
server talk to Dawarich now wait two minutes, the Atlas four. On the GL
renderers the draw also gave up whenever isStyleLoaded() was false, which it is
while any tile is still loading, and only retried on style.load. It now waits
for idle. The hazard overlay had the same guard and gets the same wait. The
pill shows a ring while the route loads, so a slow first fetch no longer looks
like "done, nothing there".

A collapsed day now takes its recorded route off the map, following the same
folds the planner already applies to places, the road trip sidebar's included.
2026-09-14 23:00:57 +02:00

112 lines
4.0 KiB
TypeScript

import { useEffect } from 'react'
import type { Map } from 'mapbox-gl'
import type { GeoJSONSource } from 'mapbox-gl'
import type { DawarichTrack } from '@trek/shared'
import { DAWARICH_TRAIL_CASING, trailGeoJson, trailSegments } from './dawarichTrail'
/**
* The recorded route on the GL renderer — the imperative twin of
* `DawarichTrailLayer`.
*
* The ids start with `trek-` because `applySatellite` finds the first layer
* whose id carries one of TREK's own prefixes and inserts the satellite tile
* beneath it. A layer named `dawarich-trail` would end up *under* the
* satellite image and simply not be there when someone switched basemap on.
*
* Everything is rebuilt on `style.load`, because the whole GL map is torn down
* and reassembled when the provider, the style or the token changes, and a
* source added outside that effect does not survive it.
*/
const SOURCE = 'trek-dawarich-trail'
const CASING_LAYER = 'trek-dawarich-trail-casing'
const LINE_LAYER = 'trek-dawarich-trail-line'
export function useDawarichTrailGL(
map: Map | null,
ready: boolean,
track: DawarichTrack | null | undefined,
selectedDate?: string | null,
/** Insert beneath this layer, so the planned route stays on top. */
beforeId?: string,
/** Local dates whose day is folded away in the day plan. */
hiddenDates?: ReadonlySet<string> | null,
): void {
useEffect(() => {
if (!map || !ready) return
const data = trailGeoJson(trailSegments(track ?? null, selectedDate, hiddenDates))
// `isStyleLoaded()` does not mean "the style has loaded". It is false while
// any source still has tiles in flight, which is the usual state right after
// someone switches the overlay on and the map is still filling in. Giving up
// there left the line undrawn until something rebuilt the style, which in
// practice meant reloading the page. `idle` is the map saying everything it
// was fetching has arrived, so the draw waits for it once and tries again.
let waiting = false
const draw = () => {
if (!map.isStyleLoaded()) {
if (!waiting) {
waiting = true
map.once('idle', retry)
}
return
}
const existing = map.getSource(SOURCE) as GeoJSONSource | undefined
if (existing) {
// setData rather than remove-and-add: a rebuilt source flickers, and a
// refresh every two minutes would flicker every two minutes.
existing.setData(data as never)
return
}
map.addSource(SOURCE, { type: 'geojson', data: data as never })
const anchor = beforeId && map.getLayer(beforeId) ? beforeId : undefined
map.addLayer(
{
id: CASING_LAYER,
type: 'line',
source: SOURCE,
layout: { 'line-cap': 'round', 'line-join': 'round' },
// theme-lint-disable — map paint
paint: { 'line-color': DAWARICH_TRAIL_CASING, 'line-width': 6, 'line-opacity': 0.55 },
},
anchor,
)
map.addLayer(
{
id: LINE_LAYER,
type: 'line',
source: SOURCE,
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: {
// Per-day colour off the feature, with the same fallback the GPX
// layer uses when a feature somehow carries none.
'line-color': ['coalesce', ['get', 'color'], DAWARICH_TRAIL_CASING],
'line-width': 3,
'line-opacity': 0.85,
// Dashed, so the recording is distinguishable from the planned
// route without relying on colour alone.
'line-dasharray': [2, 1.6],
},
},
anchor,
)
}
const retry = () => {
waiting = false
draw()
}
draw()
map.on('style.load', draw)
return () => {
map.off('style.load', draw)
map.off('idle', retry)
for (const layer of [LINE_LAYER, CASING_LAYER]) {
if (map.getLayer(layer)) map.removeLayer(layer)
}
if (map.getSource(SOURCE)) map.removeSource(SOURCE)
}
}, [map, ready, track, selectedDate, beforeId, hiddenDates])
}