#!/usr/bin/env python3
"""Generate the claims table on docs/globe-claims/index.html from one data set.

The book's closing chapter assigns the heliocentrists a hundred and three claims of its own
choosing. This is the list the globe model actually rests on: fourteen structural claims,
each with its number, the observation that tests it, what would falsify it, how far a reader
can check it without an agency, and a reference with enough detail to act on. Edit the data
here, re-run, and the page and tests/test_globe_claims.py follow.

    python3 build_globe_claims.py           -> rewrites the page table in place
    python3 build_globe_claims.py --check   -> prints the tally and exits
"""
import os
import re
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PAGE = os.path.join(ROOT, "docs", "globe-claims", "index.html")
SITE = "https://funwithscience.net/globe-deconstruction/"

# check level: "ground" = modest kit from a garden; "tools" = amateur astronomy tools, not agencies
GROUND, TOOLS = "ground", "tools"

# (id, title, claim with the number, the test, what would falsify it, check level, reader-check line, reference html, site pages)
CLAIMS = [
    ("shape", "Shape",
     "The Earth is an oblate spheroid: mean radius 6,371&nbsp;km, equatorial 6,378&nbsp;km, polar 6,357&nbsp;km, flattening 1/298.",
     "Horizon dip grows as &radic;<em>h</em> and hidden height as <em>D</em>&sup2;/2<em>R</em>; Polaris&rsquo;s altitude equals latitude; a degree of latitude is about 111&nbsp;km everywhere.",
     "A horizon that never hides objects bottom-up; Polaris not tracking latitude; a degree of latitude that changes length with position.",
     GROUND, "A camera at two known heights over calm water, or a sextant on Polaris and a road atlas.",
     "Moritz, &ldquo;Geodetic Reference System 1980,&rdquo; <a href='https://doi.org/10.1007/s001900050278'><em>Journal of Geodesy</em> 74 (2000) 128</a> for the ellipsoid constants; the hiding arithmetic on the <a href='https://funwithscience.net/refraction-solver/'>Reverse Refraction Solver</a>.",
     ["bottom-up-observations", "long-path-cases", "celestial-globes"]),
    ("spin", "Spin",
     "The Earth turns once on its axis in 23&nbsp;h&nbsp;56&nbsp;m&nbsp;4&nbsp;s (one solar day of 24&nbsp;h), west to east.",
     "Star trails at 15.04&deg; per hour about the celestial pole; the Foucault pendulum precessing at 15&deg;&nbsp;&times;&nbsp;sin(latitude) per hour; Coriolis on drifting buoys and in every weather model.",
     "Stars and planets turning at different rates; a pendulum that does not precess; no Coriolis.",
     GROUND, "A fixed camera on the night sky for an hour, or a long pendulum watched for one.",
     "IERS Conventions (2010), <a href='https://iers-conventions.obspm.fr/'>Technical Note 36</a>, ch. 1, for the rotation rate; the Coriolis drifter data on this site.",
     ["sky-rate-and-shadows", "earth-rotation", "coriolis-drifters"]),
    ("atmosphere", "An atmosphere held by gravity",
     "Air pressure falls with height by the hydrostatic (barometric) law: about 12&nbsp;hPa per 100&nbsp;m at sea level, halving by 5.5&nbsp;km.",
     "Any radiosonde sounding; a phone barometer carried up a hill; the same law predicting the altimeter setting every aircraft uses.",
     "A pressure profile that is not hydrostatic; pressure not falling with height.",
     GROUND, "A phone barometer and a 100&nbsp;m hill.",
     "<a href='https://ntrs.nasa.gov/citations/19770009539'>U.S. Standard Atmosphere 1976</a>, Table 1; the <a href='https://weather.uwyo.edu/upperair/sounding.shtml'>University of Wyoming sounding archive</a> for any station and date.",
     ["hydrostatics"]),
    ("refraction", "Refraction, computable from the weather",
     "Light through air bends toward denser air. The bend follows the temperature gradient, <em>k</em>&nbsp;=&nbsp;503&middot;(<em>P</em>/<em>T</em>&sup2;)&middot;(0.0342&nbsp;+&nbsp;d<em>T</em>/d<em>h</em>), is greatest along the horizon, lifts the setting Sun by about 34&prime; in standard air (<em>k</em>&nbsp;&asymp;&nbsp;0.13&ndash;0.17), and cannot exceed what a gradient the air can hold allows: past &minus;34.2&nbsp;&deg;C/km a layer overturns.",
     "Sunset and moonrise times against the almanac; hidden heights across water against the day&rsquo;s sounding; the mirage classes matching the measured inversion.",
     "Refraction that does not follow the measured gradient; the gradients a flat plane needs for the same pictures actually occurring in the air.",
     GROUND, "A timed sunset against the almanac, or a photograph across water with the nearest sounding.",
     "Bomford, <em>Geodesy</em> (4th ed., OUP 1980), &sect;3.19 on the refraction coefficient; the <a href='https://funwithscience.net/refraction-solver/'>Reverse Refraction Solver</a>, which prices any photograph on both surfaces; Ives (1968) on instrumented looming, cited on the Rampion page.",
     ["long-path-cases", "rampion", "mirrored-reflections", "sunlight-and-shadows"]),
    ("orbit", "Orbit",
     "The Earth orbits the Sun in 365.256 days at a mean distance of 149.6 million&nbsp;km.",
     "Stellar aberration of 20.5&Prime;, the same for every star, in phase with the orbit; annual parallax of the nearest stars (Proxima 0.77&Prime;, 61 Cygni 0.29&Prime;); the night sky shifting by 1&deg; a day.",
     "Zero aberration; zero parallax; a sky that does not advance through the year.",
     TOOLS, "Bessel&rsquo;s 1838 parallax of 61 Cygni has been repeated by amateurs with a CCD over a season; aberration needs a year of careful astrometry. Tools, not agencies.",
     "Bessel&rsquo;s letter to Herschel on the parallax of 61 Cygni, <a href='https://doi.org/10.1093/mnras/4.17.152'><em>MNRAS</em> 4 (1838) 152</a>; Bradley, &ldquo;An account of a new discovered motion of the fix&rsquo;d stars,&rdquo; <a href='https://doi.org/10.1098/rstl.1727.0064'><em>Phil. Trans.</em> 35 (1728) 637</a>; the astronomical unit from Venus radar, <a href='https://doi.org/10.1086/108900'>Muhleman, <em>AJ</em> 67 (1962) 277</a>.",
     ["sun-does-not-shrink", "sunlight-and-shadows"]),
    ("kepler", "Everything orbits the Sun; moons orbit planets",
     "Kepler&rsquo;s laws hold with one mass at the centre: <em>T</em>&sup2;&nbsp;&prop;&nbsp;<em>a</em>&sup3; for every planet about the Sun and for every moon about its planet, with the constant fixed by the central mass.",
     "A fortnight of Galilean-moon timings in a small telescope gives Jupiter&rsquo;s <em>GM</em> to a few per cent; Venus runs a full crescent-to-gibbous phase cycle; retrograde loops fall at opposition.",
     "A body whose period breaks <em>T</em>&sup2;&nbsp;&prop;&nbsp;<em>a</em>&sup3;; Venus without a full phase cycle; retrograde motion at the wrong time.",
     GROUND, "A 100-mm telescope, a clock, and two weeks on Jupiter&rsquo;s moons.",
     "Kepler, <em>Astronomia Nova</em> (1609) and <em>Harmonices Mundi</em> (1619); the CLEA <a href='https://public.gettysburg.edu/~marschal/clea/CLEAhome.html'>&ldquo;Revolution of the Moons of Jupiter&rdquo;</a> exercise for the amateur measurement.",
     ["jupiter-shadows", "eight-planets", "planetary-imaging"]),
    ("stars", "Stars are distant, not infinitely so",
     "The stars are suns at light-years, with small but measured motions: Barnard&rsquo;s star moves 10.4&Prime; a year; parallaxes are measured to microarcseconds by Gaia; the Plough&rsquo;s shape changes over tens of thousands of years.",
     "Two photographs of Barnard&rsquo;s star a year apart in a 100-mm telescope show the shift; the same spectral lines in starlight as in sunlight.",
     "Any star at a dome-scale distance (kilometres to thousands of kilometres) measured by parallax; stars with no proper motion at all.",
     TOOLS, "A small telescope and a camera, two nights a year apart, on Barnard&rsquo;s star; a grating for the spectrum.",
     "Barnard, &ldquo;A small star with large proper-motion,&rdquo; <a href='https://doi.org/10.1086/104156'><em>AJ</em> 29 (1916) 181</a>; Gaia Collaboration, &ldquo;Gaia Data Release 3,&rdquo; <a href='https://doi.org/10.1051/0004-6361/202243940'><em>A&amp;A</em> 674 (2023) A1</a>.",
     ["sky-rate-and-shadows"]),
    ("gas", "Gas obeys Newton&rsquo;s third law",
     "Exhaust is mass; momentum is conserved whatever the phase. A rocket&rsquo;s thrust is the mass rate times the exhaust speed and does not need anything outside to push on.",
     "Thrust measured in a vacuum chamber, and in flight above the atmosphere where ambient pressure is zero; thrust that <em>rises</em> as the ambient pressure falls, as the nozzle equation says.",
     "Thrust that falls to zero as the chamber pressure does; a measured engine that pushes only when there is air behind it.",
     GROUND, "A balloon cart in a bell jar under a pump, with the question the run must answer set out on the rockets page.",
     "Sutton &amp; Biblarz, <em>Rocket Propulsion Elements</em> (9th ed., Wiley 2017), ch. 2&ndash;3, the thrust equation and its pressure term; the in-flight measurements on <a href='https://funwithscience.net/globe-deconstruction/thrust-in-flight/'>Thrust, Measured in Flight</a>.",
     ["rockets-in-vacuum", "thrust-in-flight", "action-lab-footage"]),
    ("gravity", "Gravity: proportional to mass, and universal",
     "Weight is proportional to mass everywhere (<em>g</em>&nbsp;=&nbsp;9.81&nbsp;m/s&sup2; at the surface); the same inverse-square law, with one constant, gives the Moon&rsquo;s month, every satellite&rsquo;s period, and two tides a day with springs and neaps on the lunar cycle. <em>g</em> is 9.780&nbsp;m/s&sup2; at the equator and 9.832 at the poles.",
     "A spring scale against a balance shows weight tracking mass. A seconds pendulum runs about 2&nbsp;ms per swing slower at the equator than at 60&deg;&nbsp;N, 2.7&nbsp;ms against the pole &mdash; 1,000 swings against a stopwatch shows it &mdash; and the 0.53&nbsp;% pole-to-equator difference decomposes into 0.35&nbsp;% from the spin and 0.18&nbsp;% from the bulge, so one pendulum carried north tests claims 1, 2 and 9 together. The fall-off with distance is the part that needs a friend at altitude or the satellite periods.",
     "Weight not proportional to mass; the same <em>g</em> at every latitude; a satellite or moon whose period does not match <em>GM</em>/<em>r</em>&sup3;; tides not on the lunar cycle.",
     GROUND, "A pendulum, a stopwatch and a trip north; a tide table and a lunar calendar.",
     "Moritz (2000), as above, for the normal-gravity formula; Kater&rsquo;s pendulum, <a href='https://doi.org/10.1098/rstl.1818.0006'><em>Phil. Trans.</em> 108 (1818) 33</a>; <a href='https://tidesandcurrents.noaa.gov/'>NOAA Tides &amp; Currents</a> for the springs and neaps.",
     ["hydrostatics", "equator-flight"]),
    ("tilt", "Axial tilt, and the seasons",
     "The axis is tilted 23.44&deg; to the orbit. The Sun&rsquo;s noon altitude swings &plusmn;23.44&deg; through the year; the midnight sun runs poleward of 66.56&deg; in both hemispheres; day and night are equal everywhere at the equinox.",
     "A noon shadow measured on the solstices and equinoxes; the sunrise azimuth swinging 47&deg; along the horizon through the year at mid-latitudes; sunrise and sunset tables.",
     "A southern midnight sun that does not occur; unequal days at the equinox; a noon-Sun swing of other than 46.9&deg;.",
     GROUND, "A stick and its noon shadow on four days of the year.",
     "<a href='https://aa.usno.navy.mil/publications/asa'><em>The Astronomical Almanac</em></a> (USNO/HMNAO), section C, for obliquity and the Sun&rsquo;s declination; the <a href='https://aa.usno.navy.mil/data/RS_OneYear'>USNO rise/set tables</a> for the azimuth swing.",
     ["sunlight-and-shadows", "antarctica-magnetic"]),
    ("sunmoon", "The Sun and the Moon",
     "The Sun is 149.6 million&nbsp;km away and 1.39 million&nbsp;km across, a constant 0.53&deg; wide from sunrise to sunset; the Moon is 384,400&nbsp;km away and 3,474&nbsp;km across, a sphere lit by the Sun, its phase set by the Sun&ndash;Moon&ndash;Earth angle and its terminator a curve only a lit sphere shows.",
     "The Sun&rsquo;s angular size measured through the day with a filter; the Moon&rsquo;s phase against the Sun&rsquo;s direction; the lunar distance from a timed occultation seen from two places.",
     "A Sun that visibly shrinks toward the horizon; a phase inconsistent with the Sun&rsquo;s position; a terminator that is not the edge of a lit sphere.",
     GROUND, "A solar filter and a camera at two times of day; a month of Moon photographs with the Sun&rsquo;s position noted.",
     "<a href='https://aa.usno.navy.mil/publications/asa'><em>The Astronomical Almanac</em></a>, sections C and D; the angular-size measurement on <a href='https://funwithscience.net/globe-deconstruction/sun-does-not-shrink/'>The Sun Does Not Shrink</a> and the terminator geometry on <a href='https://funwithscience.net/globe-deconstruction/moon-sphericity/'>Is the Moon a Ball?</a>.",
     ["sun-does-not-shrink", "moon-sphericity", "full-moon-lighting", "luminaries"]),
    ("eclipses", "Eclipse geometry",
     "A lunar eclipse shows a circular Earth shadow about 2.7 Moon-diameters wide whatever the Moon&rsquo;s altitude; a solar eclipse&rsquo;s track of totality is 100&ndash;270&nbsp;km wide and is predicted to the kilometre and the second centuries ahead.",
     "The next lunar eclipse, photographed; the next solar eclipse, met at the predicted place and time.",
     "A non-circular umbra; a shadow edge whose curvature changes with the Moon&rsquo;s altitude; a predicted eclipse that fails to occur where and when stated.",
     GROUND, "A camera at the next lunar eclipse; a map and a clock at the next solar one.",
     "Espenak &amp; Meeus, <a href='https://eclipse.gsfc.nasa.gov/LEcat5/LEcatalog.html'><em>Five Millennium Canon of Lunar Eclipses</em></a> (NASA TP-2009-214172); the eclipse pages on this site.",
     ["selenelion", "eclipse-trajectory", "luminaries"]),
    ("hemispheres", "Two hemispheres, symmetric",
     "The southern sky turns clockwise about a southern pole with its own circumpolar stars; Coriolis reverses sign; the Sun crosses the northern sky right to left; both hemispheres see a midnight sun at the same latitude band.",
     "A star-trail photograph from the southern hemisphere; a southern sunrise-to-sunset time-lapse; the drifter data by hemisphere.",
     "A southern sky that behaves like a stretched northern one: no southern pole of rotation, or the same sense of rotation at both poles.",
     GROUND, "A camera on the night sky from anywhere south of the equator, or a friend there with one.",
     "The <a href='https://funwithscience.net/globe-deconstruction/coriolis-drifters/'>Coriolis drifter analysis</a> on this site, split by hemisphere; any southern-hemisphere star-trail photograph with its location.",
     ["coriolis-drifters", "celestial-globes"]),
    ("surface", "Surface geometry is spherical",
     "Distances and directions on the surface follow spherical, not planar, geometry: great circles are the shortest routes, a triangle&rsquo;s angles sum to more than 180&deg;, and Sydney&ndash;Santiago is 11,300&nbsp;km, flown non-stop in 12&ndash;14&nbsp;hours.",
     "Published flight times against great-circle distances on many routes at once, southern routes included; surveyed triangulation networks that close only with spherical excess.",
     "A flight time inconsistent with the spherical distance; a large surveyed triangle whose angles sum to exactly 180&deg;.",
     GROUND, "A timetable and the haversine formula, for as many routes as patience allows.",
     "Any airline timetable against the great-circle distance (the <a href='https://www.movable-type.co.uk/scripts/latlong.html'>haversine calculator at Movable Type</a> computes it); Torge &amp; M&uuml;ller, <em>Geodesy</em> (4th ed., de Gruyter 2012), ch. 2, on spherical excess in triangulation.",
     ["equator-flight", "antarctica-magnetic"]),
]

