krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

scripts/make-skycultures.py

v1.0.0
moonrise/scripts/make-skycultures.py history · blame · raw

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()