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
109 lines
4.0 KiB
TypeScript
109 lines
4.0 KiB
TypeScript
// One stretch of longitudes to draw a trip in, for a map that does not repeat its
|
|
// layers across world copies (Leaflet) (#2975).
|
|
//
|
|
// Leaflet draws a marker at its literal longitude only, on one copy of the world.
|
|
// A trip from Los Angeles (-118) to Auckland (174.8) therefore had its pins at
|
|
// opposite ends of that copy, while the flight between them, drawn the short way
|
|
// across the Pacific, ran off the edge to the Los Angeles of the next copy. The
|
|
// frame moves the seam of the drawn world into the widest stretch of the planet
|
|
// the trip leaves empty, so the whole trip, flight included, lies on one copy:
|
|
// Los Angeles at 242, Auckland at 174.8.
|
|
//
|
|
// A trip that does not cross the antimeridian gets no frame at all, and every
|
|
// layer draws exactly what it drew before.
|
|
|
|
import { wrapLongitude } from '@trek/shared';
|
|
|
|
/** The drawn world runs from `seam` to `seam + 360`. */
|
|
export interface LngFrame {
|
|
readonly seam: number;
|
|
}
|
|
|
|
/** Re-seam only when that narrows the trip by at least this much: a world tour stays as it is. */
|
|
const MIN_SAVING_DEG = 45;
|
|
/** A previous seam is kept while the trip drawn with it is at most this much wider than the best. */
|
|
const KEEP_SEAM_SLACK_DEG = 15;
|
|
|
|
/** Width of the trip, in degrees, when the world is drawn from `seam` eastwards. */
|
|
function spanFrom(seam: number, sorted: number[]): number {
|
|
let min = Infinity;
|
|
let max = -Infinity;
|
|
for (const lng of sorted) {
|
|
const framed = lng < seam ? lng + 360 : lng;
|
|
if (framed < min) min = framed;
|
|
if (framed > max) max = framed;
|
|
}
|
|
return max - min;
|
|
}
|
|
|
|
/**
|
|
* The frame for these longitudes, or null when the plain world already holds the
|
|
* trip narrowly enough. `previous` is the frame in use: it is kept while it still
|
|
* works, so adding or filtering a place does not slide every layer sideways.
|
|
*/
|
|
export function tripLngFrame(lngs: readonly number[], previous: LngFrame | null = null): LngFrame | null {
|
|
const sorted = lngs
|
|
.filter(Number.isFinite)
|
|
.map(wrapLongitude)
|
|
.sort((a, b) => a - b);
|
|
if (sorted.length < 2) return null;
|
|
const last = sorted.length - 1;
|
|
|
|
// The widest stretch with no point in it. The one across the antimeridian comes first,
|
|
// so a trip that leaves the Pacific empty keeps the plain world.
|
|
let gapSize = sorted[0] + 360 - sorted[last];
|
|
let gapWest = sorted[last];
|
|
let crossesAntimeridian = false;
|
|
for (let i = 0; i < last; i++) {
|
|
const size = sorted[i + 1] - sorted[i];
|
|
if (size > gapSize) {
|
|
gapSize = size;
|
|
gapWest = sorted[i];
|
|
crossesAntimeridian = true;
|
|
}
|
|
}
|
|
if (!crossesAntimeridian) return null;
|
|
|
|
const plainSpan = sorted[last] - sorted[0];
|
|
const bestSpan = 360 - gapSize;
|
|
if (plainSpan - bestSpan < MIN_SAVING_DEG) return null;
|
|
|
|
if (previous && spanFrom(previous.seam, sorted) <= bestSpan + KEEP_SEAM_SLACK_DEG) return previous;
|
|
// The middle of the empty stretch: lines that bulge a little past a stop stay clear of it.
|
|
return { seam: gapWest + gapSize / 2 };
|
|
}
|
|
|
|
/** Where a longitude is drawn in the frame. Without a frame it is drawn where it is. */
|
|
export function frameLng(frame: LngFrame | null, lng: number): number {
|
|
if (!frame) return lng;
|
|
return frame.seam + ((((lng - frame.seam) % 360) + 360) % 360);
|
|
}
|
|
|
|
/**
|
|
* A line in the frame: its first point moved into it, every next point kept within
|
|
* half a world of the one before, so the line stays one line where it crosses the
|
|
* antimeridian or the seam. Without a frame the line is drawn as it is.
|
|
*/
|
|
export function frameLine<T extends readonly [number, number]>(
|
|
frame: LngFrame | null,
|
|
line: readonly T[]
|
|
): [number, number][] {
|
|
if (!frame) return line as unknown as [number, number][];
|
|
const out: [number, number][] = [];
|
|
let prevRaw = 0;
|
|
let prevDrawn = 0;
|
|
for (let i = 0; i < line.length; i++) {
|
|
const [lat, lng] = line[i];
|
|
if (i === 0) {
|
|
prevDrawn = frameLng(frame, lng);
|
|
} else {
|
|
let delta = lng - prevRaw;
|
|
delta -= 360 * Math.round(delta / 360);
|
|
prevDrawn += delta;
|
|
}
|
|
prevRaw = lng;
|
|
out.push([lat, prevDrawn]);
|
|
}
|
|
return out;
|
|
}
|