mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +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
70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
// Great-circle geometry for transport routes (flights, cruises, ferries),
|
|
// shared by the Leaflet and Mapbox/MapLibre renderers (#1411).
|
|
|
|
import { frameLine, type LngFrame } from '../../utils/lngFrame'
|
|
|
|
const toRad = (d: number) => d * Math.PI / 180
|
|
const toDeg = (r: number) => r * 180 / Math.PI
|
|
|
|
export function greatCircle(a: [number, number], b: [number, number], steps = 256): [number, number][] {
|
|
const [lat1, lng1] = [toRad(a[0]), toRad(a[1])]
|
|
const [lat2, lng2] = [toRad(b[0]), toRad(b[1])]
|
|
const d = 2 * Math.asin(Math.sqrt(Math.sin((lat2 - lat1) / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin((lng2 - lng1) / 2) ** 2))
|
|
if (d === 0) return [a, b]
|
|
const pts: [number, number][] = []
|
|
for (let i = 0; i <= steps; i++) {
|
|
const f = i / steps
|
|
const A = Math.sin((1 - f) * d) / Math.sin(d)
|
|
const B = Math.sin(f * d) / Math.sin(d)
|
|
const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2)
|
|
const y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2)
|
|
const z = A * Math.sin(lat1) + B * Math.sin(lat2)
|
|
const lat = Math.atan2(z, Math.sqrt(x * x + y * y))
|
|
const lng = Math.atan2(y, x)
|
|
pts.push([toDeg(lat), toDeg(lng)])
|
|
}
|
|
return pts
|
|
}
|
|
|
|
/**
|
|
* Make the longitudes of a sampled arc continuous: atan2 normalizes every
|
|
* sample to [-180, 180], so a date-line crossing shows up as a ±360 jump
|
|
* between neighbours — which the renderers draw as a line across the whole
|
|
* map. Carrying a running offset keeps each Δlng under 180°, at the cost of
|
|
* longitudes leaving the [-180, 180] range (both map libraries project those
|
|
* linearly, which is exactly what makes the wrap seamless).
|
|
*/
|
|
export function unwrapLngs(points: [number, number][]): [number, number][] {
|
|
let offset = 0
|
|
return points.map(([lat, lng], i) => {
|
|
if (i === 0) return [lat, lng] as [number, number]
|
|
const prev = points[i - 1][1] + offset
|
|
let cur = lng + offset
|
|
if (cur - prev > 180) { offset -= 360; cur -= 360 }
|
|
else if (cur - prev < -180) { offset += 360; cur += 360 }
|
|
return [lat, cur] as [number, number]
|
|
})
|
|
}
|
|
|
|
/**
|
|
* The polylines to draw for one leg. The base arc is continuous (unwrapped),
|
|
* so panning across the antimeridian shows one unbroken line. With
|
|
* `wrapCopies` (Leaflet — its vector layers don't repeat across world
|
|
* copies), a ±360-shifted duplicate keeps both halves visible in the
|
|
* standard [-180, 180] view; GL maps repeat features themselves
|
|
* (renderWorldCopies), so the duplicate would just double the line opacity.
|
|
*
|
|
* Given a longitude frame (Leaflet, a trip across the antimeridian, #2975), the
|
|
* arc starts at `a` as the frame draws it, and the duplicate is only needed
|
|
* when the arc leaves the frame rather than the plain world.
|
|
*/
|
|
export function geodesicArcs(a: [number, number], b: [number, number], wrapCopies: boolean, frame: LngFrame | null = null): [number, number][][] {
|
|
const arc = frame ? frameLine(frame, greatCircle(a, b)) : unwrapLngs(greatCircle(a, b))
|
|
const west = frame ? frame.seam : -180
|
|
const east = west + 360
|
|
const crosses = arc.some(([, lng]) => lng < west || lng > east)
|
|
if (!crosses || !wrapCopies) return [arc]
|
|
const shift = arc.some(([, lng]) => lng < west) ? 360 : -360
|
|
return [arc, arc.map(([lat, lng]) => [lat, lng + shift] as [number, number])]
|
|
}
|