Wrapping lines/polygons across the antimeridian in Leaflet.js

Solution 1:

Oh, you're hitting antimeridian artifacts. You're not the first one, and will not be the last one.

In Leaflet, there are basically two approaches for this problem:

1a: Cut the polygon beforehand

If you know your GIS tools, preprocess your polygon, so you end up with two (or possibly more) polygons. See «How can I make a polyline wrap around the world?».

Once you have a file with several polygons which don't cross the antimeridian, they should render fine. You will hit artifacts (namely, a vertical polygon border at the antimeridian, spanning the inside ofthe polygon) if you apply a border to the polygons, so you might want to cut a polygon and a polyline with the polygon's edge if you want to render both nicely.

1b: Cut the polygon on the browser

If you don't want to cut the polygon beforehand, you can let the web browser do it on the fly.

There are some utilities that can help here, but I'm going to point to Leaflet.VectorGrid in particular. By leveraging geojson-vt, it can cut polygons and their edges into tile-sized polygons and polygon edges. It can handle geometries crossing the antimeridian quite well.

You might want to look into geojson-vt directly, or maybe turf.js to do some on-the-fly geoprocessing.

2: Think outside the [-180..180] range

Leaflet can handle longitudes outside the [-180..180] range. In Leaflet, longitudes wrap only the TileLayer's tiles and not markers or polylines.

In other words: a marker at [0, -179] is shown at a different place than [0, 181]. See this answer for an example.

In other words: a line from [0, 179] to [0, -179] is 358 degrees long, but a line from [0, 179] to [0, 181] is two degrees long.

In other words: you can have linestrings or polygons with coordinates with longitudes outside the [-180..180] range, and that's fine for Leaflet. It's not fine for a lot of GIS software (in fact, I think that the new GeoJSON spec prohibits it). But it will make Leaflet happy.

Solution 2:

When you are working with a cylindrical projection, as Leaflet does, it can be solved relatively easily with trigonometry. My solution is based on the first approach of Ivan's answer above, which is cutting the line in two parts at the 180th meridian. My solution is not perfect, as I will show below, but it is a good start.

Here is the code:

function addLineToMap(start, end) {
    if (Math.abs(start[1] - end[1]) > 180.0) {
        const start_dist_to_antimeridian = start[1] > 0 ? 180 - start[1] : 180 + start[1];
        const end_dist_to_antimeridian = end[1] > 0 ? 180 - end[1] : 180 + end[1];
        const lat_difference = Math.abs(start[0] - end[0]);
        const alpha_angle = Math.atan(lat_difference / (start_dist_to_antimeridian + end_dist_to_antimeridian)) * (180 / Math.PI) * (start[1] > 0 ? 1 : -1);
        const lat_diff_at_antimeridian = Math.tan(alpha_angle * Math.PI / 180) * start_dist_to_antimeridian;
        const intersection_lat = start[0] + lat_diff_at_antimeridian;
        const first_line_end = [intersection_lat, start[1] > 0 ? 180 : -180];
        const second_line_start = [intersection_lat, end[1] > 0 ? 180 : -180];

        L.polyline([start, first_line_end]).addTo(map);
        L.polyline([second_line_start, end]).addTo(map);
    } else {
        L.polyline([start, end]).addTo(map);
    }
}

This will calculate the latitude where the line crosses the 180th meridian, and draw the first line from the starting point to this latitude on the 180th meridian, and then a second one from this point to the end.

The picture below shows an example of the result.

Example

Even though I'm fairly certain the math checks out on my calculations, there is a small kink where the two lines are separated. I'm not sure whether this is due to the rendering of the Leaflet map, or an actual error in my calculations.

The starting point is [35.552299, 139.779999] and the end point is [64.81510162, -147.8560028].

The total longitudinal difference between the points is 72.364, and latitudinal difference is 29.263. Using the code below or an online calculator, the angle α is 22.018. Taking only the distance from the starting point to the 180th meridian, and the angle α, the latitudinal difference between starting point and intersection is 16.264. Adding the latitude of the starting point and this value, we get a latitude of 51.8166 at the 180th meridian. Drawing a straight line on a map tells me that this value should be slightly higher up, but I can't figure out why or how that is calculated.

If you want a curved line that accurately shows the curvate of the earth, I would highly recommend using Leaflet.Geodisic. It is easy to use and has a solution to the antimeridian problem built-in so you don't have to worry about it.