lines that cross the pacific
why a tokyo to los angeles route can draw the long way round the world in mapbox, and how to split it at 180° without getting the crossing wrong.
a flight from tokyo to los angeles crosses the pacific. put those two points on a web map, though, and the line can go the other way, all the way round the world.
that’s because a map draws a straight segment between consecutive coordinates, in plain longitude and latitude. tokyo is at 139.7° and los angeles is at −118.2°. the straight segment between them heads west over asia, europe and the atlantic, covering 258° of longitude instead of the 102° across the pacific.
google maps hides this from you, because its polylines take the short way across the 180° meridian by default. mapbox gl draws exactly the coordinates you give it. so if you move a map from google to mapbox, that’s when your pacific routes suddenly start wrapping the planet.
you can fix it in two ways:
- unwrap the longitudes. add or subtract 360 so consecutive points are never more than 180° apart. mapbox will draw 190° as 170° west on the next copy of the world. it looks right on screen, but the coordinates aren’t valid geojson anymore, so bounds, clustering and any other library working with them get confused.
- split the line. cut it where it crosses 180° and return a multilinestring, with one part ending at 180° and the next starting at −180°. that’s what rfc 7946 recommends, and it works everywhere.
splitting only takes one loop:
type Position = [lng: number, lat: number];
/** Cut a line wherever it crosses ±180°, as RFC 7946 recommends. */
export const splitAtAntimeridian = (line: Position[]): Position[][] => {
const parts: Position[][] = [];
let part: Position[] = [];
line.forEach((point, i) => {
const prev = line[i - 1];
if (prev && Math.abs(point[0] - prev[0]) > 180) {
const edge = prev[0] > 0 ? 180 : -180;
// Measure the short way round: unwrap the next point past the edge.
const unwrapped = point[0] + 2 * edge;
const t = (edge - prev[0]) / (unwrapped - prev[0]);
const lat = prev[1] + t * (point[1] - prev[1]);
parts.push([...part, [edge, lat]]);
part = [[-edge, lat]];
}
part.push(point);
});
return [...parts, part];
};
the function assumes that two consecutive points more than 180° apart must cross the meridian. that’s safe for anything sampled often, like a gps track or a flight path, since nothing real moves half way round the planet between two readings.
the one tricky part is working out the latitude where the line meets the edge. t is how far along the segment the crossing happens, and you get it by dividing the distance to the edge by the short way round, not by the raw difference. going from 170° to −170°, the raw difference is 340° but the short way is only 20°, so the crossing sits exactly halfway.
the first version i shipped divided by the raw difference. every crossing ended up right next to the previous point instead of where the line really meets 180°, so each route had a little kink at the meridian. my test only checked that the line got split, not where. this one checks where:
expect(splitAtAntimeridian([[170, 10], [-170, 30]])).toEqual([
[[170, 10], [180, 20]],
[[-180, 20], [-170, 30]],
]);
the interpolation is linear in longitude and latitude, which is how the map draws each segment anyway, so the two halves meet exactly. if you want proper great circles, add more points along the line first and split afterwards.
bounds have the same problem, but there you should unwrap instead. for fitBounds, use west 170 and east 190. a bounding box isn’t geojson, so the rule about valid coordinates doesn’t apply.
so: cut the line, keep the coordinates valid, and test where the cut lands, not just that it happened.