Files
TREK/client/src/components/Tours/tourMapLayers.ts
T
Maurice c83eb42730 feat(tours): plan a run at your own pace
RUN-01. Run joins the tour types in the planner, with Running as its first
profile, chosen on purpose like a bike tour's. A run is routed on foot for
runnable, mostly paved ways: tracks and ferries only where there is no other
way (use_tracks 0, use_ferry 0, measured against the public Valhalla), no
hiking limit and no via ferrata. The runner types a pace in min/km; the
running time is the distance at that pace and nothing else, shown under the
field as badges (8 km x 6:00 /km = 48 min). TREK suggests no pace: without
one the time stays unknown and Save waits, text that is no pace or a pace
outside 2:00 to 20:00 is marked. The info dot, the overview and the head say
climbs, descents and difficulty are not in the time, and the time reads as
planned everywhere, so it never passes for a recorded one; the heights are
shown as for any tour on land.

Type, profile, pace and route come back on opening, survive a draft reload,
an undo of the type, a copy and a take-over; switching to another type drops
profile and pace. Day assignment, the 3D terrain view (with topo and
satellite, no ferratas) and a routing failure keeping the last route work as
for every tour. A GPX file can be imported as a run with its profile and no
pace, and planned over as a run. Runs export to GPX as running and get their
own places along the way (water, toilets, parks, viewpoints).

The server keeps tours.run_profile (CHECK list, already holding trail for
RUN-02, since SQLite cannot widen a CHECK in place) and
tours.run_pace_seconds_per_km (120 to 1200), switches the run type on,
refuses a run without a profile and run fields on any other type, and
list_tours names both with what the planned time does not include.
2026-10-20 15:05:38 +02:00

80 lines
3.9 KiB
TypeScript

import type { TourTypeKey } from '@trek/shared';
import type { TourBaseLayer } from '../Map/MapLayerSwitcher';
/**
* P2B-06: which maps the tour planner offers for each tour type. The planner
* reads this one table; the global map setting is never touched, and a choice
* made here lasts only as long as the planner keeps it.
*
* A base layer is the map itself (one at a time). An overlay is information
* laid over it and switched on its own; it is off until the user turns it on.
* Only sources TREK already ships or that a card cleared are listed: OpenTopoMap
* and the Esri imagery the planner had, OpenHikingMap (MAP-01), the via ferrata
* layer (RC-10) and OpenSeaMap's seamarks (PAD-10).
*
* - Hike: the standard map first, as before; topo, OpenHikingMap (MAP-01, while the admin
* allows it) and satellite beside it; via ferratas.
* - Bike (BIK-01): the standard map, topo for the climbs, CyclOSM (BIK-06, while the admin
* allows it) and satellite. No ferratas, a bike tour climbs none.
* - Run (RUN-01): the standard map, topo for the climbs and satellite. No ferratas, a run
* takes none.
* - Paddle: the standard map, whose water reads clearly, and satellite. No topo and
* no ferratas, those are hiking maps. Seamarks as an overlay.
* - Any other type (switched off, or one this build does not plan): the standard map
* and satellite, no overlays.
*
* If an overlay fails, the base map and the planning go on; the switch says so.
*/
type TourOverlayId = 'ferratas' | 'seamarks';
export interface TourMapLayerSet {
bases: readonly TourBaseLayer[];
defaultBase: TourBaseLayer;
overlays: readonly TourOverlayId[];
}
const HIKE: TourMapLayerSet = {
bases: ['default', 'topo', 'satellite'],
defaultBase: 'default',
overlays: ['ferratas'],
};
const HIKE_WITH_HIKING_MAP: TourMapLayerSet = { ...HIKE, bases: ['default', 'topo', 'hiking', 'satellite'] };
const BIKE: TourMapLayerSet = { bases: ['default', 'topo', 'satellite'], defaultBase: 'default', overlays: [] };
const BIKE_WITH_CYCLE_MAP: TourMapLayerSet = { ...BIKE, bases: ['default', 'topo', 'cycle', 'satellite'] };
const RUN: TourMapLayerSet = { bases: ['default', 'topo', 'satellite'], defaultBase: 'default', overlays: [] };
const PADDLE: TourMapLayerSet = { bases: ['default', 'satellite'], defaultBase: 'default', overlays: ['seamarks'] };
const OTHER: TourMapLayerSet = { bases: ['default', 'satellite'], defaultBase: 'default', overlays: [] };
/** `hikingMap`, `cycleMap`: the admin allows OpenHikingMap (MAP-01) and CyclOSM (BIK-06) on this install. */
export function tourMapLayersFor(
type: TourTypeKey | null | undefined,
{ hikingMap = false, cycleMap = false }: { hikingMap?: boolean; cycleMap?: boolean } = {}
): TourMapLayerSet {
if (type === 'hike') return hikingMap ? HIKE_WITH_HIKING_MAP : HIKE;
if (type === 'bike') return cycleMap ? BIKE_WITH_CYCLE_MAP : BIKE;
if (type === 'run') return RUN;
if (type === 'kayak') return PADDLE;
return OTHER;
}
/** The base layer to show: the one chosen while this type offers it, else the type's own default. */
export function baseLayerFor(set: TourMapLayerSet, chosen: TourBaseLayer): TourBaseLayer {
return set.bases.includes(chosen) ? chosen : set.defaultBase;
}
/**
* OpenSeaMap's transparent seamark layer, as its embedding guide names it:
* HTTPS, the seamark path only, no basemap of theirs. Tiles CC-BY-SA 2.0, data
* ODbL. Checked on 16.10.2026: from zoom 6 to 18 every tile answers 200 with a
* PNG (an empty one is transparent), CORS open, no Cache-Control; so a failed
* tile means the service is unreachable, not that the water has no seamarks.
*/
export const SEAMARK_OVERLAY = {
id: 'seamarks',
url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png',
attribution:
'Seamarks &copy; <a href="https://www.openseamap.org">OpenSeaMap</a> contributors (<a href="https://creativecommons.org/licenses/by-sa/2.0/">CC BY-SA 2.0</a>)',
minZoom: 6,
maxZoom: 18,
} as const;