Initial version: loads GeoJSON and generates SVG map
This commit is contained in:
commit
f676af2fde
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "geo-countries"]
|
||||
path = geo-countries
|
||||
url = https://github.com/datasets/geo-countries
|
1
geo-countries
Submodule
1
geo-countries
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit cd9e0635901eac20294a57ee3b3ce0684d5e3f1a
|
255
qsomap.py
Executable file
255
qsomap.py
Executable file
|
@ -0,0 +1,255 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import svgwrite
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as pp
|
||||
from matplotlib.colors import hsv_to_rgb
|
||||
import json
|
||||
import random
|
||||
|
||||
REF_LATITUDE = 49.58666
|
||||
REF_LONGITUDE = 11.01250
|
||||
# REF_LATITUDE = 1
|
||||
# REF_LONGITUDE = 0
|
||||
|
||||
|
||||
def map_azimuthal_equidistant(lat, lon, ref_lat, ref_lon, R=1):
|
||||
""" Azimuthal equidistant projection.
|
||||
|
||||
This function takes a point to map in latitude/longitude format as well as
|
||||
a reference point which becomes the "center" of the map.
|
||||
|
||||
It then projects the point into the 2D plane such that the distance from
|
||||
the center is proportional to the distance on the Great Circle through the
|
||||
projected and the reference point. The angle represents the azimuthal
|
||||
direction of the projected point.
|
||||
|
||||
Args:
|
||||
lat(numpy.array): Latitudes of the point to project.
|
||||
lon(numpy.array): Longitudes of the point to project.
|
||||
ref_lat(float): Latitude of the reference point.
|
||||
ref_lon(float): Longitude of the reference point.
|
||||
R(float): Radius (scale) of the map.
|
||||
|
||||
Returns:
|
||||
x(numpy.array): The calculated x coordinates.
|
||||
y(numpy.array): The calculated y coordinates.
|
||||
|
||||
"""
|
||||
dlon = lon - ref_lon
|
||||
|
||||
rho_linear_norm = np.arccos(np.sin(ref_lat) * np.sin(lat)
|
||||
+ np.cos(ref_lat)
|
||||
* np.cos(lat) * np.cos(dlon)) / np.pi
|
||||
|
||||
rho = R * rho_linear_norm
|
||||
|
||||
theta = np.arctan2(np.cos(lat) * np.sin(dlon),
|
||||
(np.cos(ref_lat) * np.sin(lat)
|
||||
- np.sin(ref_lat) * np.cos(lat)
|
||||
* np.cos(dlon)))
|
||||
|
||||
x = rho * np.sin(theta)
|
||||
y = -rho * np.cos(theta)
|
||||
|
||||
return x, y
|
||||
|
||||
|
||||
def random_country_color():
|
||||
h = random.random()
|
||||
s = 0.7
|
||||
v = 0.8
|
||||
r, g, b = [int(255.99*x) for x in hsv_to_rgb([h, s, v])]
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
||||
|
||||
random.seed(0)
|
||||
|
||||
""" Test code
|
||||
test_lat = [np.pi/2, np.pi/4, 0, -np.pi/4, -np.pi/2]
|
||||
test_lon = np.arange(-np.pi, np.pi, np.pi/4)
|
||||
|
||||
for lat in test_lat:
|
||||
for lon in test_lon:
|
||||
x, y = map_azimuthal_equidistant(np.array([lat]), np.array([lon]), np.pi/2, 0)
|
||||
|
||||
print(f"{lat*180/np.pi:6.3f}, {lon*180/np.pi:6.3f} => {x[0]:6.3f}, {y[0]:6.3f}")
|
||||
"""
|
||||
|
||||
print("Loading Geodata…")
|
||||
|
||||
with open('geo-countries/data/countries.geojson', 'r') as jfile:
|
||||
geojson = json.load(jfile)
|
||||
|
||||
print("Finding boundaries…")
|
||||
|
||||
# key: 3-letter country identifier
|
||||
# data: {full_name, numpy.array(coordinates), numpy.array(proj_coordinates)}.
|
||||
# coordinates is a list of 2xN arrays, where N is the number of points. Row 0
|
||||
# contains the longitude, Row 1 the latitude.
|
||||
# proj_coordinates is a list of 2xN arrays, where N is the number of points.
|
||||
# Row 0 contains the projected x, Row 1 the projected y.
|
||||
simplegeodata = {}
|
||||
|
||||
features = geojson['features']
|
||||
|
||||
for feature in features:
|
||||
name = feature['properties']['ADMIN']
|
||||
key = feature['properties']['ISO_A2']
|
||||
|
||||
# handle duplicate keys (can happen for small countries)
|
||||
if key in simplegeodata.keys():
|
||||
key = name
|
||||
|
||||
print(f"Preparing {key} ({name})…")
|
||||
|
||||
multipoly = feature['geometry']['coordinates']
|
||||
|
||||
conv_polys = []
|
||||
|
||||
for poly in multipoly:
|
||||
for subpoly in poly:
|
||||
coords_list = [] # list of lists
|
||||
|
||||
assert(len(subpoly[0]) == 2)
|
||||
coords_list += subpoly
|
||||
|
||||
# convert coordinates to numpy array and radians
|
||||
coords = np.array(coords_list).T * np.pi / 180
|
||||
|
||||
conv_polys.append(coords)
|
||||
|
||||
simplegeodata[key] = {"name": name, "coordinates": conv_polys}
|
||||
|
||||
ref_lat = REF_LATITUDE * np.pi / 180
|
||||
ref_lon = REF_LONGITUDE * np.pi / 180
|
||||
|
||||
R = 500
|
||||
|
||||
"""
|
||||
# Override data with test coordinate system
|
||||
coords = []
|
||||
|
||||
N = 128
|
||||
|
||||
# constant-latitude circles
|
||||
coords.append(np.array([np.linspace(-np.pi, np.pi, N),
|
||||
np.ones(N) * np.pi/4]))
|
||||
coords.append(np.array([np.linspace(-np.pi, np.pi, N),
|
||||
np.ones(N) * 0]))
|
||||
coords.append(np.array([np.linspace(-np.pi, np.pi, N),
|
||||
np.ones(N) * -np.pi/4]))
|
||||
|
||||
# constant-longitude half-circles
|
||||
coords.append(np.array([np.ones(N) * -4*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * -3*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * -2*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * -1*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * 0*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * 1*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * 2*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
coords.append(np.array([np.ones(N) * 3*np.pi/4,
|
||||
np.linspace(-np.pi/2, np.pi/2, N)]))
|
||||
|
||||
simplegeodata = {"XY": {'name': 'test', 'coordinates': coords}}
|
||||
"""
|
||||
|
||||
# apply azimuthal equidistant projection
|
||||
for k, v in simplegeodata.items():
|
||||
proj_polys = []
|
||||
|
||||
for poly in v['coordinates']:
|
||||
lat = poly[1, :]
|
||||
lon = poly[0, :]
|
||||
|
||||
x, y = map_azimuthal_equidistant(lat, lon, ref_lat, ref_lon, R)
|
||||
|
||||
coords = np.array([x, y])
|
||||
|
||||
# remove any points that contain a NaN coordinate
|
||||
coords = coords[:, np.any(np.invert(np.isnan(coords)), axis=0)]
|
||||
|
||||
proj_polys.append(coords)
|
||||
|
||||
v['proj_coordinates'] = proj_polys
|
||||
|
||||
#print(simplegeodata['AW']['proj_coordinates'])
|
||||
###print(simplegeodata['DE']['proj_coordinates'])
|
||||
|
||||
#debug plot
|
||||
# generate the SVG
|
||||
|
||||
doc = svgwrite.Drawing("/tmp/test.svg", size=(2*R, 2*R))
|
||||
|
||||
doc.defs.add(doc.style("""
|
||||
.country {
|
||||
stroke: black;
|
||||
stroke-width: 0.01;
|
||||
}
|
||||
"""))
|
||||
|
||||
doc.add(doc.circle(center=(R, R), r=R, fill='#ddeeff',
|
||||
stroke_width=1, stroke='black'))
|
||||
|
||||
for k, v in simplegeodata.items():
|
||||
print(f"Exporting {k}…")
|
||||
|
||||
color = random_country_color()
|
||||
|
||||
group = doc.g()
|
||||
|
||||
for poly in v['proj_coordinates']:
|
||||
points = poly.T + R # shift to the center of the drawing
|
||||
|
||||
pgon = doc.polygon(points, **{
|
||||
'class': 'country',
|
||||
'fill': color})
|
||||
|
||||
group.add(pgon)
|
||||
|
||||
group.set_desc(title=v['name'])
|
||||
doc.add(group)
|
||||
|
||||
# generate equidistant circles
|
||||
|
||||
d_max = 40075/2
|
||||
for distance in [500, 1000, 2000, 3000, 4000, 5000, 6000, 8000, 10000, 12000,
|
||||
14000, 16000, 18000, 20000]:
|
||||
r = R * distance / d_max
|
||||
doc.add(doc.circle(center=(R, R), r=r, fill='none',
|
||||
stroke_width=0.1, stroke='black'))
|
||||
|
||||
print(f"Saving {doc.filename}…")
|
||||
doc.save(pretty=True)
|
||||
|
||||
exit(0)
|
||||
|
||||
# Debug Plot
|
||||
|
||||
for k, v in simplegeodata.items():
|
||||
for poly in v['proj_coordinates']:
|
||||
pp.plot(poly[0, :], poly[1, :])
|
||||
|
||||
pp.plot([-1, 1], [0, 0], 'k', linewidth=0.5)
|
||||
pp.plot([0, 0], [-1, 1], 'k', linewidth=0.5)
|
||||
|
||||
t = np.linspace(-np.pi, np.pi, 256)
|
||||
ct, st = np.cos(t), np.sin(t)
|
||||
pp.plot(ct, st, 'k', linewidth=0.5)
|
||||
|
||||
U = 40075
|
||||
for distance in np.arange(0, U/2, 2000):
|
||||
f = distance / (U/2)
|
||||
pp.plot(f*ct, f*st, 'k', linewidth=0.2)
|
||||
|
||||
pp.axis('equal')
|
||||
|
||||
pp.show()
|
||||
|
Loading…
Reference in a new issue