PAGES = {
    "bottom-up-observations": "Bottom Up Observations", "long-path-cases": "Chicago &amp; Pontchartrain", "celestial-globes": "Celestial Globes Exposed",
    "sky-rate-and-shadows": "The Sky Turns at One Rate", "earth-rotation": "Does the Earth Spin?", "coriolis-drifters": "Coriolis Drifters",
    "hydrostatics": "Hydrostatics", "rampion": "The Black Swan (Rampion)", "mirrored-reflections": "Mirrored Reflections",
    "sunlight-and-shadows": "Sunlight &amp; Shadows", "sun-does-not-shrink": "The Sun Does Not Shrink", "jupiter-shadows": "Jupiter's Shadows",
    "eight-planets": "The Eight-Planet Photograph", "planetary-imaging": "Stacked &amp; Sharpened", "rockets-in-vacuum": "Rockets Don't Push Against Air",
    "thrust-in-flight": "Thrust, Measured in Flight", "action-lab-footage": "Action Lab Footage", "equator-flight": "Equator Flight Data",
    "antarctica-magnetic": "The Compass in Antarctica", "moon-sphericity": "Is the Moon a Ball?", "full-moon-lighting": "Full-Moon Lighting",
    "luminaries": "Where's the Moon's Silhouette?", "selenelion": "The Selenelion", "eclipse-trajectory": "The Eclipse Trajectory",
}


