Files
TREK/client/src/components/Map/glRasterLayers.ts
T
Maurice 008aaeb408 fix(tours): say to zoom in when the pistes are on further out than they draw
The pistes overlay has no tiles below zoom 9, so switching it on at a region
or country view left the map empty while the switch looked on, which read as
broken. Both maps now report their zoom with the viewport, on the 256 px tile
scale raster sources count in (a GL map's own zoom is one step lower there),
and the planner hands it to its overlays. While the pistes are on and the map
is further out than their first tiles, the switch stays pressed in the soft
accent, its tooltip and the key's first line say to zoom in, and the screen
reader hears it once. An unknown zoom claims nothing.
2026-10-20 15:06:05 +02:00

107 lines
5.1 KiB
TypeScript

import { SATELLITE_TILE_ATTRIBUTION, SATELLITE_TILE_MAXZOOM, SATELLITE_TILE_URL } from '../../constants/mapDefaults';
import { TOUR_RASTER_BASE_IDS, TOUR_RASTER_BASES, type TourRasterBase } from './tourRasterBases';
/*
* The raster layers a GL map draws between its basemap style and what TREK
* adds on top: the satellite, topo and hiking base layers, and (in mapRasterOverlay.ts)
* the transparent overlays such as the seamarks.
*/
/**
* The satellite base layer, the GL twin of the Leaflet one.
*
* Leaflet swaps its whole tile layer for the imagery; a GL map cannot, because the
* basemap is a style with dozens of layers in it. So the imagery goes on as a raster
* layer of its own, drawn over the style and under everything TREK adds — the route,
* the pins and the tracks stay on top and stay legible, which is the same order the
* Leaflet map ends up with.
*
* Same tokenless ESRI source as Leaflet, so the two look alike and neither needs a key.
*/
const SATELLITE_SOURCE_ID = 'trip-satellite';
const SATELLITE_LAYER_ID = 'trip-satellite-raster';
/** Everything TREK draws is prefixed; the imagery is inserted before the first of them. */
// Everything TREK draws on a GL map, so the satellite raster can be slipped underneath all
// of it. `route-alt-` earns its place here: the roads offered for a leg are added before the
// route source so they sit under the current route, which also puts them before the first
// `trip-` layer, and anchoring the imagery there painted right over them.
export const OWN_LAYER_PREFIXES = ['trip-', 'trek-', 'route-alt-'];
/**
* A GL map's zoom on the 256 px tile scale that raster tile URLs, their
* min and max zoom, and Leaflet count in. The GL engines count on 512 px
* tiles, so the same view is one step further in on that scale, and a 256 px
* raster source is drawn from its tiles one level up.
*/
export function rasterTileZoom(glZoom: number): number {
return glZoom + 1;
}
/**
* Puts the imagery on the map, or takes it off again.
*
* Built on demand rather than once at load: a style change drops every source the map
* had, and a hot reload does the same. Visibility comes first: a layer that is already
* there only needs flipping, so the usual pass is one call and a tap answers at once.
*
* Deliberately not gated on `isStyleLoaded()`. That reports false for as long as any
* source has tiles or a setData in flight (see useDawarichTrailGL), which on a phone is
* the usual state, and the only retry here is `styledata`, which fires on style edits
* and not when those finish. Behind that gate a stored choice met a busy map on load,
* and a tap on the road trip stage met the sources the same render had just set again,
* so the imagery was never drawn. The gate was also the wrong question: this only runs
* after `load`, and addSource and addLayer refuse only a style document that is not in
* yet. The catch covers that case, and `styledata` brings the pass back once it is in.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function applySatellite(map: any, on: boolean, raster?: TourRasterBase): void {
const layerId = raster ? `trip-${raster}-raster` : SATELLITE_LAYER_ID;
const sourceId = raster ? tourRasterSourceId(raster) : SATELLITE_SOURCE_ID;
const source = raster ? TOUR_RASTER_BASES[raster] : null;
try {
if (map.getLayer(layerId)) {
map.setLayoutProperty(layerId, 'visibility', on ? 'visible' : 'none');
return;
}
if (!on) return; // Nothing to build while it is switched off.
// The layer is what is missing, not necessarily the source: a pass that got the
// source in and then failed on the layer would otherwise leave a source that stops
// every later pass from ever building the layer.
if (!map.getSource(sourceId)) {
map.addSource(sourceId, {
type: 'raster',
tiles: source ? source.tiles : [SATELLITE_TILE_URL],
tileSize: 256,
maxzoom: source ? source.maxZoom : SATELLITE_TILE_MAXZOOM,
attribution: source ? source.attribution : SATELLITE_TILE_ATTRIBUTION,
});
}
// Under the first thing TREK draws, over everything the basemap style draws.
// Without the anchor the imagery lands on top and buries the route.
const layers = map.getStyle?.()?.layers ?? [];
const firstOwn = layers.find((l: { id: string }) => OWN_LAYER_PREFIXES.some((prefix) => l.id.startsWith(prefix)));
map.addLayer(
{
id: layerId,
type: 'raster',
source: sourceId,
paint: { 'raster-opacity': 1 },
},
firstOwn?.id
);
} catch {
/* a style that refuses the layer keeps the plain basemap */
}
}
/** The GL source a Tours raster base map is drawn from, which its tile events name. */
export function tourRasterSourceId(raster: TourRasterBase): string {
return `trip-${raster}`;
}
/** The base layer on a GL map: the imagery, a Tours raster map, or neither over the user's own style. */
export function applyBaseLayer(map: Parameters<typeof applySatellite>[0], baseLayer: string): void {
applySatellite(map, baseLayer === 'satellite');
for (const raster of TOUR_RASTER_BASE_IDS) applySatellite(map, baseLayer === raster, raster);
}