import { useI18n } from '@/lib/i18n'; // The sky, seen from underneath it. // // The map answers "where is the satellite over the earth". This answers "where // do I look", which during a pass is the question that matters: whether the // bird comes over the top or clips the horizon behind the house is something no // amount of azimuth and elevation digits conveys, and one glance at a polar // plot settles it. // // The projection is the one every satellite tracker uses and every operator // already reads: the centre is the zenith, the outer circle is the horizon, and // north is up. So the radius is (90 − elevation), NOT the elevation — a // satellite overhead is a dot in the middle, and a pass that stays near the rim // is one that never rises. export type SkyPoint = { at: string; az: number; el: number }; export function SkyPlot({ track, az, el, name, visible, size = 300 }: { // The pass, sampled from rise to set. Empty draws the dial alone, which is // still worth showing: it says where the antenna is pointing now. track: SkyPoint[]; // Where the satellite is at this instant, or null when it is not up. az?: number | null; el?: number | null; name?: string; visible?: boolean; size?: number; }) { const { t } = useI18n(); const R = size / 2 - 14; // the horizon circle const cx = size / 2, cy = size / 2; // Where a bearing and an elevation land on the dial. const pt = (azDeg: number, elDeg: number): [number, number] => { const r = R * (90 - Math.max(0, Math.min(90, elDeg))) / 90; const a = (azDeg * Math.PI) / 180; return [cx + r * Math.sin(a), cy - r * Math.cos(a)]; }; const rings = [15, 30, 45, 60, 75]; const path = track.length > 1 ? track.map((p, i) => `${i === 0 ? 'M' : 'L'}${pt(p.az, p.el).map((v) => v.toFixed(1)).join(' ')}`).join(' ') : ''; // Ticks every 10°, longer every 30°, so the rim reads as a compass rather // than a plain circle. const ticks = []; for (let a = 0; a < 360; a += 10) { const long = a % 30 === 0; const rad = (a * Math.PI) / 180; const r1 = R, r2 = R - (long ? 7 : 4); ticks.push( , ); } const here = az != null && el != null ? pt(az, el) : null; const start = track.length > 1 ? pt(track[0].az, track[0].el) : null; const end = track.length > 1 ? pt(track[track.length - 1].az, track[track.length - 1].el) : null; return ( {/* An arrowhead on the track: a pass has a direction, and which way the satellite is travelling decides where to point the antenna next. */} {rings.map((e) => ( ))} {ticks} {/* The cardinal cross. */} {([ { lbl: 'N', x: cx, y: cy - R - 3, anchor: 'middle' }, { lbl: 'S', x: cx, y: cy + R + 11, anchor: 'middle' }, { lbl: 'E', x: cx + R + 4, y: cy + 4, anchor: 'start' }, { lbl: 'W', x: cx - R - 4, y: cy + 4, anchor: 'end' }, ] as const).map((c) => ( {c.lbl} ))} {/* Elevation labels along the west arm, the way a tracker draws them. */} {[0, 30, 60].map((e) => ( {e}° ))} {/* The pass. */} {path && ( )} {start && } {end && } {/* Where it is now. Hollow and grey below the horizon: the numbers are still right, but nothing can be worked through the earth. */} {here && ( )} {/* The name and the look angles, in the middle, where a tracker puts them — big enough to read from the other side of the shack. */} {!!name && ( {name} )} {az != null && el != null && ( AZ {az.toFixed(1)}° EL {el.toFixed(1)}° )} ); }