mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
73237ffedb
The tour type is a dropdown; a bike tour picks its profile, routes for it, says when a point lies off every way, and shows what it runs on: surfaces, kinds of way and, for a mountain bike, the trail grades, each pointed at on the map. A type's own map (OpenHikingMap for a hike, CyclOSM for a bike tour) is offered while a tour of that type is open, with an admin switch each. From the planned cards: a hike avoids steep climbs, steps or ferries, nothing else; a saved tour can be shared where the platform takes GPX files; and the tours rail adds a tour from one menu: plan new, import GPX, or take one over from another trip.
159 lines
6.1 KiB
TypeScript
159 lines
6.1 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 { BaseMapHealth, 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 the map on terms (OpenHikingMap, CyclOSM) answers while it is the base map.
|
|
*/
|
|
export function useTourMapLayersGL(
|
|
map: RasterOverlayGlMap | null,
|
|
ferrataLayer: FerrataMapLayer | undefined,
|
|
layers: { overlays?: readonly MapRasterOverlay[]; paddle?: PaddleMapLayer; baseHealth?: BaseMapHealth } | undefined
|
|
) {
|
|
useFerrataMapLayerGL(map, ferrataLayer);
|
|
useRasterOverlaysGL(map, layers?.overlays);
|
|
usePaddleLayerGL(map as unknown as PaddleGlMap | null, layers?.paddle);
|
|
useSourceTileHealthGL(map, tourRasterSourceId(layers?.baseHealth?.base ?? 'hiking'), layers?.baseHealth);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|