diff --git a/backend/apps/ingest/constellation.py b/backend/apps/ingest/constellation.py new file mode 100644 index 0000000..9086add --- /dev/null +++ b/backend/apps/ingest/constellation.py @@ -0,0 +1,82 @@ +"""Identify the IAU constellation that contains an equatorial position. + +Uses the classic Roman (1987) point-in-constellation method: the official IAU +boundaries are rectangular in RA/Dec *referred to equinox B1875.0*, so a given +position is precessed back to B1875 and then matched against an ordered table of +boundary segments (the first segment whose Dec floor and RA span contain the +point wins). Boundary table: VI/42 "Identification of a Constellation from +Position" (Roman 1987), bundled as ``data/constellation_boundaries.dat``. + +Kept dependency-free and closed-form, matching the parser's philosophy: the +phone's degree-level orientation accuracy is far coarser than the arc-minute +error of this precession, so no heavy astrometry library is warranted. The only +ambiguity this cannot resolve is a trail that straddles a boundary — we report +the constellation of each endpoint independently, which is exactly what the UI +shows ("started in X, ended in Y"). +""" +from __future__ import annotations + +import math +from functools import lru_cache +from pathlib import Path + +_DATA = Path(__file__).with_name("data") / "constellation_boundaries.dat" + + +@lru_cache(maxsize=1) +def _boundaries(): + """Parsed boundary table as a list of (ra_low_h, ra_high_h, dec_low, abbr). + + Rows are kept in file order; the lookup relies on that ordering (the table + is sorted by descending Dec floor, so the first containing row is correct). + """ + rows = [] + for line in _DATA.read_text().splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + ra_low, ra_high, dec_low, abbr = line.split() + rows.append((float(ra_low), float(ra_high), float(dec_low), abbr)) + return rows + + +def _precess_j2000_to_b1875(ra_deg, dec_deg): + """Precess a mean equatorial position from J2000.0 to B1875.0. + + IAU 1976 precession angles; T is the (negative) Julian-century interval from + J2000.0 to the B1875.0 epoch. Accurate to well under an arc-minute over this + 125-year span — comfortably finer than the boundary granularity. + """ + # B1875.0 = JD 2405889.25 -> centuries from J2000.0 (JD 2451545.0). + t = (2405889.25 - 2451545.0) / 36525.0 + arcsec = math.pi / (180.0 * 3600.0) + zeta = (2306.2181 * t + 0.30188 * t * t + 0.017998 * t**3) * arcsec + z = (2306.2181 * t + 1.09468 * t * t + 0.018203 * t**3) * arcsec + theta = (2004.3109 * t - 0.42665 * t * t - 0.041833 * t**3) * arcsec + + ra = math.radians(ra_deg) + dec = math.radians(dec_deg) + a = math.cos(dec) * math.sin(ra + zeta) + b = math.cos(theta) * math.cos(dec) * math.cos(ra + zeta) - math.sin(theta) * math.sin(dec) + c = math.sin(theta) * math.cos(dec) * math.cos(ra + zeta) + math.cos(theta) * math.sin(dec) + ra_out = math.degrees(math.atan2(a, b) + z) % 360.0 + dec_out = math.degrees(math.asin(max(-1.0, min(1.0, c)))) + return ra_out, dec_out + + +def constellation_abbr(ra_deg, dec_deg): + """IAU 3-letter abbreviation (e.g. ``"Ori"``) for a J2000 RA/Dec, or None. + + Returns None when coordinates are missing or no boundary row matches (which + should not happen for a valid position — the table tiles the whole sphere). + """ + if ra_deg is None or dec_deg is None: + return None + ra1875, dec1875 = _precess_j2000_to_b1875(ra_deg, dec_deg) + ra_h = ra1875 / 15.0 # boundary table RA is in hours + for ra_low, ra_high, dec_low, abbr in _boundaries(): + if dec1875 < dec_low: + continue + if ra_low <= ra_h < ra_high: + return abbr + return None diff --git a/backend/apps/ingest/data/constellation_boundaries.dat b/backend/apps/ingest/data/constellation_boundaries.dat new file mode 100644 index 0000000..4166150 --- /dev/null +++ b/backend/apps/ingest/data/constellation_boundaries.dat @@ -0,0 +1,360 @@ +# IAU constellation boundaries, epoch B1875.0 (Roman 1987). +# Source: VizieR VI/42 "Identification of a Constellation from Position". +# Columns: RA_low(h) RA_high(h) Dec_low(deg) IAU_abbrev + 0.0000 24.0000 88.0000 UMi + 8.0000 14.5000 86.5000 UMi + 21.0000 23.0000 86.1667 UMi + 18.0000 21.0000 86.0000 UMi + 0.0000 8.0000 85.0000 Cep + 9.1667 10.6667 82.0000 Cam + 0.0000 5.0000 80.0000 Cep + 10.6667 14.5000 80.0000 Cam + 17.5000 18.0000 80.0000 UMi + 20.1667 21.0000 80.0000 Dra + 0.0000 3.5083 77.0000 Cep + 11.5000 13.5833 77.0000 Cam + 16.5333 17.5000 75.0000 UMi + 20.1667 20.6667 75.0000 Cep + 7.9667 9.1667 73.5000 Cam + 9.1667 11.3333 73.5000 Dra + 13.0000 16.5333 70.0000 UMi + 3.1000 3.4167 68.0000 Cas + 20.4167 20.6667 67.0000 Dra + 11.3333 12.0000 66.5000 Dra + 0.0000 0.3333 66.0000 Cep + 14.0000 15.6667 66.0000 UMi + 23.5833 24.0000 66.0000 Cep + 12.0000 13.5000 64.0000 Dra + 13.5000 14.4167 63.0000 Dra + 23.1667 23.5833 63.0000 Cep + 6.1000 7.0000 62.0000 Cam + 20.0000 20.4167 61.5000 Dra + 20.5367 20.6000 60.9167 Cep + 7.0000 7.9667 60.0000 Cam + 7.9667 8.4167 60.0000 UMa + 19.7667 20.0000 59.5000 Dra + 20.0000 20.5367 59.5000 Cep + 22.8667 23.1667 59.0833 Cep + 0.0000 2.4333 58.5000 Cas + 19.4167 19.7667 58.0000 Dra + 1.7000 1.9083 57.5000 Cas + 2.4333 3.1000 57.0000 Cas + 3.1000 3.1667 57.0000 Cam + 22.3167 22.8667 56.2500 Cep + 5.0000 6.1000 56.0000 Cam + 14.0333 14.4167 55.5000 UMa + 14.4167 19.4167 55.5000 Dra + 3.1667 3.3333 55.0000 Cam + 22.1333 22.3167 55.0000 Cep + 20.6000 21.9667 54.8333 Cep + 0.0000 1.7000 54.0000 Cas + 6.1000 6.5000 54.0000 Lyn + 12.0833 13.5000 53.0000 UMa + 15.2500 15.7500 53.0000 Dra + 21.9667 22.1333 52.7500 Cep + 3.3333 5.0000 52.5000 Cam + 22.8667 23.3333 52.5000 Cas + 15.7500 17.0000 51.5000 Dra + 2.0417 2.5167 50.5000 Per + 17.0000 18.2333 50.5000 Dra + 0.0000 1.3667 50.0000 Cas + 1.3667 1.6667 50.0000 Per + 6.5000 6.8000 50.0000 Lyn + 23.3333 24.0000 50.0000 Cas + 13.5000 14.0333 48.5000 UMa + 0.0000 1.1167 48.0000 Cas + 23.5833 24.0000 48.0000 Cas + 18.1750 18.2333 47.5000 Her + 18.2333 19.0833 47.5000 Dra + 19.0833 19.1667 47.5000 Cyg + 1.6667 2.0417 47.0000 Per + 8.4167 9.1667 47.0000 UMa + 0.1667 0.8667 46.0000 Cas + 12.0000 12.0833 45.0000 UMa + 6.8000 7.3667 44.5000 Lyn + 21.9083 21.9667 44.0000 Cyg + 21.8750 21.9083 43.7500 Cyg + 19.1667 19.4000 43.5000 Cyg + 9.1667 10.1667 42.0000 UMa + 10.1667 10.7833 40.0000 UMa + 15.4333 15.7500 40.0000 Boo + 15.7500 16.3333 40.0000 Her + 9.2500 9.5833 39.7500 Lyn + 0.0000 2.5167 36.7500 And + 2.5167 2.5667 36.7500 Per + 19.3583 19.4000 36.5000 Lyr + 4.5000 4.6917 36.0000 Per + 21.7333 21.8750 36.0000 Cyg + 21.8750 22.0000 36.0000 Lac + 6.5333 7.3667 35.5000 Aur + 7.3667 7.7500 35.5000 Lyn + 0.0000 2.0000 35.0000 And + 22.0000 22.8167 35.0000 Lac + 22.8167 22.8667 34.5000 Lac + 22.8667 23.5000 34.5000 And + 2.5667 2.7167 34.0000 Per + 10.7833 11.0000 34.0000 UMa + 12.0000 12.3333 34.0000 CVn + 7.7500 9.2500 33.5000 Lyn + 9.2500 9.8833 33.5000 LMi + 0.7167 1.4083 33.0000 And + 15.1833 15.4333 33.0000 Boo + 23.5000 23.7500 32.0833 And + 12.3333 13.2500 32.0000 CVn + 23.7500 24.0000 31.3333 And + 13.9583 14.0333 30.7500 CVn + 2.4167 2.7167 30.6667 Tri + 2.7167 4.5000 30.6667 Per + 4.5000 4.7500 30.0000 Aur + 18.1750 19.3583 30.0000 Lyr + 11.0000 12.0000 29.0000 UMa + 19.6667 20.9167 29.0000 Cyg + 4.7500 5.8833 28.5000 Aur + 9.8833 10.5000 28.5000 LMi + 13.2500 13.9583 28.5000 CVn + 0.0000 0.0667 28.0000 And + 1.4083 1.6667 28.0000 Tri + 5.8833 6.5333 28.0000 Aur + 7.8833 8.0000 28.0000 Gem + 20.9167 21.7333 28.0000 Cyg + 19.2583 19.6667 27.5000 Cyg + 1.9167 2.4167 27.2500 Tri + 16.1667 16.3333 27.0000 CrB + 15.0833 15.1833 26.0000 Boo + 15.1833 16.1667 26.0000 CrB + 18.3667 18.8667 26.0000 Lyr + 10.7500 11.0000 25.5000 LMi + 18.8667 19.2583 25.5000 Lyr + 1.6667 1.9167 25.0000 Tri + 0.7167 0.8500 23.7500 Psc + 10.5000 10.7500 23.5000 LMi + 21.2500 21.4167 23.5000 Vul + 5.7000 5.8833 22.8333 Tau + 0.0667 0.1417 22.0000 And + 15.9167 16.0333 22.0000 Ser + 5.8833 6.2167 21.5000 Gem + 19.8333 20.2500 21.2500 Vul + 18.8667 19.2500 21.0833 Vul + 0.1417 0.8500 21.0000 And + 20.2500 20.5667 20.5000 Vul + 7.8083 7.8833 20.0000 Gem + 20.5667 21.2500 19.5000 Vul + 19.2500 19.8333 19.1667 Vul + 3.2833 3.3667 19.0000 Ari + 18.8667 19.0000 18.5000 Sge + 5.7000 5.7667 18.0000 Ori + 6.2167 6.3083 17.5000 Gem + 19.0000 19.8333 16.1667 Sge + 4.9667 5.3333 16.0000 Tau + 15.9167 16.0833 16.0000 Her + 19.8333 20.2500 15.7500 Sge + 4.6167 4.9667 15.5000 Tau + 5.3333 5.6000 15.5000 Tau + 12.8333 13.5000 15.0000 Com + 17.2500 18.2500 14.3333 Her + 11.8667 12.8333 14.0000 Com + 7.5000 7.8083 13.5000 Gem + 16.7500 17.2500 12.8333 Her + 0.0000 0.1417 12.5000 Peg + 5.6000 5.7667 12.5000 Tau + 7.0000 7.5000 12.5000 Gem + 21.1167 21.3333 12.5000 Peg + 6.3083 6.9333 12.0000 Gem + 18.2500 18.8667 12.0000 Her + 20.8750 21.0500 11.8333 Del + 21.0500 21.1167 11.8333 Peg + 11.5167 11.8667 11.0000 Leo + 6.2417 6.3083 10.0000 Ori + 6.9333 7.0000 10.0000 Gem + 7.8083 7.9250 10.0000 Cnc + 23.8333 24.0000 10.0000 Peg + 1.6667 3.2833 9.9167 Ari + 20.1417 20.3000 8.5000 Del + 13.5000 15.0833 8.0000 Boo + 22.7500 23.8333 7.5000 Peg + 7.9250 9.2500 7.0000 Cnc + 9.2500 10.7500 7.0000 Leo + 18.2500 18.6622 6.2500 Oph + 18.6622 18.8667 6.2500 Aql + 20.8333 20.8750 6.0000 Del + 7.0000 7.0167 5.5000 CMi + 18.2500 18.4250 4.5000 Ser + 16.0833 16.7500 4.0000 Her + 18.2500 18.4250 3.0000 Oph + 21.4667 21.6667 2.7500 Peg + 0.0000 2.0000 2.0000 Psc + 18.5833 18.8667 2.0000 Ser + 20.3000 20.8333 2.0000 Del + 20.8333 21.3333 2.0000 Equ + 21.3333 21.4667 2.0000 Peg + 22.0000 22.7500 2.0000 Peg + 21.6667 22.0000 1.7500 Peg + 7.0167 7.2000 1.5000 CMi + 3.5833 4.6167 0.0000 Tau + 4.6167 4.6667 0.0000 Ori + 7.2000 8.0833 0.0000 CMi + 14.6667 15.0833 0.0000 Vir + 17.8333 18.2500 0.0000 Oph + 2.6500 3.2833 -01.7500 Cet + 3.2833 3.5833 -01.7500 Tau + 15.0833 16.2667 -03.2500 Ser + 4.6667 5.0833 -04.0000 Ori + 5.8333 6.2417 -04.0000 Ori + 17.8333 17.9667 -04.0000 Ser + 18.2500 18.5833 -04.0000 Ser + 18.5833 18.8667 -04.0000 Aql + 22.7500 23.8333 -04.0000 Psc + 10.7500 11.5167 -06.0000 Leo + 11.5167 11.8333 -06.0000 Vir + 0.0000 00.3333 -07.0000 Psc + 23.8333 24.0000 -07.0000 Psc + 14.2500 14.6667 -08.0000 Vir + 15.9167 16.2667 -08.0000 Oph + 20.0000 20.5333 -09.0000 Aql + 21.3333 21.8667 -09.0000 Aqr + 17.1667 17.9667 -10.0000 Oph + 5.8333 8.0833 -11.0000 Mon + 4.9167 5.0833 -11.0000 Eri + 5.0833 5.8333 -11.0000 Ori + 8.0833 8.3667 -11.0000 Hya + 9.5833 10.7500 -11.0000 Sex + 11.8333 12.8333 -11.0000 Vir + 17.5833 17.6667 -11.6667 Oph + 18.8667 20.0000 -12.0333 Aql + 4.8333 4.9167 -14.5000 Eri + 20.5333 21.3333 -15.0000 Aqr + 17.1667 18.2500 -16.0000 Ser + 18.2500 18.8667 -16.0000 Sct + 8.3667 8.5833 -17.0000 Hya + 16.2667 16.3750 -18.2500 Oph + 8.5833 9.0833 -19.0000 Hya + 10.7500 10.8333 -19.0000 Crt + 16.2667 16.3750 -19.2500 Sco + 15.6667 15.9167 -20.0000 Lib + 12.5833 12.8333 -22.0000 Crv + 12.8333 14.2500 -22.0000 Vir + 9.0833 9.7500 -24.0000 Hya + 1.6667 2.6500 -24.3833 Cet + 2.6500 3.7500 -24.3833 Eri + 10.8333 11.8333 -24.5000 Crt + 11.8333 12.5833 -24.5000 Crv + 14.2500 14.9167 -24.5000 Lib + 16.2667 16.7500 -24.5833 Oph + 0.0000 1.6667 -25.5000 Cet + 21.3333 21.8667 -25.5000 Cap + 21.8667 23.8333 -25.5000 Aqr + 23.8333 24.0000 -25.5000 Cet + 9.7500 10.2500 -26.5000 Hya + 4.7000 4.8333 -27.2500 Eri + 4.8333 6.1167 -27.2500 Lep + 20.0000 21.3333 -28.0000 Cap + 10.2500 10.5833 -29.1667 Hya + 12.5833 14.9167 -29.5000 Hya + 14.9167 15.6667 -29.5000 Lib + 15.6667 16.0000 -29.5000 Sco + 4.5833 4.7000 -30.0000 Eri + 16.7500 17.6000 -30.0000 Oph + 17.6000 17.8333 -30.0000 Sgr + 10.5833 10.8333 -31.1667 Hya + 6.1167 7.3667 -33.0000 CMa + 12.2500 12.5833 -33.0000 Hya + 10.8333 12.2500 -35.0000 Hya + 3.5000 3.7500 -36.0000 For + 8.3667 9.3667 -36.7500 Pyx + 4.2667 4.5833 -37.0000 Eri + 17.8333 19.1667 -37.0000 Sgr + 21.3333 23.0000 -37.0000 PsA + 23.0000 23.3333 -37.0000 Scl + 3.0000 3.5000 -39.5833 For + 9.3667 11.0000 -39.7500 Ant + 0.0000 1.6667 -40.0000 Scl + 1.6667 3.0000 -40.0000 For + 3.8667 4.2667 -40.0000 Eri + 23.3333 24.0000 -40.0000 Scl + 14.1667 14.9167 -42.0000 Cen + 15.6667 16.0000 -42.0000 Lup + 16.0000 16.4208 -42.0000 Sco + 4.8333 5.0000 -43.0000 Cae + 5.0000 6.5833 -43.0000 Col + 8.0000 8.3667 -43.0000 Pup + 3.4167 3.8667 -44.0000 Eri + 16.4208 17.8333 -45.5000 Sco + 17.8333 19.1667 -45.5000 CrA + 19.1667 20.3333 -45.5000 Sgr + 20.3333 21.3333 -45.5000 Mic + 3.0000 3.4167 -46.0000 Eri + 4.5000 4.8333 -46.5000 Cae + 15.3333 15.6667 -48.0000 Lup + 0.0000 2.3333 -48.1667 Phe + 2.6667 3.0000 -49.0000 Eri + 4.0833 4.2667 -49.0000 Hor + 4.2667 4.5000 -49.0000 Cae + 21.3333 22.0000 -50.0000 Gru + 6.0000 8.0000 -50.7500 Pup + 8.0000 8.1667 -50.7500 Vel + 2.4167 2.6667 -51.0000 Eri + 3.8333 4.0833 -51.0000 Hor + 0.0000 1.8333 -51.5000 Phe + 6.0000 6.1667 -52.5000 Car + 8.1667 8.4500 -53.0000 Vel + 3.5000 3.8333 -53.1667 Hor + 3.8333 4.0000 -53.1667 Dor + 0.0000 1.5833 -53.5000 Phe + 2.1667 2.4167 -54.0000 Eri + 4.5000 5.0000 -54.0000 Pic + 15.0500 15.3333 -54.0000 Lup + 8.4500 8.8333 -54.5000 Vel + 6.1667 6.5000 -55.0000 Car + 11.8333 12.8333 -55.0000 Cen + 14.1667 15.0500 -55.0000 Lup + 15.0500 15.3333 -55.0000 Nor + 4.0000 4.3333 -56.5000 Dor + 8.8333 11.0000 -56.5000 Vel + 11.0000 11.2500 -56.5000 Cen + 17.5000 18.0000 -57.0000 Ara + 18.0000 20.3333 -57.0000 Tel + 22.0000 23.3333 -57.0000 Gru + 3.2000 3.5000 -57.5000 Hor + 5.0000 5.5000 -57.5000 Pic + 6.5000 6.8333 -58.0000 Car + 0.0000 1.3333 -58.5000 Phe + 1.3333 2.1667 -58.5000 Eri + 23.3333 24.0000 -58.5000 Phe + 4.3333 4.5833 -59.0000 Dor + 15.3333 16.4208 -60.0000 Nor + 20.3333 21.3333 -60.0000 Ind + 5.5000 6.0000 -61.0000 Pic + 15.1667 15.3333 -61.0000 Cir + 16.4208 16.5833 -61.0000 Ara + 14.9167 15.1667 -63.5833 Cir + 16.5833 16.7500 -63.5833 Ara + 6.0000 6.8333 -64.0000 Pic + 6.8333 9.0333 -64.0000 Car + 11.2500 11.8333 -64.0000 Cen + 11.8333 12.8333 -64.0000 Cru + 12.8333 14.5333 -64.0000 Cen + 13.5000 13.6667 -65.0000 Cir + 16.7500 16.8333 -65.0000 Ara + 2.1667 3.2000 -67.5000 Hor + 3.2000 4.5833 -67.5000 Ret + 14.7500 14.9167 -67.5000 Cir + 16.8333 17.5000 -67.5000 Ara + 17.5000 18.0000 -67.5000 Pav + 22.0000 23.3333 -67.5000 Tuc + 4.5833 6.5833 -70.0000 Dor + 13.6667 14.7500 -70.0000 Cir + 14.7500 17.0000 -70.0000 TrA + 0.0000 1.3333 -75.0000 Tuc + 3.5000 4.5833 -75.0000 Hyi + 6.5833 9.0333 -75.0000 Vol + 9.0333 11.2500 -75.0000 Car + 11.2500 13.6667 -75.0000 Mus + 18.0000 21.3333 -75.0000 Pav + 21.3333 23.3333 -75.0000 Ind + 23.3333 24.0000 -75.0000 Tuc + 0.7500 1.3333 -76.0000 Tuc + 0.0000 3.5000 -82.5000 Hyi + 7.6667 13.6667 -82.5000 Cha + 13.6667 18.0000 -82.5000 Aps + 3.5000 7.6667 -85.0000 Men + 0.0000 24.0000 -90.0000 Oct diff --git a/backend/apps/ingest/migrations/0003_parsedmeasurement_constellation.py b/backend/apps/ingest/migrations/0003_parsedmeasurement_constellation.py new file mode 100644 index 0000000..a9bbd2d --- /dev/null +++ b/backend/apps/ingest/migrations/0003_parsedmeasurement_constellation.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("ingest", "0002_parsedmeasurement"), + ] + + operations = [ + migrations.AddField( + model_name="parsedmeasurement", + name="start_constellation", + field=models.CharField(blank=True, default="", max_length=3), + ), + migrations.AddField( + model_name="parsedmeasurement", + name="end_constellation", + field=models.CharField(blank=True, default="", max_length=3), + ), + ] diff --git a/backend/apps/ingest/models.py b/backend/apps/ingest/models.py index 51bfe11..933c807 100644 --- a/backend/apps/ingest/models.py +++ b/backend/apps/ingest/models.py @@ -71,6 +71,10 @@ class ParsedMeasurement(models.Model): end_ra = models.FloatField(null=True, blank=True) end_dec = models.FloatField(null=True, blank=True) + # IAU constellation (3-letter abbreviation) each trail endpoint falls in. + start_constellation = models.CharField(max_length=3, blank=True, default="") + end_constellation = models.CharField(max_length=3, blank=True, default="") + lat = models.FloatField(null=True, blank=True) lon = models.FloatField(null=True, blank=True) accuracy = models.FloatField(null=True, blank=True) diff --git a/backend/apps/ingest/parser.py b/backend/apps/ingest/parser.py index 109d0f6..65ca911 100644 --- a/backend/apps/ingest/parser.py +++ b/backend/apps/ingest/parser.py @@ -23,8 +23,11 @@ import math from datetime import UTC, datetime +from .constellation import constellation_abbr + # Bump when the parsing logic changes so stored records can be re-derived. -PARSE_VERSION = 1 +# v2: added start/end IAU constellation. +PARSE_VERSION = 2 class ParseError(ValueError): @@ -191,6 +194,9 @@ def parse_and_store(raw): data = dict(fields) data["start_ra"], data["start_dec"] = start or (None, None) data["end_ra"], data["end_dec"] = end or (None, None) + # Constellation each endpoint falls in (needs the derived RA/Dec). + data["start_constellation"] = constellation_abbr(data["start_ra"], data["start_dec"]) or "" + data["end_constellation"] = constellation_abbr(data["end_ra"], data["end_dec"]) or "" # UTC is already absolute (epoch ms); resolve the site's civil time zone so # the observer's local time can be shown alongside it. data["event_tz"] = site_timezone(fields["lat"], fields["lon"]) or "" diff --git a/backend/apps/web/api.py b/backend/apps/web/api.py index fc423ed..3098763 100644 --- a/backend/apps/web/api.py +++ b/backend/apps/web/api.py @@ -67,6 +67,7 @@ class TrailPoint(Schema): az: float | None = None ra: float | None = None # equatorial, degrees (for the star map) dec: float | None = None + constellation: str | None = None # IAU 3-letter abbreviation (e.g. "Ori") class ReportDetail(Schema): @@ -265,10 +266,12 @@ def report_detail(request, client_key: str): "start": { "alt": parsed.start_alt, "az": parsed.start_az, "ra": parsed.start_ra, "dec": parsed.start_dec, + "constellation": parsed.start_constellation or None, }, "end": { "alt": parsed.end_alt, "az": parsed.end_az, "ra": parsed.end_ra, "dec": parsed.end_dec, + "constellation": parsed.end_constellation or None, }, } diff --git a/backend/tests/test_parser.py b/backend/tests/test_parser.py index d8b505b..3f3f285 100644 --- a/backend/tests/test_parser.py +++ b/backend/tests/test_parser.py @@ -7,6 +7,7 @@ from apps.devices.models import Device from apps.ingest import parser +from apps.ingest.constellation import constellation_abbr from apps.ingest.models import RawIngest from tests.test_auth_sync import _register, _token @@ -59,6 +60,24 @@ def test_altaz_to_radec_without_location_is_none(): assert parser.altaz_to_radec(40, 90, None, None, when) is None +@pytest.mark.parametrize( + "ra,dec,abbr", + [ + (88.79, 7.41, "Ori"), # Betelgeuse + (37.95, 89.26, "UMi"), # Polaris + (279.23, 38.78, "Lyr"), # Vega + (101.29, -16.72, "CMa"), # Sirius + (247.35, -26.43, "Sco"), # Antares + ], +) +def test_constellation_for_known_stars(ra, dec, abbr): + assert constellation_abbr(ra, dec) == abbr + + +def test_constellation_missing_coords_is_none(): + assert constellation_abbr(None, None) is None + + # ---- persistence + endpoint ---- def _device(client): @@ -90,6 +109,7 @@ def test_parse_and_store_creates_record(): parsed = parser.parse_and_store(raw) assert parsed.start_ra is not None and parsed.end_dec is not None + assert parsed.start_constellation and parsed.end_constellation # 3-letter IAU abbr assert parsed.event_time is not None and parsed.parse_version == parser.PARSE_VERSION raw.refresh_from_db() assert raw.status == RawIngest.STATUS_PROCESSED @@ -108,6 +128,7 @@ def test_report_detail_endpoint_returns_render_payload(): body = res.json() assert body["status"] == "processed" assert body["start"]["alt"] == 40.0 and body["start"]["ra"] is not None + assert body["start"]["constellation"] and body["end"]["constellation"] assert body["end"]["az"] == 95.0 assert body["event_utc"] is not None if _HAS_TZF: diff --git a/frontend/app.js b/frontend/app.js index 1cf2a37..8f04b3e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -81,16 +81,31 @@ async function selectReport(key, tr) { } function showSkyCaption(d) { - const cap = $('skyCaption'); const rendered = d ? MeteorSky.render(d) : false; - if (!d) { cap.textContent = t('sky.pick'); return; } - if (!rendered) { cap.textContent = t('sky.noCoords'); return; } + if (!d) { $('skyCaption').textContent = t('sky.pick'); return; } + if (!rendered) { $('skyCaption').textContent = t('sky.noCoords'); return; } + renderCaptionText(d); +} + +// Build just the caption text (no sky re-render), so it can be refreshed when +// the sky data loads or the language changes without re-triggering the dome. +function renderCaptionText(d) { + const cap = $('skyCaption'); const tz = d.event_tz ? ` (${d.event_tz})` : ''; const s = d.start, e = d.end; cap.innerHTML = `${fmtLocal(d.event_local || d.event_utc)}${tz}
` + - `${t('sky.startLabel')} ALT/AZ ${fmt(s.alt)}° / ${fmt(s.az)}° · RA/Dek ${fmt(s.ra, 1)}° / ${fmt(s.dec, 1)}°
` + - `${t('sky.endLabel')} ALT/AZ ${fmt(e.alt)}° / ${fmt(e.az)}° · RA/Dek ${fmt(e.ra, 1)}° / ${fmt(e.dec, 1)}°`; + `${t('sky.startLabel')} ALT/AZ ${fmt(s.alt)}° / ${fmt(s.az)}° · RA/Dek ${fmt(s.ra, 1)}° / ${fmt(s.dec, 1)}°${constSuffix(s)}
` + + `${t('sky.endLabel')} ALT/AZ ${fmt(e.alt)}° / ${fmt(e.az)}° · RA/Dek ${fmt(e.ra, 1)}° / ${fmt(e.dec, 1)}°${constSuffix(e)}`; +} + +// "· in " suffix for a trail endpoint, when known. The full +// localized name comes from the sky data; until that loads we show the bare +// abbreviation, then refresh the caption via onSkyData once it is available. +function constSuffix(pt) { + if (!pt || !pt.constellation) return ''; + const name = window.MeteorSky ? MeteorSky.constNameByAbbr(pt.constellation) : pt.constellation; + return ` · ${t('sky.in')} ${name}`; } let pollTimer = null; @@ -121,6 +136,12 @@ async function startLogin() { }, (interval || 2) * 1000); } +// Sky data finished loading: re-render the caption so constellation +// abbreviations are replaced by their localized full names. +window.onSkyData = function (detail) { + if (selectedDetail && detail === selectedDetail) renderCaptionText(selectedDetail); +}; + // Re-apply language-dependent dynamic content whenever the language changes. window.onI18nApplied = function () { refreshGridIntro(); diff --git a/frontend/i18n.js b/frontend/i18n.js index dd6ad1e..0370a2d 100644 --- a/frontend/i18n.js +++ b/frontend/i18n.js @@ -24,7 +24,7 @@ loading: 'Načítám…', loadFail: 'Detail měření se nepodařilo načíst.', noCoords: 'Měření nemá GPS souřadnice nebo čas — stopu na obloze nelze vykreslit.', - startLabel: 'Start', endLabel: 'Konec', + startLabel: 'Start', endLabel: 'Konec', in: 'v souhvězdí', hint: 'Kolečkem přiblížíš, tažením posuneš, dvojklikem resetuješ pohled.', }, dir: { N: 'S', NE: 'SV', E: 'V', SE: 'JV', S: 'J', SW: 'JZ', W: 'Z', NW: 'SZ' }, @@ -60,7 +60,7 @@ loading: 'Loading…', loadFail: 'Could not load the measurement detail.', noCoords: 'This measurement has no GPS location or time — the trail cannot be drawn.', - startLabel: 'Start', endLabel: 'End', + startLabel: 'Start', endLabel: 'End', in: 'in', hint: 'Scroll to zoom, drag to pan, double-click to reset the view.', }, dir: { N: 'N', NE: 'NE', E: 'E', SE: 'SE', S: 'S', SW: 'SW', W: 'W', NW: 'NW' }, diff --git a/frontend/sky.js b/frontend/sky.js index 741f6f8..ed1fc08 100644 --- a/frontend/sky.js +++ b/frontend/sky.js @@ -39,6 +39,13 @@ return { ra: f.geometry.coordinates[0], dec: f.geometry.coordinates[1], props: f.properties }; }), }; + // Index constellation props by IAU abbreviation (e.g. "Ori") so the + // caption can show a localized full name for a detected constellation. + dataCache.byAbbr = {}; + res[2].features.forEach(function (f) { + var k = (f.properties.desig || f.id || '').toLowerCase(); + if (k) dataCache.byAbbr[k] = f.properties; + }); return dataCache; }).catch(function () { dataCache = { stars: [], lines: [], names: [] }; return dataCache; }); } @@ -69,6 +76,13 @@ ]; function dirLabel(k) { return global.MPI18n ? global.MPI18n.t('dir.' + k) : k; } function constName(p) { var k = (global.MPI18n && global.MPI18n.lang === 'en') ? 'en' : 'cz'; return p[k] || p.en || p.name || ''; } + // Localized full constellation name for an IAU abbreviation (e.g. "Ori" -> + // "Orion"/"Orion"); falls back to the abbreviation until the data has loaded. + function constNameByAbbr(abbr) { + if (!abbr) return ''; + var p = dataCache && dataCache.byAbbr && dataCache.byAbbr[String(abbr).toLowerCase()]; + return p ? constName(p) : abbr; + } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>'); } function build(detail, data) { @@ -211,6 +225,9 @@ if (MeteorSky._detail !== detail) return; var host = document.getElementById('skymap'); if (host) bind(host); build(detail, data); reset(); paint(); + // Names are now available; let the caption resolve localized constellation + // names (it may have rendered with bare abbreviations before the fetch). + if (typeof global.onSkyData === 'function') global.onSkyData(detail); }); return true; } @@ -226,5 +243,8 @@ else { zoomAt(host.clientWidth / 2, act === 'in' ? 1.3 : 1 / 1.3); paint(); } }); - global.MeteorSky = { render: render, redraw: redraw, refit: refit, _detail: null }; + global.MeteorSky = { + render: render, redraw: redraw, refit: refit, + constNameByAbbr: constNameByAbbr, _detail: null, + }; })(window);