Files
astronomy/demo/nodejs/calendar/calendar.ts
2021-05-16 13:32:19 -04:00

456 lines
14 KiB
TypeScript

/*
calendar.ts - Don Cross - 2021-05-09
A demo of using Astronomy Engine to find a series of
interesting events for a calendar.
*/
import {
AstroTime, Body, Observer,
GeoVector, EquatorFromVector, Constellation, ConstellationInfo,
PairLongitude,
SearchHourAngle,
SearchLocalSolarEclipse, NextLocalSolarEclipse, LocalSolarEclipseInfo,
SearchLunarApsis, NextLunarApsis, Apsis,
SearchLunarEclipse, NextLunarEclipse, LunarEclipseInfo,
SearchMaxElongation,
SearchMoonQuarter, NextMoonQuarter, MoonQuarter,
SearchPlanetApsis, NextPlanetApsis,
SearchPeakMagnitude,
SearchRelativeLongitude,
SearchRiseSet,
SearchTransit, NextTransit, TransitInfo,
Seasons,
} from "./astronomy";
class AstroEvent {
constructor(
public time: AstroTime,
public title: string,
public creator: AstroEventEnumerator)
{}
}
interface AstroEventEnumerator {
FindFirst(startTime: AstroTime): AstroEvent;
FindNext(): AstroEvent;
}
class EventCollator implements AstroEventEnumerator {
private eventQueue: AstroEvent[];
constructor(private enumeratorList: AstroEventEnumerator[]) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.eventQueue = [];
for (let enumerator of this.enumeratorList)
this.InsertEvent(enumerator.FindFirst(startTime));
return this.FindNext();
}
FindNext(): AstroEvent {
if (this.eventQueue.length === 0)
return null;
const evt = this.eventQueue.shift();
const another = evt.creator.FindNext();
this.InsertEvent(another);
return evt;
}
InsertEvent(evt: AstroEvent): void {
if (evt !== null) {
// Insert the event in time order -- after anything that happens before it.
let i = 0;
while (i < this.eventQueue.length && this.eventQueue[i].time.tt < evt.time.tt)
++i;
this.eventQueue.splice(i, 0, evt);
}
}
}
class RiseSetEnumerator implements AstroEventEnumerator {
private nextSearchTime: AstroTime;
constructor(private observer: Observer, private body: Body, private direction: number, private title: string) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.nextSearchTime = SearchRiseSet(this.body, this.observer, this.direction, startTime, 366.0);
if (this.nextSearchTime)
return new AstroEvent(this.nextSearchTime, this.title, this);
return null;
}
FindNext(): AstroEvent {
if (this.nextSearchTime) {
const startTime = this.nextSearchTime.AddDays(0.01);
return this.FindFirst(startTime);
}
return null;
}
}
class SeasonEnumerator implements AstroEventEnumerator {
private slist: AstroEvent[];
private year: number;
private index: number;
FindFirst(startTime: AstroTime): AstroEvent {
this.year = startTime.date.getUTCFullYear();
this.LoadYear(this.year);
while (this.index < this.slist.length && this.slist[this.index].time.tt < startTime.tt)
++this.index;
return this.FindNext();
}
FindNext(): AstroEvent {
if (this.index === this.slist.length)
this.LoadYear(++this.year);
return this.slist[this.index++];
}
private LoadYear(year: number): void {
const seasons = Seasons(year);
this.slist = [
new AstroEvent(seasons.mar_equinox, 'March equinox', this),
new AstroEvent(seasons.jun_solstice, 'June solstice', this),
new AstroEvent(seasons.sep_equinox, 'September equinox', this),
new AstroEvent(seasons.dec_solstice, 'December solstice', this)
];
this.index = 0;
}
}
class MoonQuarterEnumerator implements AstroEventEnumerator {
private mq: MoonQuarter;
FindFirst(startTime: AstroTime): AstroEvent {
this.mq = SearchMoonQuarter(startTime);
return this.MakeEvent();
}
FindNext(): AstroEvent {
this.mq = NextMoonQuarter(this.mq);
return this.MakeEvent();
}
private MakeEvent(): AstroEvent {
return new AstroEvent(
this.mq.time,
['new moon', 'first quarter', 'full moon', 'third quarter'][this.mq.quarter],
this
);
}
}
class ConjunctionOppositionEnumerator implements AstroEventEnumerator {
private title: string;
private nextTime: AstroTime;
constructor(private body: Body, private targetRelLon: number, kind: string) {
this.title = `${body} ${kind}`; // e.g. "Jupiter opposition" or "Venus inferior conjunction"
}
FindFirst(startTime: AstroTime): AstroEvent {
this.nextTime = startTime;
return this.FindNext();
}
FindNext(): AstroEvent {
const time = SearchRelativeLongitude(this.body, this.targetRelLon, this.nextTime);
this.nextTime = time.AddDays(1);
return new AstroEvent(time, this.title, this);
}
}
class MaxElongationEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
constructor(private body: Body) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.nextTime = startTime;
return this.FindNext();
}
FindNext(): AstroEvent {
const elon = SearchMaxElongation(this.body, this.nextTime);
this.nextTime = elon.time.AddDays(1);
return new AstroEvent(
elon.time,
`${this.body} max ${elon.visibility} elongation: ${elon.elongation.toFixed(2)} degrees from Sun`,
this);
}
}
class VenusPeakMagnitudeEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
FindFirst(startTime: AstroTime): AstroEvent {
this.nextTime = startTime;
return this.FindNext();
}
FindNext(): AstroEvent {
const illum = SearchPeakMagnitude(Body.Venus, this.nextTime);
const rlon = PairLongitude(Body.Venus, Body.Sun, illum.time);
this.nextTime = illum.time.AddDays(1);
return new AstroEvent(
illum.time,
`Venus peak magnitude ${illum.mag.toFixed(2)} in ${(rlon < 180) ? 'evening' : 'morning'} sky`,
this
);
}
}
class LunarEclipseEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
FindFirst(startTime: AstroTime): AstroEvent {
const info = SearchLunarEclipse(startTime);
return this.MakeEvent(info);
}
FindNext(): AstroEvent {
const info = NextLunarEclipse(this.nextTime);
return this.MakeEvent(info);
}
private MakeEvent(info: LunarEclipseInfo): AstroEvent {
this.nextTime = info.peak;
return new AstroEvent(info.peak, `${info.kind} lunar eclipse`, this);
}
}
class LocalSolarEclipseEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
constructor(private observer: Observer) {}
FindFirst(startTime: AstroTime): AstroEvent {
const info = SearchLocalSolarEclipse(startTime, this.observer);
return this.MakeEvent(info);
}
FindNext(): AstroEvent {
const info = NextLocalSolarEclipse(this.nextTime, this.observer);
return this.MakeEvent(info);
}
private MakeEvent(info: LocalSolarEclipseInfo): AstroEvent {
this.nextTime = info.peak.time;
return new AstroEvent(info.peak.time, `${info.kind} solar eclipse peak at ${info.peak.altitude.toFixed(2)} degrees altitude`, this);
}
}
class TransitEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
constructor(private body: Body) {}
FindFirst(startTime: AstroTime): AstroEvent {
const info = SearchTransit(this.body, startTime);
return this.MakeEvent(info);
}
FindNext(): AstroEvent {
const info = NextTransit(this.body, this.nextTime);
return this.MakeEvent(info);
}
private MakeEvent(info: TransitInfo): AstroEvent {
this.nextTime = info.peak;
return new AstroEvent(info.peak, `transit of ${this.body}`, this);
}
}
class LunarApsisEnumerator implements AstroEventEnumerator {
private apsis: Apsis;
FindFirst(startTime: AstroTime): AstroEvent {
this.apsis = SearchLunarApsis(startTime);
return this.MakeEvent();
}
FindNext(): AstroEvent {
this.apsis = NextLunarApsis(this.apsis);
return this.MakeEvent();
}
private MakeEvent(): AstroEvent {
const kind = (this.apsis.kind === 0) ? 'perigee' : 'apogee';
return new AstroEvent(this.apsis.time, `lunar ${kind} at ${this.apsis.dist_km.toFixed(0)} km`, this);
}
}
class PlanetApsisEnumerator implements AstroEventEnumerator {
private apsis: Apsis;
constructor(private body: Body) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.apsis = SearchPlanetApsis(this.body, startTime);
return this.MakeEvent();
}
FindNext(): AstroEvent {
this.apsis = NextPlanetApsis(this.body, this.apsis);
return this.MakeEvent();
}
private MakeEvent(): AstroEvent {
const kind = (this.apsis.kind === 0) ? 'perihelion' : 'aphelion';
return new AstroEvent(this.apsis.time, `${this.body} ${kind} at ${this.apsis.dist_au.toFixed(4)} AU`, this);
}
}
class BodyCulminationEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
constructor(private observer: Observer, private body: Body) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.nextTime = startTime;
return this.FindNext();
}
FindNext(): AstroEvent {
const info = SearchHourAngle(this.body, this.observer, 0, this.nextTime);
this.nextTime = info.time.AddDays(0.5);
return new AstroEvent(info.time, `${this.body} culminates ${info.hor.altitude.toFixed(2)} degrees above the horizon`, this);
}
}
class ConstellationEnumerator implements AstroEventEnumerator {
private nextTime: AstroTime;
private currentConst: ConstellationInfo;
constructor(private body: Body, private dayIncrement: number) {}
FindFirst(startTime: AstroTime): AstroEvent {
this.nextTime = startTime;
this.currentConst = this.BodyConstellation(startTime);
return this.FindNext();
}
FindNext(): AstroEvent {
const tolerance = 0.1 / (24 * 3600); // one tenth of a second, expressed in days
// Step through one time increment at a time until we see a constellation change.
let t1 = this.nextTime;
let c1 = this.currentConst;
for(;;) {
let t2 = t1.AddDays(this.dayIncrement);
let c2 = this.BodyConstellation(t2);
if (c1.symbol === c2.symbol) {
t1 = t2;
} else {
// The body moved from one constellation to another during this time step.
// Narrow in on the exact moment by doing a binary search.
for(;;) {
let dt = t2.ut - t1.ut;
let tx = t1.AddDays(dt/2);
let cx = this.BodyConstellation(tx);
if (cx.symbol === c1.symbol) {
t1 = tx;
} else {
if (dt < tolerance) {
// We have found the transition time within tolerance.
// Always end the search inside the new constellation.
this.nextTime = tx;
this.currentConst = cx;
return new AstroEvent(tx, `${this.body} moves from ${c1.name} to ${cx.name}`, this);
} else {
t2 = tx;
}
}
}
}
}
}
private BodyConstellation(time: AstroTime): ConstellationInfo {
const vec = GeoVector(this.body, time, false);
const equ = EquatorFromVector(vec);
return Constellation(equ.ra, equ.dec);
}
}
function RunTest(): void {
const startTime = new AstroTime(new Date('2021-05-12T00:00:00Z'));
const observer = new Observer(28.6, -81.2, 10.0);
var enumeratorList: AstroEventEnumerator[] = [
new RiseSetEnumerator(observer, Body.Sun, +1, 'sunrise'),
new RiseSetEnumerator(observer, Body.Sun, -1, 'sunset'),
new RiseSetEnumerator(observer, Body.Moon, +1, 'moonrise'),
new RiseSetEnumerator(observer, Body.Moon, -1, 'moonset'),
new BodyCulminationEnumerator(observer, Body.Sun),
new BodyCulminationEnumerator(observer, Body.Moon),
new SeasonEnumerator(),
new MoonQuarterEnumerator(),
new VenusPeakMagnitudeEnumerator(),
new LunarEclipseEnumerator(),
new LocalSolarEclipseEnumerator(observer),
new LunarApsisEnumerator()
];
// Inferior and superior conjunctions of inner planets.
// Maximum elongation of inner planets.
// Transits of inner planets.
for (let body of [Body.Mercury, Body.Venus]) {
enumeratorList.push(
new ConjunctionOppositionEnumerator(body, 0, 'inferior conjunction'),
new ConjunctionOppositionEnumerator(body, 180, 'superior conjunction'),
new MaxElongationEnumerator(body),
new TransitEnumerator(body)
);
}
// Conjunctions and oppositions of outer planets.
for (let body of [Body.Mars, Body.Jupiter, Body.Saturn, Body.Uranus, Body.Neptune, Body.Pluto]) {
enumeratorList.push(
new ConjunctionOppositionEnumerator(body, 0, 'opposition'),
new ConjunctionOppositionEnumerator(body, 180, 'conjunction')
);
}
// Perihelion and aphelion of all planets.
// Constellation change of all planets.
for (let body of [Body.Mercury, Body.Venus, Body.Earth, Body.Mars, Body.Jupiter, Body.Saturn, Body.Uranus, Body.Neptune, Body.Pluto]) {
enumeratorList.push(new PlanetApsisEnumerator(body));
if (body !== Body.Earth) {
enumeratorList.push(new ConstellationEnumerator(body, 1));
}
}
const collator = new EventCollator(enumeratorList);
const stopYear = startTime.date.getUTCFullYear() + 12;
let evt:AstroEvent = collator.FindFirst(startTime);
while (evt !== null && evt.time.date.getUTCFullYear() < stopYear) {
console.log(`${evt.time} ${evt.title}`);
evt = collator.FindNext();
}
}
RunTest();