d3.jsleafletmap-projectionsmercatorgreat-circle

Wrapping great circles with Mercator maps with D3.js, Leaflet, or Mapbox


The question, in short: How can I accurately project a wrapping great circle radius in Mercator using something other than the Google Maps API?

The question, in long:

So I've got a conundrum. I run a mapping application that uses Google Maps API to project huge circles onto the Mercator map — it is an attempt to show very large, accurate radii, say, on the order of 13,000 km. But I don't want to use Google Maps API anymore, because Google's new pricing scheme is insane. So I'm trying to convert the code to Leaflet, or Mapbox, or anything non-Google, and nothing can handle these circles correctly.

Here's how Google Maps API handles a geodesic circle with a 13,000 km radius centered just north of Africa: Google Maps API great circle

This looks intuitively weird but is correct. The wavy pattern is caused by the circle wrapping around the Earth.

D3.js can render this correctly in an orthographic projection. So here's the same circle rendered in D3.js with d3.geo.circle() on a globe, in two rotations:

D3.js great circle on orthographic v1D3.js great circle on orthographic v2

Which makes the 2D-"wavy" pattern make more sense, right? Right. I love it. Totally works for my purposes of science communication and all that.

But when I convert my code to Leaflet, it doesn't work at all. Why? Because Leaflet's circle class is not a great circle at all. Instead it seems like it is just an ellipse that is distorted a bit with latitude, but not in a true geodesic way. Same circle, same radius, same origin point, and we get this:

Leaflet's attempt at a 13000 km circle

So wrong, so wrong! Aside from looking totally unrealistic, it's just incorrect — Australia would not be inside such a circle radius. This matters for my application! This can't do.

OK, I thought, maybe the trick is to just try and implement my own great circle class. The approach I took was to iterate over circle points as distances from an origin point, but to calculate the distances using the "Destination point given distance and bearing from start point" calculations from this very helpful website, and then project those as a polygon in Leaflet. This is what I get as a result:

Attempted Great Circle implementation in Leaflet

This looks bad but is actually much closer to being accurate! We're getting the wave effect, which is correct. Like me, you might ask, "what's really going on here?" So I did a version that allowed me to highlight every iterated point:

Attempted Great Circle implementation in Leaflet with points

And you can see quite clearly that it's correctly rendered the circle, but that the polygon is incorrectly joining it. What it ought to be doing (one might naively think) is wrapping that wave figure around the multiple instances of the Mercator map projection, not naively joining them at the top, but joining them spherically. Like this crude Photoshop rendering:

Photoshopped version of Leaflet

And then the polygon would "close" in a way that indicated that everything above the polygon was enclosed in it, as well.

I have no idea how to implement something like this in Leaflet, though. Or anything else for that matter. Maybe I have to somehow process the raw SVG myself, taking into account the zoom state? Or something? Before I go off into those treacherous weeds, I thought I'd ask for any suggestions/ideas/etc. Maybe there's some more obvious approach.

Oh, and I tried one other thing: using the same d3.geo.circle constructor that worked so well in an orthographic projection for a Mercator/Leaflet projection. It produces more or less the same results as my "naive" Leaflet great circle implementation:

D3.js Great Circle in Mercator

Which is promising, I guess. If you move the longitude of the origin point, though, the D3.js version wraps in a much weirder way (D3.js in red, my Leaflet class in turquoise):

D3.js vs Leaflet at different Longitude

I wouldn't be surprised if there was some way in D3.js to change how that worked, but I haven't gone fully down the D3.js rabbit hole. I was hoping that D3.js would make this "easier" (since it is the more full-formed cartographic tool than Leaflet), so I'm going to keep looking into this.

I have not tried to do this in Mapbox-gl yet (I guess that's next on the "attempt" list).

Anyway. Thanks for reading. To reiterate the question: How can I accurately project a wrapping great circle radius in Mercator using something other than the Google Maps API?


Solution

  • So it ended up being not something with an easy solution. To achieve the desired Google Maps-like behavior, I ended up having to code a Leaflet plugin from scratch that extended the L.Polygon object. This is because the desired behavior including a "wrapping" polygon, and there's no "magic" way to do that in Leaflet.

    What I ended up doing was creating a plugin that could detect whether it ought (based on the zoom level) to create numerous wrapped "copies," and then use a bit of logic to determine whether it should be splicing together polygons or not. It's not especially elegant (it is more logic than math) but that's my programming in a nutshell.

    Anyway, here is the final plugin. It can be dropped in like a regular L.Circle object (just change it to L.greatCircle) without too many other changes. You can see it in action on my MISSILEMAP (which also features a geodesic polyline class I had to write, which was a lot easier).

    Thanks for those who gave advice and suggestions.