This notebook shows how long straight lines are unusable on map projections like the very popular Mercator one, also used in Folium. And it implements a more detailed version with intermediate points in order to get a more realistic visualization of long lines on such map projections. These might look unusual (except for pilots), but they are much closer to the truth.
You will need to pip-install folium
and geographiclib
for this notebook to work as expected. If you look at this inside a GitHub repo you won't see the maps. Then try on the notebook viewer, there you will.
In [1]:
%matplotlib inline
import math
import folium
In [2]:
la = 34.05351, -118.24531
nyc = 40.71453, -74.00713
berlin = 52.516071, 13.37698
potsdam = 52.39962, 13.04784
singapore = 1.29017, 103.852
sydney = -33.86971, 151.20711
In [3]:
map = folium.Map()
In [4]:
folium.Marker(location=la, popup="Los Angeles").add_to(map)
folium.Marker(location=nyc, popup="New York").add_to(map)
folium.Marker(location=berlin, popup="Berlin").add_to(map)
folium.Marker(location=singapore, popup="Singapore").add_to(map)
folium.Marker(location=sydney, popup="Sydney").add_to(map)
folium.PolyLine([la, nyc, berlin, singapore, sydney],
weight=2,
color="red"
).add_to(map)
Out[4]:
In [5]:
map
Out[5]:
In [6]:
from geographiclib.geodesic import Geodesic
In [7]:
def intermediatePoints(start, end, min_length_km=2000, segment_length_km=1000):
geod = Geodesic.WGS84
if geod.Inverse(*(start + end))["s12"] / 1000.0 < min_length_km:
yield start
yield end
else:
inv_line = geod.InverseLine(*(start + end))
segment_length_m = 1000 * segment_length_km
n = int(math.ceil(inv_line.s13 / segment_length_m))
for i in range(n + 1):
s = min(segment_length_m * i, inv_line.s13)
g = inv_line.Position(s, Geodesic.STANDARD | Geodesic.LONG_UNROLL)
yield g["lat2"], g["lon2"]
In [8]:
for lat, lon in intermediatePoints(berlin, sydney):
print(lat, lon)
In [9]:
for lat, lon in intermediatePoints(berlin, potsdam):
print(lat, lon)
In [10]:
from itertools import tee
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = tee(iterable)
next(b, None)
return zip(a, b)
In [11]:
map = folium.Map()
folium.Marker(location=la, popup="Los Angeles").add_to(map)
folium.Marker(location=nyc, popup="New York").add_to(map)
folium.Marker(location=berlin, popup="Berlin").add_to(map)
folium.Marker(location=singapore, popup="Singapore").add_to(map)
folium.Marker(location=sydney, popup="Sydney").add_to(map)
for start, end in pairwise([la, nyc, berlin, singapore, sydney]):
folium.PolyLine(list(intermediatePoints(start, end)), weight=2).add_to(map)
for start, end in pairwise([la, nyc, berlin, singapore, sydney]):
folium.PolyLine([start, end], weight=2, color="red").add_to(map)
map
Out[11]:
In [12]:
# not used anymore, but still nice ;-)
def extract_dict(aDict, keys):
"Return a sub-dict of ``aDict`` by keys and remove those from ``aDict``."
return {k: aDict.pop(k) for k in keys}
In [13]:
class GeodesicPolyLine(folium.PolyLine):
"""
A geodesic version of a PolyLine inserting intermediate points when needed.
This will calculate intermediate points with some segment length whenever
the geodesic length between two adjacent locations is above some maximal
value.
"""
def __init__(self, locations, min_length_km=2000, segment_length_km=1000, **kwargs):
kwargs1 = dict(min_length_km=min_length_km, segment_length_km=segment_length_km)
geodesic_locs = [intermediatePoints(start, end, **kwargs1) for start, end in pairwise(locations)]
super().__init__(geodesic_locs, **kwargs)
In [14]:
map = folium.Map()
folium.Marker(location=la, popup="Los Angeles").add_to(map)
folium.Marker(location=berlin, popup="Berlin").add_to(map)
folium.Marker(location=sydney, popup="Sydney").add_to(map)
GeodesicPolyLine([la, berlin, sydney]).add_to(map)
map
Out[14]:
Test using longer segment lengths:
In [15]:
map = folium.Map()
folium.Marker(location=la, popup="Los Angeles").add_to(map)
folium.Marker(location=berlin, popup="Berlin").add_to(map)
folium.Marker(location=sydney, popup="Sydney").add_to(map)
GeodesicPolyLine([la, berlin, sydney], segment_length_km=2000).add_to(map)
map
Out[15]:
Test using longer minimum length:
In [16]:
map = folium.Map()
folium.Marker(location=la, popup="Los Angeles").add_to(map)
folium.Marker(location=berlin, popup="Berlin").add_to(map)
folium.Marker(location=sydney, popup="Sydney").add_to(map)
GeodesicPolyLine([la, berlin, sydney], min_length_km=10000).add_to(map)
map
Out[16]:
Let's check the distances (in km):
In [17]:
Geodesic.WGS84.Inverse(*(la + berlin))["s12"] / 1000
Out[17]:
In [18]:
Geodesic.WGS84.Inverse(*(berlin + sydney))["s12"] / 1000
Out[18]: