import json, math, os, re, sys, urllib.request
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUTPUT = os.path.join(ROOT, "admin-ui/src/world-map.js")
COMMIT = "ca96624a56bd078437bca8184e78163e5039ad19"
URL = f"https://raw.githubusercontent.com/nvkelso/natural-earth-vector/{COMMIT}/geojson/ne_110m_admin_0_countries.geojson"
WIDTH = 1000
SOUTH = -58.0
A1, A2, A3, A4 = 1.340264, -0.081106, 0.000893, 0.003796
M = math.sqrt(3) / 2
def equal_earth(lon, lat):
lam, phi = math.radians(lon), math.radians(lat)
t = math.asin(M * math.sin(phi))
t2, t6 = t * t, t ** 6
x = lam * math.cos(t) / (M * (A1 + 3 * A2 * t2 + t6 * (7 * A3 + 9 * A4 * t2)))
y = t * (A1 + A2 * t2 + t6 * (A3 + A4 * t2))
return x, y
XMAX = equal_earth(180, 0)[0]
YTOP = equal_earth(0, 90)[1]
YBOTTOM = equal_earth(0, SOUTH)[1]
SCALE = WIDTH / (2 * XMAX)
HEIGHT = round((YTOP - YBOTTOM) * SCALE)
def point(lon, lat):
x, y = equal_earth(lon, max(lat, SOUTH))
return round((x + XMAX) * SCALE), round((YTOP - y) * SCALE)
def ring_path(ring):
pts = []
for lon, lat in ring:
p = point(lon, lat)
if not pts or pts[-1] != p:
pts.append(p)
if len(pts) < 3:
return ""
return "M" + "L".join(f"{x},{y}" for x, y in pts) + "Z"
def build(geojson):
countries = {}
for f in geojson["features"]:
props = f["properties"]
code = props.get("ISO_A2_EH") or props.get("ISO_A2")
if not code or len(code) != 2 or not code.isalpha() or code == "AQ":
continue
g = f["geometry"]
polys = g["coordinates"] if g["type"] == "MultiPolygon" else [g["coordinates"]]
d = "".join(ring_path(ring) for poly in polys for ring in poly[:1]) if d:
countries[code.upper()] = countries.get(code.upper(), "") + d
lines = [
"// Generated by ci/gen_world_map.py from Natural Earth 1:110m countries",
f"// (public domain, nvkelso/natural-earth-vector@{COMMIT[:12]}) — don't edit.",
f"export const VIEWBOX = \"0 0 {WIDTH} {HEIGHT}\";",
"export const COUNTRIES = {",
]
for code in sorted(countries):
lines.append(f' {code}: "{countries[code]}",')
lines.append("};")
return "\n".join(lines) + "\n", len(countries)
def check():
text = open(OUTPUT).read()
n = len(re.findall(r'^ [A-Z]{2}: "M[0-9,LMZ]+",$', text, re.M))
if n < 150 or "export const VIEWBOX" not in text:
sys.exit(f"{OUTPUT}: malformed ({n} countries)")
print(f"{OUTPUT}: {n} countries, {len(text)} bytes")
def main():
args = sys.argv[1:]
if args[:1] == ["--check"]:
check()
return
if args[:1] == ["--from"] and len(args) == 2:
data = json.load(open(args[1]))
else:
req = urllib.request.Request(URL, headers={"User-Agent": "rws gen_world_map.py"})
data = json.load(urllib.request.urlopen(req, timeout=60))
text, n = build(data)
with open(OUTPUT, "w") as f:
f.write(text)
check()
if __name__ == "__main__":
main()