mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
51af575c9a
A right-click, click or drag on a repeated world copy reported the longitude it has on that copy (274.9 for -85.1). The place was saved there, the geocoder found nothing, and a route to a place on the real world ran across the whole map (#2976). Longitudes are now brought back onto [-180, 180] where the map hands them out (Leaflet and GL), where they are stored (places, booking endpoints, collection places, so REST, MCP, plugin RPC and importers alike) and before reverse geocoding. A migration moves the ones already stored. Leaflet draws a layer at its literal longitude only, so a trip from Los Angeles to Auckland had its pins at opposite ends of the world while the flight ran off the edge to the next copy (#2975). A trip that crosses the antimeridian is now drawn in one longitude frame, seamed in the widest stretch it leaves empty, so pins, routes, flights and every overlay lie on one copy and the map opens across the Pacific. Every other trip has no frame and draws exactly as before. The GL day fit also takes the short way. A new install now starts with Journey and Collections switched on, next to the trip addons (all but Road trip), Atlas, Vacay and the Naver import that already were. Existing installs keep their addons as they are. Closes #2975 Closes #2976
155 lines
5.7 KiB
TypeScript
155 lines
5.7 KiB
TypeScript
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<LeafletFeatureGroup | null>(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 (
|
|
<FeatureGroup ref={group}>
|
|
<Polyline
|
|
positions={line.lines}
|
|
interactive={false}
|
|
pathOptions={{ color: line.casing, weight: CASING_WEIGHT, opacity: opacity * 0.9, ...ROUND }}
|
|
/>
|
|
<Polyline
|
|
positions={line.lines}
|
|
interactive={false}
|
|
pathOptions={{
|
|
color: line.color,
|
|
weight: CORE_WEIGHT,
|
|
opacity,
|
|
// Always named: Leaflet merges a new style into the old one, so a dash left out would stay drawn.
|
|
dashArray: line.dashed ? DASH : undefined,
|
|
...ROUND,
|
|
}}
|
|
/>
|
|
{line.tints?.map((tint, index) => (
|
|
<Polyline
|
|
key={index}
|
|
positions={tint.line}
|
|
interactive={false}
|
|
pathOptions={{ color: tint.color, weight: CORE_WEIGHT, opacity, ...ROUND }}
|
|
/>
|
|
))}
|
|
{line.ends && (
|
|
<CircleMarker
|
|
center={line.ends.start}
|
|
radius={END_RADIUS}
|
|
interactive={false}
|
|
pathOptions={{ color: line.casing, weight: 2, fillColor: line.color, fillOpacity: 1, opacity }}
|
|
/>
|
|
)}
|
|
{line.ends?.end && (
|
|
<CircleMarker
|
|
center={line.ends.end}
|
|
radius={END_RADIUS}
|
|
interactive={false}
|
|
pathOptions={{ color: line.color, weight: 3, fillColor: line.casing, fillOpacity: 1, opacity }}
|
|
/>
|
|
)}
|
|
{line.hot &&
|
|
line.stops?.map((stop, index) => (
|
|
<CircleMarker
|
|
key={index}
|
|
center={[stop.lat, stop.lng]}
|
|
radius={STOP_RADIUS}
|
|
interactive={false}
|
|
pathOptions={{ color: line.casing, weight: 2, fillColor: line.color, fillOpacity: 1 }}
|
|
>
|
|
<Tooltip permanent direction="top" className="map-tooltip">
|
|
{stop.label}
|
|
</Tooltip>
|
|
</CircleMarker>
|
|
))}
|
|
<Polyline
|
|
positions={line.lines}
|
|
bubblingMouseEvents={false}
|
|
pathOptions={{ color: line.casing, weight: HIT_WEIGHT, opacity: 0 }}
|
|
eventHandlers={{
|
|
mouseover: () => onHover?.(line.ref),
|
|
mouseout: () => onHover?.(null),
|
|
mousemove: point,
|
|
click: point,
|
|
}}
|
|
/>
|
|
</FeatureGroup>
|
|
);
|
|
}
|
|
|
|
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) => (
|
|
<GhostLine key={line.id} line={line} onHover={onHover} onPoint={onPoint} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|