#!/usr/bin/env python3
"""Two tracks for the same Moon: where it was seen, and where it actually was.

The multiple-exposure photographs of this eclipse show the Moon stepping down
toward the ridge. Drawing the unrefracted position beside each step answers
the question the animation could not: how far apart are the two, and is the
gap the sort of thing anybody could have noticed?

The photograph supplies its own ruler. The Moon was 30.0 arcmin across that
morning, and horizon refraction is 34.5 arcmin, so at the bottom of the run
the true Moon is more than one full disc-width below the one in the frame --
while at the top of the run the two are indistinguishable. That contrast is
the whole character of refraction: nothing at altitude, everything at the
horizon.

Positions from docs/selenelion/selenelion-frames.json (JPL DE421 via
scripts/selenelion_2011.py). Nothing here is traced from any photograph.

    python3 refraction_tracks.py
"""

import json, os

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
SRC = os.path.join(ROOT, 'docs', 'selenelion', 'selenelion-frames.json')
OUT = os.path.join(ROOT, 'docs', 'selenelion', 'img', 'fig-refraction-tracks.svg')

W, H = 880, 500
INK, DIM, FAINT = '#2E4057', '#6E675B', '#a09a8c'
RED, MOON, EDGE = '#c0392b', '#6b3a33', '#8d8677'
SHADOWC = '#4a2225'
SUNRAY = '#d9a441'
SKY, GROUND, RULE = '#eaf1f8', '#dfe6d8', '#cbd6e2'

AZ0, AZ1 = 289.5, 300.5          # degrees of azimuth across the frame
ALT0, ALT1 = -1.1, 13.2           # degrees of altitude, bottom to top
L, R, T, B = 66.0, 850.0, 74.0, 330.0


def esc(s):
    return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')


def txt(x, y, s, size=11, fill=INK, anchor='middle', weight=None):
    w = f' font-weight="{weight}"' if weight else ''
    return (f'<text x="{x:.1f}" y="{y:.1f}" font-size="{size}" text-anchor="{anchor}" '
            f'fill="{fill}"{w}>{esc(s)}</text>')


def X(az):
    return L + (az - AZ0) / (AZ1 - AZ0) * (R - L)


def Y(alt):
    return B - (alt - ALT0) / (ALT1 - ALT0) * (B - T)


