Files
TREK/client/src/components/Map/mapViewContract.ts
T
Maurice 9494521ac7 feat(tours): plan paddle tours, hand-drawn legs, reverse and return to start, maps per tour type
- Paddle as a plannable tour type: one boat (kayak, canoe, SUP, other), river or
  lake, a pace that a boat change never overwrites; time is distance over pace
- Hand-drawn legs beside routed ones, drawn dotted and labelled as not routed,
  kept through save, reopen and edits elsewhere
- Reverse a tour and close it back to its start, each one undo step
- GPX import shows a preview first (line, measures, duplicates, type) and only
  the confirmation stores the tour
- Base maps and overlays per tour type in the planner: hike keeps topo and the
  via ferrata layer, paddle gets the OpenSeaMap seamark overlay
2026-10-16 23:14:52 +02:00

199 lines
9.2 KiB
TypeScript

import type { DawarichTrack, RoadtripHazard, RoadtripVia } from '@trek/shared';
import type { Day, Place, Reservation, RouteVia } from '../../types';
import type { ViewportPadding } from '../../utils/mapViewport';
import type { RouteProfileFocus } from '../../utils/routeGeometry';
import type { FerrataMapLayer } from '../Ferratas/useFerrataMapLayer';
import type { AlternativeOverlay } from '../Roadtrip/alternativeOverlays';
import type { DayBoundaryControls } from './dayBoundaryDrag';
import type { CompassMap } from './MapCompassPill';
import type { MapPlaceTool, TourBaseLayer, TourLayerOffer } from './MapLayerSwitcher';
import type { Poi } from './poiCategories';
/*
* The props both trip map renderers take: the Leaflet MapView and the GL
* MapViewGL (Mapbox or MapLibre), between which MapViewAuto picks at runtime.
*
* Callers talk to MapViewAuto, so one prop type has to describe what either
* renderer does with it; before this file each side read `any`, and a prop one
* renderer learned and the other never heard of went unnoticed. Both renderers
* now declare `satisfies ComponentType<...>` against this type.
*
* Types only, on purpose: this module must stay free of leaflet, mapbox-gl and
* maplibre-gl (`check:gl-split`), so every import here is `import type`.
*/
/**
* A pin as the callers of MapViewAuto hand it down. Besides trip places they
* pass the places inside a day's assignments and collection places, and those
* carry no trip_id. Neither renderer reads trip_id, but both type their pins
* as Place, which requires it; see MapViewAutoProps.
*/
export type MapPlace = Omit<Place, 'trip_id'> & { trip_id?: number };
/** A leg label between two consecutive stops: where it sits and what it says. */
export interface RouteSegment {
mid: [number, number];
from: [number, number];
to: [number, number];
walkingText?: string;
drivingText?: string;
}
export interface MapViewProps {
places?: Place[];
dayPlaces?: Place[];
// Enables the plugin map contributions (markers + layers). Absent on surfaces
// without a trip (CollectionMap), which naturally excludes them — same rule as
// the Leaflet MapPluginMarkers.
tripId?: number;
// Charging stops / rest areas a plugin route places on the drawn day route.
routeVias?: RouteVia[];
dayBoundaryControls?: DayBoundaryControls;
/** The dashed last bit to a place the road network does not reach. */
accessLines?: { line: [[number, number], [number, number]]; meters: number }[];
route?: [number, number][][] | null;
/**
* One colour pair per entry of `route`, or absent for the blue the route has always
* been. Only the road trip passes these, and only while colouring by day is on.
*/
routeColors?: ({ line: string; casing: string } | undefined)[] | null;
/** One flag per entry of `route`: true where the stretch is walked, drawn dashed (#2532). */
routeWalking?: boolean[] | null;
/** False while the map is locked (#2010): picking a place leaves the view alone. */
followSelection?: boolean;
/** Given, the lock that sets followSelection sits above the layer switcher. */
onToggleFollow?: () => void;
routeSegments?: RouteSegment[];
selectedPlaceId?: number | null;
/** The selected place itself, for when no pin on this map stands for it. */
selectedPlace?: Place | null;
onMarkerClick?: (id: number) => void;
hoverDisabled?: boolean;
onMapClick?: (info: { latlng: { lat: number; lng: number } }) => void;
onMapContextMenu?:
((e: { latlng: { lat: number; lng: number }; originalEvent: MouseEvent | TouchEvent }) => void) | null;
center?: [number, number];
zoom?: number;
fitKey?: number | null;
dayOrderMap?: Record<number, number[] | null>;
leftWidth?: number;
rightWidth?: number;
hasInspector?: boolean;
hasDayDetail?: boolean;
reservations?: Reservation[];
visibleConnectionIds?: number[];
showTransitRoutes?: boolean;
days?: Day[];
selectedDayId?: number | null;
/**
* Whether a booking switched on by hand also has to run on the selected day to be
* drawn. Only the phone's plan map asks for it; see RouteVisibilityOptions.
*/
scopeConnectionsToDay?: boolean;
showReservationStats?: boolean;
onReservationClick?: (reservationId: number) => void;
pois?: Poi[];
onPoiClick?: (poi: Poi) => void;
/**
* A corridor hit dropped somewhere on the map, with the coordinate it landed on.
* The caller decides whether that point is near enough to the drive to mean anything.
*/
onPoiDropOnRoute?: (osmId: string, lat: number, lng: number) => void;
/** A click on the drawn route, for putting a via point there (#1797). */
onRouteClick?: (lat: number, lng: number) => void;
/** The ways of driving one leg, drawn while the picker is open. */
alternativeRoutes?: AlternativeOverlay[];
/** Which option is being considered, so it can be lit up in its own colour. */
activeAlternative?: number | null;
onChooseAlternative?: (index: number) => void;
/** Reports which option the pointer is over, so the list and the map agree. */
onHighlightAlternative?: (index: number | null) => void;
/** Generic numbered control points for list-first route editors. */
/** `name` and `detail` (its role) show on hover; without a name the pin is named by its number. */
plannerWaypoints?: Array<{ id: string; lat: number; lng: number; name?: string; detail?: string }>;
selectedPlannerWaypointId?: string | null;
onPlannerWaypointClick?: (id: string) => void;
/** Drops a dragged control point; returns false when the move was refused. */
onPlannerWaypointMove?: (id: string, lat: number, lng: number) => boolean;
routeProfileFocus?: RouteProfileFocus | null;
viewBaseLayer?: TourBaseLayer;
onViewBaseLayerChange?: (layer: TourBaseLayer) => void;
/** Offers the 3D terrain switch in the tour layer pill. */
onToggleTerrain3d?: () => void;
/** Offers the "set points" switch in the tour layer pill, while a tour is being edited. */
placeTool?: MapPlaceTool;
/**
* The via ferrata layer (RC-10): its switch, with the count as a badge, joins
* the tour layer pill, and while it is on its lines are drawn and a click on
* one hands the ferrata to `onSelect`. Both renderers mount it through the
* Ferratas modules.
*/
ferrataLayer?: FerrataMapLayer;
/**
* What the tour's type offers in the tour layer pill (P2B-06): its base layers,
* and transparent overlays drawn over the basemap and under the route.
*/
tourLayers?: TourLayerOffer;
/**
* An explicit stretch of map to frame, independent of the day being shown.
*
* `fitKey` cannot express this: it carries no coordinates, and each renderer decides
* for itself that it means "the selected day". Weighing the ways of driving one leg
* needs that leg on screen, which is neither the day nor the trip.
*/
focusPoints?: [number, number][];
/** Changes only when the caller intentionally wants a new initial frame. */
focusKey?: number | string;
/**
* What the caller's own chrome covers while `focusPoints` is framed, in pixels per edge.
*
* The default padding knows this component's panels and nothing else, and on a phone it
* is a flat margin. A shell that lays its own bars over the map passes what they cover,
* so the frame lands in the part still visible. Only the fit on `focusPoints` reads it.
* Compared by value: the same numbers in a new object do not refit, while new numbers
* refit the points already handed over, because the chrome they must clear has moved.
*/
fitPadding?: ViewportPadding;
/**
* Let markers stay apart longer than usual.
*
* A road trip is read along a line: two stops fifty kilometres apart on the same
* motorway are the shape of the day, and merging them into one dot hides it.
*/
clusterLoosely?: boolean;
hazards?: RoadtripHazard[];
/** The route recorded in Dawarich, already fetched by MapViewAuto (#2279). */
dawarichTrack?: DawarichTrack | null;
/** Draw only this local day of the recording. */
dawarichSelectedDate?: string | null;
/** Local dates whose day is collapsed in the day plan; their recording is not drawn. */
dawarichHiddenDates?: ReadonlySet<string> | null;
/** Via points to draw as draggable handles, keyed by day (#1797). */
roadtripVias?: Record<number, RoadtripVia[]>;
onMoveVia?: (dayId: number, id: number, lat: number, lng: number) => void;
onRemoveVia?: (dayId: number, id: number) => void;
onViewportChange?: (bbox: { south: number; west: number; north: number; east: number }) => void;
/**
* The raster tile URL template. Only the Leaflet renderer draws raster tiles;
* the GL renderer draws the style of the chosen provider and ignores it.
*/
tileUrl?: string;
/**
* Hands the caller the live map once it is ready. Only the GL renderer calls
* it; the Leaflet renderer takes the prop and never calls it.
*/
onMapReady?: (map: CompassMap | null) => void;
}
/**
* What MapViewAuto accepts: the renderer contract with pins as callers really
* pass them (MapPlace, possibly without trip_id). The gap between this and
* MapViewProps is known drift between the callers and the renderers' Place
* type; it is bridged in one place, in MapViewAuto, until the renderers type
* their pins by what they read.
*/
export type MapViewAutoProps = Omit<MapViewProps, 'places' | 'dayPlaces'> & {
places?: MapPlace[];
dayPlaces?: MapPlace[];
};