import type { FeatureGroup as LeafletFeatureGroup, LeafletMouseEvent } from 'leaflet'; import { useEffect, useMemo, useRef } from 'react'; import { CircleMarker, FeatureGroup, Polyline, Tooltip } from 'react-leaflet'; import type { AssistantRouteLine } from '../../types/assistantMap'; import { frameLine, frameLng, type LngFrame } from '../../utils/lngFrame'; import { onRealWorld } from '../../utils/worldCopy'; import { useLngFrame } from './lngFrameContext'; /** A rim under the line, the line, and the invisible fat line that takes the pointer. */ const CASING_WEIGHT = 7; const CORE_WEIGHT = 4; const HIT_WEIGHT = 16; /** A way not chosen, or one joined straight between its stops. */ const DASH = '6 7'; /** The way a change replaces, beside the new one. */ const DIM_OPACITY = 0.45; const END_RADIUS = 5; const STOP_RADIUS = 4; const ROUND = { lineCap: 'round', lineJoin: 'round' } as const; interface LineHandlers { onHover?: (ref: string | null) => void; onPoint?: (ref: string, lat: number, lng: number) => void; } /** * One route the chat shows: its rim and its line (dashed while it is a way * not chosen or only its stops joined straight, faint while it is the way a * change replaces), its hard grades over it, a disc at its start and a ring * at its end, its named stops with their names while it is lit, and a wide * invisible line on top that reports the pointer: onto it, along it (for the route's profile) * and off it. Nothing but the invisible line takes the pointer, so the plan's * own lines and pins stay where a click lands. Leaflet keeps each path where * it was first drawn, whatever order the lines come in later, so a line that * lights up is brought to the front by hand. */ function GhostLine({ line, onHover, onPoint }: { line: AssistantRouteLine } & LineHandlers) { const group = useRef(null); useEffect(() => { if (line.hot) group.current?.bringToFront(); }, [line.hot]); const opacity = line.dim ? DIM_OPACITY : 1; const point = (event: LeafletMouseEvent) => { const at = onRealWorld(event.latlng); onPoint?.(line.ref, at.lat, at.lng); }; return ( {line.tints?.map((tint, index) => ( ))} {line.ends && ( )} {line.ends?.end && ( )} {line.hot && line.stops?.map((stop, index) => ( {stop.label} ))} onHover?.(line.ref), mouseout: () => onHover?.(null), mousemove: point, click: point, }} /> ); } interface AssistantGhostLineLayerProps extends LineHandlers { lines: readonly AssistantRouteLine[] | undefined; } /** * The routes the chat shows on the Leaflet map, in the order they come * (the store's route model puts the one under the pointer last; here a line * brings itself to the front when it lights up). Over the plan's lines, * under its pins. The twin of * useAssistantGhostLinesGL. */ /** A line drawn in the map's longitude frame, with the trip it proposes changes to (#2975). */ function inFrame(frame: LngFrame, line: AssistantRouteLine): AssistantRouteLine { const at = ([lat, lng]: [number, number]): [number, number] => [lat, frameLng(frame, lng)]; return { ...line, lines: line.lines.map((part) => frameLine(frame, part)), ends: line.ends && { start: at(line.ends.start), end: line.ends.end && at(line.ends.end) }, stops: line.stops?.map((stop) => ({ ...stop, lng: frameLng(frame, stop.lng) })), tints: line.tints?.map((tint) => ({ ...tint, line: frameLine(frame, tint.line) })), }; } export function AssistantGhostLineLayer({ lines, onHover, onPoint }: AssistantGhostLineLayerProps) { const frame = useLngFrame(); const drawn = useMemo(() => (frame && lines ? lines.map((line) => inFrame(frame, line)) : lines), [frame, lines]); if (!drawn?.length) return null; return ( <> {drawn.map((line) => ( ))} ); }