def validate():
    assert len(CLAIMS) == 14
    ids = [c[0] for c in CLAIMS]; assert len(set(ids)) == 14
    for c in CLAIMS:
        assert c[5] in (GROUND, TOOLS), c[0]
        assert "href=" in c[7], c[0]
        for p in c[8]:
            assert p in PAGES and os.path.isdir(os.path.join(ROOT, "docs", p)), (c[0], p)


def tally():
    return {GROUND: sum(1 for c in CLAIMS if c[5] == GROUND), TOOLS: sum(1 for c in CLAIMS if c[5] == TOOLS)}


def table_html():
    o = ['<table class="cmp claims"><thead><tr><th>#</th><th>The claim, with its number</th><th>The test</th><th>What would falsify it</th><th>Reader-check</th></tr></thead><tbody>']
    for i, (cid, title, claim, test, falsify, level, reader, ref, pages) in enumerate(CLAIMS, 1):
        chip = '<span class="lv ground">from the ground</span>' if level == GROUND else '<span class="lv tools">tools, not agencies</span>'
        links = ", ".join(f'<a href="{SITE}{p}/">{PAGES[p]}</a>' for p in pages)
        o.append(f'<tr id="c-{cid}"><td class="metric">{i}</td><td><strong>{title}.</strong> {claim}<br><span class="ref">{ref}</span></td>'
                 f'<td>{test}<br><span class="onsite">On this site: {links}</span></td><td>{falsify}</td><td>{chip}<br>{reader}</td></tr>')
    o.append("</tbody></table>")
    return "\n".join(o)


def inject(html, key, body):
    pat = re.compile(rf"(<!--CLAIMS:{key}-->).*?(<!--/CLAIMS:{key}-->)", re.S)
    assert pat.search(html), key
    return pat.sub(lambda m: m.group(1) + "\n" + body + "\n" + m.group(2), html)


def main():
    validate()
    if "--check" in sys.argv:
        print(len(CLAIMS), "claims", tally()); return
    html = open(PAGE, encoding="utf-8").read()
    html = inject(html, "TABLE", table_html())
    open(PAGE, "w", encoding="utf-8").write(html)
    print("wrote", os.path.relpath(PAGE, ROOT), tally())


if __name__ == "__main__":
    main()
