mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
03d6457bde
PAD-05B: a click on an obstacle on the map or on the route band opens its card in the look of the via ferrata card: the kind with its glyph, the name, how it stands to the route (on it at a distance along it, near it with the link marked unclear, or only in the area), the paddling rule its tags give with canoe read before boat, a condition as written, height, grade, hours and fees, the source with the date of the data and the OpenStreetMap link. A missing rule reads as unknown, never as allowed, and a portage way recorded as private says so. Objects only in the area show from zoom 13 on, on Leaflet and GL alike; on GL the lines only near the route are dashed as on Leaflet, one click handler picks the marker over the line, and a picked marker no longer flickers. The route band marks the obstacles on and near the route by their share of the measured line. The obstacle notice also names an answer cut at its limit. PAD-05A: the admin area gets a Paddle water data card under the addons, on desktop and phone: a search in Geofabrik's list with each region's path, the regions with their state (waiting, reading with its progress, ready with objects and data date, failed with the reason), reading again and removing after a confirmation. The list follows a read without reloading. Region changes write audit rows. Map colours of paddle objects and portages come from theme tokens read at draw time, as the via ferratas do; the ferrata layer now shares that reader. The rail takes the planner page's parts as one bag, the map hit tests of the tour layers are one helper, and the unused noWaterData string is gone from every locale.
127 lines
4.9 KiB
TypeScript
127 lines
4.9 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 } from './glRasterLayers';
|
|
|
|
/**
|
|
* 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]);
|
|
}
|
|
|
|
/** The tour planner's layers on a GL map: the via ferratas, the raster overlays its type offers and the paddle obstacles. */
|
|
export function useTourMapLayersGL(
|
|
map: RasterOverlayGlMap | null,
|
|
ferrataLayer: FerrataMapLayer | undefined,
|
|
overlays: readonly MapRasterOverlay[] | undefined,
|
|
paddle?: PaddleMapLayer
|
|
) {
|
|
useFerrataMapLayerGL(map, ferrataLayer);
|
|
useRasterOverlaysGL(map, overlays);
|
|
usePaddleLayerGL(map as unknown as PaddleGlMap | null, paddle);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|