mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
06e6e25b25
While a hike is planned, the planner's map switch offers "Hiking" beside the map, topo and satellite: OpenHikingMap, with marked trails over the terrain. It is a choice of the planner's view alone, like topo: the global map setting, the tour and its days stay as they are. With no tour open, over an import viewed as it came in and for a paddle tour it is not offered, and a choice made earlier falls back to the standard map there. openmaps.fr's tile usage policy is followed: tiles load only as the map shows them, with the browser's Referer, from the server's own caching; no service worker rule keeps them and the 3D flyover's warm-up passes their source by. Its credit and donation link are on the map; the 3D map, whose attribution is folded away under the panel, shows them as a line of its own above its controls. The service is best effort. A run of failed tiles while it is shown marks the switch, with a tooltip and a status for screen readers that say it is not answering and that the tour is untouched; one tile arriving clears it. The tile health the seamark overlay had is now useTileHealth, shared by both. The source is free for non-commercial installs up to 500,000 tiles a month. tours_hiking_map_enabled is an admin switch under Tours (admin tours-hiking-map, audited), on by default, for an install that is commercial or larger to switch off until it has an agreement with the operator. /api/addons carries it as toursHikingMap, true only while Tours is on. Leaflet, GL and the 3D map read one table of the planner's raster bases (tourRasterBases), so topo and the hiking map are drawn the same way. tile.openmaps.fr is on the CSP's connect-src for the GL maps.
159 lines
6.0 KiB
TypeScript
159 lines
6.0 KiB
TypeScript
import type { Map } from 'mapbox-gl';
|
|
import { useEffect } from 'react';
|
|
import { isFerrataHit, useFerrataMapLayerGL } from '../Ferratas/useFerrataLayerGL';
|
|
import type { FerrataMapLayer } from '../Ferratas/useFerrataMapLayer';
|
|
import type { PaddleMapLayer } from '../Tours/paddle/paddleMapLayer';
|
|
import { isPaddleHit, type PaddleGlMap, usePaddleLayerGL } from '../Tours/paddle/usePaddleLayerGL';
|
|
import { OWN_LAYER_PREFIXES, tourRasterSourceId } from './glRasterLayers';
|
|
import type { TileHealth } from './tourRasterBases';
|
|
|
|
/**
|
|
* A transparent raster layer laid over the basemap and under the route, such
|
|
* as OpenSeaMap's seamarks over a paddle tour. Only the tiles in view are
|
|
* fetched while it is on: nothing is prefetched or stored for offline use, and
|
|
* the browser's own cache follows the answer's headers.
|
|
*/
|
|
export interface MapRasterOverlay {
|
|
id: string;
|
|
url: string;
|
|
attribution: string;
|
|
minZoom: number;
|
|
maxZoom: number;
|
|
active: boolean;
|
|
/** A tile failed to load; the layer's owner decides when that means the source is down. */
|
|
onTileError: () => void;
|
|
/** A tile arrived; the source answers again. */
|
|
onTileLoad: () => void;
|
|
}
|
|
|
|
type RasterOverlayStatus = 'off' | 'on' | 'down';
|
|
|
|
/** An overlay with its switch: whether it is on, and whether its source stopped answering. */
|
|
export interface ToggleableRasterOverlay extends MapRasterOverlay {
|
|
status: RasterOverlayStatus;
|
|
onToggle: () => void;
|
|
}
|
|
|
|
/** One map type for both engines, as the ferrata layer does: MapLibre answers every call made here. */
|
|
export type RasterOverlayGlMap = Map;
|
|
|
|
/**
|
|
* The layer id. It carries TREK's own prefix, so a basemap raster added later
|
|
* (satellite, topo) is put under it, and it goes in under the first other own
|
|
* layer, so the route and its markers stay on top.
|
|
*/
|
|
function overlayLayerId(id: string): string {
|
|
return `trek-overlay-${id}`;
|
|
}
|
|
|
|
function applyOverlay(map: RasterOverlayGlMap, overlay: MapRasterOverlay): void {
|
|
const layerId = overlayLayerId(overlay.id);
|
|
try {
|
|
if (map.getLayer(layerId)) {
|
|
map.setLayoutProperty(layerId, 'visibility', overlay.active ? 'visible' : 'none');
|
|
return;
|
|
}
|
|
if (!overlay.active) return;
|
|
if (!map.getSource(layerId)) {
|
|
map.addSource(layerId, {
|
|
type: 'raster',
|
|
tiles: [overlay.url],
|
|
tileSize: 256,
|
|
minzoom: overlay.minZoom,
|
|
maxzoom: overlay.maxZoom,
|
|
attribution: overlay.attribution,
|
|
});
|
|
}
|
|
const firstOwn = (map.getStyle()?.layers ?? []).find(
|
|
(layer) =>
|
|
OWN_LAYER_PREFIXES.some((prefix) => layer.id.startsWith(prefix)) && !layer.id.startsWith('trek-overlay-')
|
|
);
|
|
map.addLayer({ id: layerId, type: 'raster', source: layerId, paint: { 'raster-opacity': 1 } }, firstOwn?.id);
|
|
} catch {
|
|
// A style that refuses the layer keeps the plain map; the switch reports nothing loaded.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The overlays on a GL map, kept through a basemap change (which rebuilds the
|
|
* style and drops them) and reporting their tiles' arrivals and failures.
|
|
*/
|
|
export function useRasterOverlaysGL(map: RasterOverlayGlMap | null, overlays: readonly MapRasterOverlay[] | undefined) {
|
|
useEffect(() => {
|
|
if (!map || !overlays?.length) return;
|
|
const apply = () => overlays.forEach((overlay) => applyOverlay(map, overlay));
|
|
const sourceOf = (event: { sourceId?: string }) =>
|
|
overlays.find((overlay) => overlayLayerId(overlay.id) === event.sourceId);
|
|
// A tile's failure carries the source it came from; other errors carry none and are not ours.
|
|
const failed = (event: { error?: Error; sourceId?: string }) => {
|
|
const overlay = sourceOf(event);
|
|
if (overlay?.active) overlay.onTileError();
|
|
};
|
|
const loaded = (event: { sourceId?: string; tile?: unknown }) => {
|
|
const overlay = sourceOf(event);
|
|
if (overlay?.active && event.tile) overlay.onTileLoad();
|
|
};
|
|
apply();
|
|
map.on('styledata', apply);
|
|
map.on('error', failed);
|
|
map.on('sourcedata', loaded);
|
|
return () => {
|
|
map.off('styledata', apply);
|
|
map.off('error', failed);
|
|
map.off('sourcedata', loaded);
|
|
};
|
|
}, [map, overlays]);
|
|
}
|
|
|
|
/**
|
|
* Reports the tiles of one GL source into its health while `health` is given:
|
|
* a failure carries the source it came from, an arrival a tile. The base layer
|
|
* itself is drawn by applyBaseLayer.
|
|
*/
|
|
export function useSourceTileHealthGL(
|
|
map: RasterOverlayGlMap | null,
|
|
sourceId: string,
|
|
health: TileHealth | undefined
|
|
) {
|
|
const { onTileError, onTileLoad } = health ?? {};
|
|
useEffect(() => {
|
|
if (!map || !onTileError || !onTileLoad) return;
|
|
const failed = (event: { error?: Error; sourceId?: string }) => {
|
|
if (event.sourceId === sourceId) onTileError();
|
|
};
|
|
const loaded = (event: { sourceId?: string; tile?: unknown }) => {
|
|
if (event.sourceId === sourceId && event.tile) onTileLoad();
|
|
};
|
|
map.on('error', failed);
|
|
map.on('sourcedata', loaded);
|
|
return () => {
|
|
map.off('error', failed);
|
|
map.off('sourcedata', loaded);
|
|
};
|
|
}, [map, sourceId, onTileError, onTileLoad]);
|
|
}
|
|
|
|
/**
|
|
* The tour planner's layers on a GL map: the via ferratas, the raster overlays its type offers,
|
|
* the paddle obstacles, and whether OpenHikingMap answers while it is the base map.
|
|
*/
|
|
export function useTourMapLayersGL(
|
|
map: RasterOverlayGlMap | null,
|
|
ferrataLayer: FerrataMapLayer | undefined,
|
|
layers: { overlays?: readonly MapRasterOverlay[]; paddle?: PaddleMapLayer; hikingMap?: TileHealth } | undefined
|
|
) {
|
|
useFerrataMapLayerGL(map, ferrataLayer);
|
|
useRasterOverlaysGL(map, layers?.overlays);
|
|
usePaddleLayerGL(map as unknown as PaddleGlMap | null, layers?.paddle);
|
|
useSourceTileHealthGL(map, tourRasterSourceId('hiking'), layers?.hikingMap);
|
|
}
|
|
|
|
/**
|
|
* Whether a click at `point` landed on an object of the tour layers, a via
|
|
* ferrata or a paddle obstacle. The maps ask this before their own click
|
|
* handler, so picking one opens its card instead of also setting a waypoint.
|
|
*/
|
|
export function isTourLayerHit<Point>(map: Parameters<typeof isFerrataHit<Point>>[0], point: Point): boolean {
|
|
return isFerrataHit(map, point) || isPaddleHit(map, point);
|
|
}
|