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:
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:
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:
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:
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:
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:
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:
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):
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?
d3.geoCircle()
works just fine. Have a look at this demo forked from the one I set up for my answer to "D3 Topojson Circle with Radius Scaled in Miles". – Cralg