scripts/make-skycultures.py
117 lines · 4389 bytes
1#!/usr/bin/env python3
2"""Builds SkyRenderer/Resources/skycultures.json from Stellarium sky cultures (CC BY-SA only).
3
4Usage: make-skycultures.py HIP_TSV
5
6HIP_TSV is VizieR I/239/hip_main with columns HIP, RAICRS, DEICRS, Vmag (Vmag < 7.2):
7https://vizier.cds.unistra.fr/viz-bin/asu-tsv?-source=I/239/hip_main&-out=HIP,RAICRS,DEICRS,Vmag&Vmag=%3C7.2&-out.max=unlimited
8
9Stellarium lines use Hipparcos numbers; the app's catalogue is the Bright Star Catalogue (HR).
10Each HIP star is matched to the BSC star within 0.05 degrees. Lines are split where a star has no match
11(fainter than the BSC's limit), and segments with fewer than two stars are dropped.
12"""
13import json
14import math
15import sys
16import urllib.request
17from pathlib import Path
18
19ROOT = Path(__file__).resolve().parent.parent
20RES = ROOT / "Packages/MoonriseKit/Sources/SkyRenderer/Resources"
21BASE = "https://raw.githubusercontent.com/Stellarium/stellarium-skycultures/master"
22
23# id, display name. All CC BY-SA in their description.md as of 2026-10-18.
24CULTURES = [
25 ("chinese", "Chinese"),
26 ("indian", "Indian (Vedic)"),
27 ("mongolian", "Mongolian"),
28 ("japanese_moon_stations", "Japanese moon stations"),
29 ("hawaiian_starlines", "Hawaiian"),
30 ("maori", "Māori"),
31 ("tongan", "Tongan"),
32 ("boorong", "Boorong (Australia)"),
33 ("sami", "Sámi"),
34 ("norse", "Norse"),
35]
36
37
38def fetch(url):
39 with urllib.request.urlopen(url, timeout=60) as r:
40 return r.read().decode("utf-8")
41
42
43def license_section(md):
44 out, on = [], False
45 for line in md.splitlines():
46 if line.startswith("#"):
47 on = "licen" in line.lower()
48 continue
49 if on and line.strip():
50 out.append(line.strip())
51 return " ".join(out)
52
53
54def main():
55 hip = {}
56 for line in open(sys.argv[1]):
57 p = line.rstrip("\n").split("\t")
58 if len(p) == 4 and p[0].strip().isdigit() and p[1].strip() and p[2].strip():
59 hip[int(p[0])] = (float(p[1]), float(p[2]), float(p[3] or 99))
60
61 bsc = json.load(open(RES / "bsc5.json"))
62 cells = {}
63 for row in bsc:
64 cells.setdefault((int(row[2])), []).append(row)
65
66 def hr_for(h):
67 if h not in hip:
68 return None
69 ra, dec, _ = hip[h]
70 best = None
71 for d in (int(dec) - 1, int(dec), int(dec) + 1):
72 for row in cells.get(d, []):
73 dra = (row[1] - ra + 180) % 360 - 180
74 sep = math.hypot(dra * math.cos(math.radians(dec)), row[2] - dec)
75 if sep < 0.05 and (best is None or sep < best[0]):
76 best = (sep, int(row[0]))
77 return best[1] if best else None
78
79 out = []
80 for cid, name in CULTURES:
81 index = json.loads(fetch(f"{BASE}/{cid}/index.json"))
82 lic = license_section(fetch(f"{BASE}/{cid}/description.md"))
83 assert "BY-SA" in lic.replace("-BY SA", "BY-SA").replace("BY SA", "BY-SA"), (cid, lic)
84 constellations = []
85 for c in index.get("constellations", []):
86 segments = []
87 for line in c.get("lines", []):
88 current = []
89 for h in line:
90 hr = hr_for(h) if isinstance(h, int) else None
91 if hr is None:
92 if len(current) > 1:
93 segments.append(current)
94 current = []
95 else:
96 current.append(hr)
97 if len(current) > 1:
98 segments.append(current)
99 if not segments:
100 continue
101 cn = c.get("common_name", {})
102 entry = {"name": cn.get("english") or cn.get("native") or c["id"], "lines": segments}
103 if cn.get("native") and cn.get("native") != entry["name"]:
104 entry["native"] = cn["native"]
105 constellations.append(entry)
106 out.append({
107 "id": cid, "name": name, "region": index.get("region", ""),
108 "licence": lic, "source": f"https://github.com/Stellarium/stellarium-skycultures/tree/master/{cid}",
109 "constellations": constellations,
110 })
111 print(f"{cid}: {len(constellations)} of {len(index.get('constellations', []))} figures", file=sys.stderr)
112
113 (RES / "skycultures.json").write_text(json.dumps(out, ensure_ascii=False, separators=(",", ":")))
114
115
116if __name__ == "__main__":
117 main()