def build():
    F = json.load(open(SRC))['frames']
    for f in F:                                   # the shadow's lower edge, both ways
        f['_edge'] = f['uedge']                   # refracted at the edge's own altitude
        f['_edgeg'] = f['uedgeg']
    run = [f for f in F if AZ0 <= f['maz'] <= AZ1 and f['malt'] >= 0.0 and f['malt'] <= ALT1]
    dia = run[-1]['sdm'] * 2 * 60                     # arcmin: the Moon is the ruler
    rpx = (Y(0) - Y(run[-1]['sdm'] * 2)) / 2

    def track(key, colour, width, dash=None):
        pts = ' '.join(f'{X(f["maz"]):.1f},{Y(f[key]):.1f}' for f in run)
        d = f' stroke-dasharray="{dash}"' if dash else ''
        return f'<polyline points="{pts}" fill="none" stroke="{colour}" stroke-width="{width}"{d}/>'

    o = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">',
         f'<rect width="{W}" height="{H}" fill="#fff"/>',
         txt(W / 2, 24, 'Both are lifted: the Moon, and Earth\u2019s shadow on it',
             13.5, INK, weight='700'),
         txt(W / 2, 42, '10 December 2011 over Santa Fe \u2014 the last hour of the descent. '
                        'Beside each step: how far the Moon was lifted, and how far the shadow was.',
             10.5, FAINT),
         f'<rect x="{L}" y="{T}" width="{R-L}" height="{Y(0)-T:.1f}" fill="{SKY}" stroke="{RULE}" rx="4"/>',
         f'<rect x="{L}" y="{Y(0):.1f}" width="{R-L}" height="{B-Y(0):.1f}" fill="{GROUND}" '
         f'stroke="{RULE}" rx="4"/>',
         f'<line x1="{L}" y1="{Y(0):.1f}" x2="{R}" y2="{Y(0):.1f}" stroke="{DIM}" stroke-width="1.5"/>',
         txt(L + 6, Y(0) - 7, 'true horizon', 9.5, DIM, 'start')]
    for a in (2, 4, 6, 8, 10, 12):
        o.append(f'<line x1="{L}" y1="{Y(a):.1f}" x2="{R}" y2="{Y(a):.1f}" stroke="{RULE}" '
                 f'stroke-dasharray="2 4"/>')
        o.append(txt(L - 8, Y(a) + 4, f'{a}\u00b0', 9.5, FAINT, 'end'))

    # the shadow's lower edge -- where Earth's shadow reaches down to. Its place
    # in the sky is set by the Sun, so its lift IS the solar half of the story.
    o.append('<g>')
    o.append(track('_edge', SHADOWC, 2.0))
    o.append(track('_edgeg', SHADOWC, 1.4, '5 4'))
    o.append('</g>')
    # the Moon
    o.append(track('malt', EDGE, 1.8))
    o.append(track('maltg', RED, 1.4, '5 4'))

    step = max(1, len(run) // 6)
    for i, f in enumerate(run):
        if i % step and i != len(run) - 1:
            continue
        xs = X(f['maz'])
        ys, yt = Y(f['malt']), Y(f['maltg'])
        es, et = Y(f['_edge']), Y(f['_edgeg'])
        # the two lifts, as vertical ties
        o.append(f'<line x1="{xs-13:.1f}" y1="{et:.1f}" x2="{xs-13:.1f}" y2="{es:.1f}" '
                 f'stroke="{SHADOWC}" stroke-width="3.4" stroke-opacity="0.5"/>')
        o.append(f'<line x1="{xs+13:.1f}" y1="{yt:.1f}" x2="{xs+13:.1f}" y2="{ys:.1f}" '
                 f'stroke="{EDGE}" stroke-width="3.4" stroke-opacity="0.6"/>')
        o.append(f'<circle cx="{xs:.1f}" cy="{ys:.1f}" r="{rpx:.1f}" fill="{MOON}" '
                 f'stroke="{EDGE}" stroke-width="1"/>')
        o.append(f'<circle cx="{xs:.1f}" cy="{yt:.1f}" r="{rpx:.1f}" fill="none" '
                 f'stroke="{RED}" stroke-width="1.1" stroke-dasharray="3 3"/>')
        dy = 0 if i % (2 * step) == 0 else 26
        o.append(txt(xs, ys - rpx - 24 - dy, f['t'], 9.5, FAINT))
        o.append(txt(xs, ys - rpx - 12 - dy,
                     f"{(f['malt']-f['maltg'])*60:.0f}\u2032 / {(f['_edge']-f['_edgeg'])*60:.0f}\u2032",
                     9.5, INK, weight='600'))

    o.append(txt(R - 14, Y(12.4), 'the Moon \u2014 solid as seen, dashed as it was',
                 11, EDGE, 'end', '600'))
    o.append(txt(R - 14, Y(11.4), 'Earth\u2019s shadow, lower edge \u2014 the same two',
                 11, SHADOWC, 'end', '600'))


    y = B + 34
    for t, col, sz, wt in [
        ('The Moon is lifted by refraction as it falls. So is Earth\u2019s shadow \u2014 its place in '
         'the sky is set by the Sun, so the shadow\u2019s lift is the solar half.', DIM, 11.5, None),
        ('The two figures above each step are those lifts. They run 5\u2032 and 5\u2032 at ten degrees '
         'up, 12\u2032 and 13\u2032 at four degrees, 30\u2032 and 32\u2032 near the horizon.', DIM, 11.5, None),
        ('Both grow, and they grow together. Near the horizon the shadow\u2019s edge is lifted slightly '
         'more than the Moon, by 2.3\u2032, because that edge hangs lower and sits in thicker air.', RED, 11.5, '700'),
        ('That difference is the entire effect of refraction on the eclipse itself: two arcminutes on a '
         '30\u2032 disc, at the very end. It cannot move a shadow from one limb to the other.', RED, 12, '700'),
    ]:
        o.append(txt(W / 2, y, t, sz, col, weight=wt)); y += 18

    o.append(txt(W / 2, H - 12, f'The Moon was {dia:.1f}\u2032 across that morning. Positions from '
                                f'JPL DE421; refraction from Bennett (1982). '
                                f'scripts/refraction_tracks.py', 9.5, FAINT))
    o.append('</svg>')

    with open(OUT, 'w') as fh:
        fh.write('\n'.join(o))
    print(f'wrote {OUT}')
    for f in (run[0], run[len(run)//2], run[-1]):
        print(f"  {f['t']}  Moon lifted {(f['malt']-f['maltg'])*60:5.1f}',"
              f"  shadow lifted {(f['_edge']-f['_edgeg'])*60:5.1f}'")


if __name__ == '__main__':
    build()
