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-messier.py

b327e0d6e7ab67511c5bade3f3c3a93834f6e9ca
moonrise/scripts/make-messier.py history · blame · raw

73 lines · 2686 bytes

 1#!/usr/bin/env python3
 2"""Builds SkyRenderer/Resources/messier.json from OpenNGC (CC BY-SA 4.0), pinned to v20260501.
 3
 4Usage: make-messier.py
 5
 6Rows with a Messier number in NGC.csv and addendum.csv (M40, M45). Coordinates are J2000 degrees,
 7axes arcminutes, position angle degrees east of north; magnitude is V, else B. Each object carries the
 8id of its Library page (link.messier in pages/deep-sky-*.json).
 9
10M102: OpenNGC lists it as a duplicate observation of M101 (addendum type "Dup"), so it is left out
11and the file has 109 objects. The Library page for M102 keeps its own account (probably NGC 5866).
12"""
13import csv
14import io
15import json
16import urllib.request
17from pathlib import Path
18
19ROOT = Path(__file__).resolve().parent.parent
20RES = ROOT / "Packages/MoonriseKit/Sources/SkyRenderer/Resources"
21PAGES = ROOT / "Packages/MoonriseKit/Sources/Library/Resources/pages"
22VERSION = "v20260501"
23BASE = f"https://raw.githubusercontent.com/mattiaverga/OpenNGC/{VERSION}/database_files"
24
25
26def fetch(url):
27    with urllib.request.urlopen(url, timeout=60) as r:
28        return r.read().decode("utf-8")
29
30
31def sexagesimal(s, hours):
32    sign = -1 if s.startswith("-") else 1
33    d, m, sec = (float(x) for x in s.lstrip("+-").split(":"))
34    v = sign * (d + m / 60 + sec / 3600)
35    return v * 15 if hours else v
36
37
38def number(s):
39    return float(s) if s.strip() else None
40
41
42def main():
43    pages = {}
44    for f in sorted(PAGES.glob("deep-sky-*.json")):
45        for p in json.loads(f.read_text()):
46            pages[p["link"]["messier"]] = p["id"]
47
48    out = {}
49    for name in ("NGC.csv", "addendum.csv"):
50        for row in csv.DictReader(io.StringIO(fetch(f"{BASE}/{name}")), delimiter=";"):
51            if not row["M"] or row["Type"] == "Dup":
52                continue
53            m = int(row["M"])
54            assert m not in out, m
55            o = {
56                "messier": m,
57                "type": row["Type"],
58                "ra": round(sexagesimal(row["RA"], True), 5),
59                "dec": round(sexagesimal(row["Dec"], False), 5),
60                "magnitude": number(row["V-Mag"]) if row["V-Mag"].strip() else number(row["B-Mag"]),
61                "majorAxis": number(row["MajAx"]),
62                "minorAxis": number(row["MinAx"]),
63                "positionAngle": number(row["PosAng"]),
64                "page": pages[m],
65            }
66            out[m] = {k: v for k, v in o.items() if v is not None}
67
68    assert sorted(out) == [m for m in range(1, 111) if m != 102], sorted(set(range(1, 111)) - set(out))
69    (RES / "messier.json").write_text(json.dumps([out[m] for m in sorted(out)], ensure_ascii=False, separators=(",", ":")))
70
71
72if __name__ == "__main__":
73    main()