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

server/src/aurora.ts

v1.0.0
moonrise/server/src/aurora.ts history · blame · raw

47 lines · 2012 bytes

 1// Aurora alert rules. Pure functions; the Worker supplies data and sends.
 2
 3/**
 4 * Geomagnetic latitude of the auroral oval's equatorward edge for a Kp value,
 5 * after NOAA SWPC's Kp-to-latitude chart (66.5° at Kp 0, about 2° lower per Kp).
 6 */
 7export function ovalEdge(kp: number): number {
 8  return 66.5 - 2.04 * kp;
 9}
10
11/** Aurora low on the poleward horizon is often visible about this far beyond the oval's edge. */
12export const HORIZON_MARGIN = 6;
13
14/** Lowest geomagnetic latitude (absolute) where aurora may be seen on the horizon. */
15export function visibleFrom(kp: number): number {
16  return ovalEdge(kp) - HORIZON_MARGIN;
17}
18
19/** 5° bands, identified by their equatorward edge: 52 covers 52–57°N, -57 covers 52–57°S. */
20export function bandFor(geomagneticLatitude: number): number {
21  const b = Math.floor(Math.abs(geomagneticLatitude) / 5) * 5;
22  return geomagneticLatitude < 0 ? -(b + 5) : b;
23}
24
25/** Bands whose poleward edge reaches the visible region for this Kp. */
26export function alertBands(kp: number, bands: number[]): number[] {
27  const limit = visibleFrom(kp);
28  return bands.filter((b) => (b >= 0 ? b + 5 : -b) > limit);
29}
30
31/** Local hours treated as dark enough to bother someone: 20:00 to 04:00. */
32export function isNight(now: Date, utcOffsetMinutes: number): boolean {
33  const hour = (now.getUTCHours() + now.getUTCMinutes() / 60 + utcOffsetMinutes / 60 + 48) % 24;
34  return hour >= 20 || hour < 4;
35}
36
37/** One alert per band per 12-hour UTC window. */
38export function eventKey(now: Date): string {
39  const d = now.toISOString().slice(0, 10);
40  return `${d}${now.getUTCHours() < 12 ? "a" : "b"}`;
41}
42
43/** Kp to act on: the larger of now and the forecast for the next 3 hours. */
44export function actionableKp(kpNow: number, forecast: { start: Date; kp: number }[], now: Date): number {
45  const soon = forecast.filter((f) => f.start.getTime() <= now.getTime() + 3 * 3600_000 && f.start.getTime() + 3 * 3600_000 > now.getTime());
46  return Math.max(kpNow, ...soon.map((f) => f.kp));
47}