@openwaters/almanac 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/dist/events.js +175 -36
- package/dist/moon.js +2172 -119
- package/dist/sun.d.ts +9 -0
- package/dist/sun.js +119 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,31 +26,32 @@ cover positions, a 228-hour sky track, short/year/polar event windows, full-rang
|
|
|
26
26
|
moon phases, next/previous/range lunar and solar eclipse searches, including empty
|
|
27
27
|
windows, and solar obscuration over a 228-hour track.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Event searches find altitude extrema with Brent's method and altitude crossings
|
|
30
|
+
with a cosine-seeded secant solver, each root proven inside a half-second
|
|
31
|
+
bracket, and the TypeScript Sun series evaluates its terms as straight-line
|
|
32
|
+
arithmetic. Median query time relative to v0.4.1:
|
|
33
33
|
|
|
34
34
|
| Workload | TypeScript: less time | Swift: less time |
|
|
35
35
|
| --- | ---: | ---: |
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| Sun events / 228 hours |
|
|
39
|
-
| Moon events / 228 hours |
|
|
40
|
-
|
|
|
41
|
-
| Lunar eclipses / 1950–2100 |
|
|
36
|
+
| Sun events / year | 73.4% | 62.8% |
|
|
37
|
+
| Moon events / year | 57.6% | 57.4% |
|
|
38
|
+
| Sun events / 228 hours | 73.4% | 61.9% |
|
|
39
|
+
| Moon events / 228 hours | 57.1% | 57.0% |
|
|
40
|
+
| Sun positions / 1,024 hours | 35.5% | — |
|
|
41
|
+
| Lunar eclipses / 1950–2100 | 24.6% | — |
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Swift already evaluated the Sun series efficiently, so its gains are confined to
|
|
44
|
+
the searches. Each comparison builds both revisions with the same harness and toolchain, then takes seven interleaved process pairs with 300 ms warmup per process. Build and startup time are excluded. Timings vary by machine; the shared correctness fixtures and parity tolerances remain the accuracy gates.
|
|
44
45
|
|
|
45
46
|
Reproduce the comparison locally from the repository root with mise 2026.9.1 or newer after installing the configured Node and Swift versions in `mise.toml`:
|
|
46
47
|
|
|
47
48
|
```bash
|
|
48
49
|
mise install
|
|
49
50
|
mise exec -- npm ci --prefix typescript
|
|
50
|
-
mise exec -- node benchmarks/run.mjs --base v0.
|
|
51
|
+
mise exec -- node benchmarks/run.mjs --base v0.4.1
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
Pass `--skip '^solar/'` when the base predates 0.4.0, which is when the solar eclipse workloads arrived.
|
|
54
55
|
|
|
55
56
|
CI runs the harness on code changes and fails on **median regressions over 20%**.
|
|
56
57
|
Results include timing tables, raw samples, checksums, and revision/toolchain
|
package/dist/events.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// a grid can straddle both halves of a grazing pair (the polar-onset day where
|
|
9
9
|
// the Sun sets at 08:00 and rises again at 08:16) and report neither. Here each
|
|
10
10
|
// daily cycle's altitude extrema are located numerically and every monotonic
|
|
11
|
-
// segment between consecutive extrema is
|
|
12
|
-
// minutes apart is bracketed by construction rather than by
|
|
11
|
+
// segment between consecutive extrema is solved for each target level, so a
|
|
12
|
+
// pair of crossings minutes apart is bracketed by construction rather than by
|
|
13
|
+
// luck of the grid.
|
|
13
14
|
// Hour angle supplies only the initial bracket: HA 0/12h are not the exact
|
|
14
15
|
// extrema, because declination, distance and lunar parallax all drift within a
|
|
15
16
|
// cycle, and toward the poles the daily altitude cycle flattens until the
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
// and `NextMoonQuarter` (~5362).
|
|
23
24
|
import { assertObserver, assertSupported, assertSupportedWindowEnd, SUPPORTED_MIN, SUPPORTED_MAX } from './types.js';
|
|
24
25
|
import { dateFromUt, ttDaysFromUt, utDays } from './time.js';
|
|
25
|
-
import { KM_PER_AU, RAD2DEG } from './nutation.js';
|
|
26
|
+
import { DEG2RAD, KM_PER_AU, RAD2DEG } from './nutation.js';
|
|
26
27
|
import { topoAltAzUnrefracted } from './transforms.js';
|
|
27
28
|
import { sunGeoVectorEqj } from './sun.js';
|
|
28
29
|
import { moonGeoVectorEqj } from './moon.js';
|
|
@@ -57,7 +58,7 @@ const SUN_CYCLE_DAYS = 1;
|
|
|
57
58
|
const MOON_CYCLE_DAYS = 1.035050;
|
|
58
59
|
const MOON_HA_RATE_DEG_PER_DAY = 360 / MOON_CYCLE_DAYS;
|
|
59
60
|
const SECOND_DAYS = 1 / 86400;
|
|
60
|
-
/**
|
|
61
|
+
/** Every search stops under a second; events are reported to the ms. */
|
|
61
62
|
const TIME_TOL_DAYS = SECOND_DAYS;
|
|
62
63
|
/** An extremum this close to the target grazes it without crossing: no event. */
|
|
63
64
|
const TANGENT_EPS_DEG = 1e-6;
|
|
@@ -76,6 +77,7 @@ const EXTREMUM_HALF_WIDTH_DAYS = 2 / 24;
|
|
|
76
77
|
const FLAT_CYCLE_LATITUDE_DEG = 85;
|
|
77
78
|
const GOLDEN_SECTION_CAP = 100;
|
|
78
79
|
const BISECTION_CAP = 60;
|
|
80
|
+
const SECANT_CAP = 8;
|
|
79
81
|
const HOUR_ANGLE_ITER_CAP = 20;
|
|
80
82
|
const MOON_PHASE_ITER_CAP = 50;
|
|
81
83
|
const INV_PHI = 0.6180339887498949;
|
|
@@ -171,8 +173,7 @@ function nextHourAngle(sample, afterUt, targetHaDeg, rateDegPerDay) {
|
|
|
171
173
|
return ut;
|
|
172
174
|
}
|
|
173
175
|
/**
|
|
174
|
-
*
|
|
175
|
-
* `centreUt`. Evaluating the true extremum is what makes a grazing pair
|
|
176
|
+
* Refinement of the altitude extremum bracketed around `centreUt`. Evaluating the true extremum is what makes a grazing pair
|
|
176
177
|
* findable: the segment on either side of it is monotonic, so bisection can
|
|
177
178
|
* only miss a crossing if the extremum itself was never seen.
|
|
178
179
|
*
|
|
@@ -182,9 +183,105 @@ function nextHourAngle(sample, afterUt, targetHaDeg, rateDegPerDay) {
|
|
|
182
183
|
* declination's slow annual drift, unclamped golden section will happily walk
|
|
183
184
|
* backwards and break the chain's ordering.
|
|
184
185
|
*/
|
|
185
|
-
function refineExtremum(sample, centreUt, wantMax, halfWidthDays, afterUt) {
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
function refineExtremum(sample, centreUt, wantMax, halfWidthDays, afterUt, flatCycle) {
|
|
187
|
+
const a = Math.max(clampUt(centreUt - halfWidthDays), afterUt);
|
|
188
|
+
const b = clampUt(centreUt + halfWidthDays);
|
|
189
|
+
if (!flatCycle) {
|
|
190
|
+
const best = brentExtremum(sample, a, b, wantMax);
|
|
191
|
+
if (best !== null)
|
|
192
|
+
return best;
|
|
193
|
+
}
|
|
194
|
+
return goldenExtremum(sample, a, b, wantMax);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Brent's parabolic-plus-golden minimizer on `[a, b]`, scored for a maximum
|
|
198
|
+
* or minimum. Each step evaluates one sample; a parabola through the three
|
|
199
|
+
* best points supplies the step whenever it is finite, lands strictly inside
|
|
200
|
+
* the bracket, and contracts faster than the step before last, and golden
|
|
201
|
+
* section takes over otherwise. Returns `null` when the search hits its cap or
|
|
202
|
+
* settles within a tolerance of the original bracket edge, where the flat,
|
|
203
|
+
* asymmetric altitude curve has made the parabola unreliable; the caller then
|
|
204
|
+
* runs the golden search unchanged.
|
|
205
|
+
*/
|
|
206
|
+
function brentExtremum(sample, a0, b0, wantMax) {
|
|
207
|
+
const cost = (s) => (wantMax ? -s.altDeg : s.altDeg);
|
|
208
|
+
let a = a0;
|
|
209
|
+
let b = b0;
|
|
210
|
+
let x = a + (1 - INV_PHI) * (b - a);
|
|
211
|
+
let w = x;
|
|
212
|
+
let v = x;
|
|
213
|
+
let sx = sample(x);
|
|
214
|
+
let fx = cost(sx);
|
|
215
|
+
let fw = fx;
|
|
216
|
+
let fv = fx;
|
|
217
|
+
let d = 0; // the last step taken
|
|
218
|
+
let e = 0; // the step before last, which the parabolic step must beat
|
|
219
|
+
for (let i = 0; i < GOLDEN_SECTION_CAP; i++) {
|
|
220
|
+
if (b - a < TIME_TOL_DAYS) {
|
|
221
|
+
return x - a0 < TIME_TOL_DAYS || b0 - x < TIME_TOL_DAYS ? null : sx;
|
|
222
|
+
}
|
|
223
|
+
const tol1 = Math.max(TIME_TOL_DAYS / 16, 4 * Number.EPSILON * Math.abs(x));
|
|
224
|
+
const xm = (a + b) / 2;
|
|
225
|
+
let golden = true;
|
|
226
|
+
if (Math.abs(e) > tol1) {
|
|
227
|
+
const r = (x - w) * (fx - fv);
|
|
228
|
+
let q = (x - v) * (fx - fw);
|
|
229
|
+
let p = (x - v) * q - (x - w) * r;
|
|
230
|
+
q = 2 * (q - r);
|
|
231
|
+
if (q > 0)
|
|
232
|
+
p = -p;
|
|
233
|
+
q = Math.abs(q);
|
|
234
|
+
const eTemp = e;
|
|
235
|
+
e = d;
|
|
236
|
+
if (Number.isFinite(p / q) && Math.abs(p) < Math.abs(0.5 * q * eTemp) && p > q * (a - x) && p < q * (b - x)) {
|
|
237
|
+
d = p / q;
|
|
238
|
+
// Near an edge the parabola only re-proposes the best point; step across it instead.
|
|
239
|
+
if (x + d - a < 2 * tol1 || b - (x + d) < 2 * tol1)
|
|
240
|
+
d = xm >= x ? tol1 : -tol1;
|
|
241
|
+
golden = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (golden) {
|
|
245
|
+
e = x >= xm ? a - x : b - x;
|
|
246
|
+
d = (1 - INV_PHI) * e;
|
|
247
|
+
}
|
|
248
|
+
const u = Math.abs(d) >= tol1 ? x + d : x + (d >= 0 ? tol1 : -tol1);
|
|
249
|
+
const su = sample(u);
|
|
250
|
+
const fu = cost(su);
|
|
251
|
+
if (fu <= fx) {
|
|
252
|
+
if (u >= x)
|
|
253
|
+
a = x;
|
|
254
|
+
else
|
|
255
|
+
b = x;
|
|
256
|
+
v = w;
|
|
257
|
+
fv = fw;
|
|
258
|
+
w = x;
|
|
259
|
+
fw = fx;
|
|
260
|
+
x = u;
|
|
261
|
+
fx = fu;
|
|
262
|
+
sx = su;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (u < x)
|
|
266
|
+
a = u;
|
|
267
|
+
else
|
|
268
|
+
b = u;
|
|
269
|
+
if (fu <= fw || w === x) {
|
|
270
|
+
v = w;
|
|
271
|
+
fv = fw;
|
|
272
|
+
w = u;
|
|
273
|
+
fw = fu;
|
|
274
|
+
}
|
|
275
|
+
else if (fu <= fv || v === x || v === w) {
|
|
276
|
+
v = u;
|
|
277
|
+
fv = fu;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
/** Golden-section search of the extremum on `[a, b]`: slow, and safe on any curve shape. */
|
|
284
|
+
function goldenExtremum(sample, a, b, wantMax) {
|
|
188
285
|
let x1 = b - INV_PHI * (b - a);
|
|
189
286
|
let x2 = a + INV_PHI * (b - a);
|
|
190
287
|
let s1 = sample(x1);
|
|
@@ -215,16 +312,27 @@ function refineExtremum(sample, centreUt, wantMax, halfWidthDays, afterUt) {
|
|
|
215
312
|
return score(s1) > score(s2) ? s1 : s2;
|
|
216
313
|
}
|
|
217
314
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
315
|
+
* The single crossing of `level` on a monotonic segment between two extrema,
|
|
316
|
+
* reported from inside a verified half-second bracket.
|
|
317
|
+
*
|
|
318
|
+
* Between extrema the altitude offset is close to a half cosine, so the cosine
|
|
319
|
+
* through the two endpoints predicts the crossing to within a minute for the
|
|
320
|
+
* Sun and a few minutes for the Moon, with no probe at all. Secant steps from
|
|
321
|
+
* that estimate reach the second in one or two probes, and one more probe on
|
|
322
|
+
* the side the last sign points to proves the bracket. Anything that escapes
|
|
323
|
+
* the segment, stalls, or fails the proof falls back to the quadratically
|
|
324
|
+
* accelerated bracketed search with bisection behind it.
|
|
220
325
|
*/
|
|
221
|
-
function
|
|
326
|
+
function solveCrossing(sample, level, s0, s1) {
|
|
222
327
|
let a = s0.ut;
|
|
223
328
|
let b = s1.ut;
|
|
224
329
|
const belowAtA = offsetDeg(s0, level) < 0;
|
|
225
|
-
//
|
|
226
|
-
// ascending crossing. Endpoint geometry has already been evaluated.
|
|
330
|
+
// Normalized to an ascending crossing. Endpoint geometry has already been evaluated.
|
|
227
331
|
const f = (ut) => (belowAtA ? 1 : -1) * offsetDeg(ut === s0.ut ? s0 : ut === s1.ut ? s1 : sample(ut), level);
|
|
332
|
+
const targetDeg = level === null ? (s0.riseSetAltDeg + s1.riseSetAltDeg) / 2 : level;
|
|
333
|
+
const secant = secantCrossing(f, a, b, s0, s1, targetDeg);
|
|
334
|
+
if (secant !== null)
|
|
335
|
+
return secant;
|
|
228
336
|
const root = search(f, a, b, 0.25, BISECTION_CAP, 'altitude crossing');
|
|
229
337
|
if (root !== null) {
|
|
230
338
|
// A derivative-based estimate alone does not establish a time bound.
|
|
@@ -246,6 +354,51 @@ function bisectCrossing(sample, level, s0, s1) {
|
|
|
246
354
|
assertReached(false, 'crossing bisection');
|
|
247
355
|
return (a + b) / 2;
|
|
248
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Secant refinement of the ascending crossing of `f` on `[a, b]`, started from
|
|
359
|
+
* the half cosine that the sine of altitude follows between two extrema.
|
|
360
|
+
* Returns the root inside a proven half-second bracket, or `null` when the
|
|
361
|
+
* caller should run the bracketed search.
|
|
362
|
+
*/
|
|
363
|
+
function secantCrossing(f, a, b, s0, s1, targetDeg) {
|
|
364
|
+
// sin(alt) ≈ c + amp·cos(π (t − a) / (b − a)) passes through both extrema; a
|
|
365
|
+
// constant declination and no parallax would make it exact.
|
|
366
|
+
const g0 = Math.sin(s0.altDeg * DEG2RAD);
|
|
367
|
+
const g1 = Math.sin(s1.altDeg * DEG2RAD);
|
|
368
|
+
const c = (g0 + g1) / 2;
|
|
369
|
+
const amp = (g0 - g1) / 2;
|
|
370
|
+
const ratio = (Math.sin(targetDeg * DEG2RAD) - c) / amp;
|
|
371
|
+
if (!(Math.abs(ratio) < 1))
|
|
372
|
+
return null;
|
|
373
|
+
const theta = Math.acos(ratio);
|
|
374
|
+
let t = a + theta * (b - a) / Math.PI;
|
|
375
|
+
let slope = Math.abs(amp) * Math.sin(theta) * Math.PI / (b - a) / Math.cos(targetDeg * DEG2RAD) * RAD2DEG;
|
|
376
|
+
let ft = f(t);
|
|
377
|
+
for (let i = 0; i < SECANT_CAP; i++) {
|
|
378
|
+
const step = -ft / slope;
|
|
379
|
+
if (!Number.isFinite(step))
|
|
380
|
+
return null;
|
|
381
|
+
if (Math.abs(step) < TIME_TOL_DAYS / 4) {
|
|
382
|
+
// The last sign says which side the root is on: prove a half-second
|
|
383
|
+
// bracket there, then report the secant root itself, held inside the
|
|
384
|
+
// bracket, so the answer does not jump with the side the probe picked.
|
|
385
|
+
if (ft < 0) {
|
|
386
|
+
const right = Math.min(b, t + TIME_TOL_DAYS / 2);
|
|
387
|
+
return f(right) >= 0 ? Math.min(t + step, right) : null;
|
|
388
|
+
}
|
|
389
|
+
const left = Math.max(a, t - TIME_TOL_DAYS / 2);
|
|
390
|
+
return f(left) < 0 ? Math.max(left, t + step) : null;
|
|
391
|
+
}
|
|
392
|
+
const next = t + step;
|
|
393
|
+
if (next <= a || next >= b)
|
|
394
|
+
return null;
|
|
395
|
+
const fNext = f(next);
|
|
396
|
+
slope = (fNext - ft) / (next - t);
|
|
397
|
+
t = next;
|
|
398
|
+
ft = fNext;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
249
402
|
/**
|
|
250
403
|
* Walk the window one half-cycle at a time, refining each altitude extremum and
|
|
251
404
|
* solving every monotonic segment between consecutive extrema for every target
|
|
@@ -253,7 +406,8 @@ function bisectCrossing(sample, level, s0, s1) {
|
|
|
253
406
|
* containing the window's first instant is covered, and runs one extremum past
|
|
254
407
|
* `endUt` so an event just inside the window's end is too.
|
|
255
408
|
*/
|
|
256
|
-
function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRateDegPerDay, cycleDays,
|
|
409
|
+
function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRateDegPerDay, cycleDays, flatCycle) {
|
|
410
|
+
const extremumHalfWidthDays = flatCycle ? cycleDays / 2 : EXTREMUM_HALF_WIDTH_DAYS;
|
|
257
411
|
const found = [];
|
|
258
412
|
const emit = (ut, kind) => {
|
|
259
413
|
const q = quantizedUt(ut);
|
|
@@ -276,29 +430,16 @@ function searchAltitudeEvents(sample, levels, transitKind, startUt, endUt, haRat
|
|
|
276
430
|
// extremum we are not allowed to probe past. The segment up to it is
|
|
277
431
|
// still monotonic, so any crossing inside the window is still found.
|
|
278
432
|
const s1 = atRangeEnd ? sample(MAX_UT)
|
|
279
|
-
: refineExtremum(sample, nextEst, nextIsMax, extremumHalfWidthDays, s0.ut);
|
|
433
|
+
: refineExtremum(sample, nextEst, nextIsMax, extremumHalfWidthDays, s0.ut, flatCycle);
|
|
280
434
|
assertReached(s1.ut > s0.ut, 'extremum chain ordering');
|
|
281
435
|
// Earlier extrema still establish the chain, but their crossings cannot be emitted.
|
|
282
436
|
if (quantizedUt(s1.ut) >= startUt) {
|
|
283
|
-
let crossingSample = sample;
|
|
284
|
-
if (levels.length > 1) {
|
|
285
|
-
// Twilight levels share midpoint probes; retain them only for this segment.
|
|
286
|
-
const samples = new Map();
|
|
287
|
-
crossingSample = (ut) => {
|
|
288
|
-
const previous = samples.get(ut);
|
|
289
|
-
if (previous)
|
|
290
|
-
return previous;
|
|
291
|
-
const current = sample(ut);
|
|
292
|
-
samples.set(ut, current);
|
|
293
|
-
return current;
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
437
|
for (const spec of levels) {
|
|
297
438
|
const g0 = crossingSign(s0, spec.level);
|
|
298
439
|
const g1 = crossingSign(s1, spec.level);
|
|
299
440
|
if (g0 * g1 >= 0)
|
|
300
441
|
continue; // no crossing, or a graze that only touches
|
|
301
|
-
emit(
|
|
442
|
+
emit(solveCrossing(sample, spec.level, s0, s1), g1 > 0 ? spec.rising : spec.falling);
|
|
302
443
|
}
|
|
303
444
|
}
|
|
304
445
|
if (nextIsMax && transitKind !== null && !atRangeEnd)
|
|
@@ -321,10 +462,8 @@ const SUN_LEVELS = [
|
|
|
321
462
|
const MOON_LEVELS = [
|
|
322
463
|
{ level: null, rising: 'rise', falling: 'set' },
|
|
323
464
|
];
|
|
324
|
-
function
|
|
325
|
-
return Math.abs(observer.latitudeDeg) >= FLAT_CYCLE_LATITUDE_DEG
|
|
326
|
-
? cycleDays / 2
|
|
327
|
-
: EXTREMUM_HALF_WIDTH_DAYS;
|
|
465
|
+
function flatCycle(observer) {
|
|
466
|
+
return Math.abs(observer.latitudeDeg) >= FLAT_CYCLE_LATITUDE_DEG;
|
|
328
467
|
}
|
|
329
468
|
/**
|
|
330
469
|
* Sun rise, set, the three twilights and upper transit within the half-open
|
|
@@ -344,7 +483,7 @@ export function sunEvents(startUtc, endUtc, observer) {
|
|
|
344
483
|
assertObserver(observer);
|
|
345
484
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
346
485
|
return [];
|
|
347
|
-
return searchAltitudeEvents(sunSampler(observer), SUN_LEVELS, 'transit', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS,
|
|
486
|
+
return searchAltitudeEvents(sunSampler(observer), SUN_LEVELS, 'transit', utDays(startUtc), utDays(endUtc), SUN_HA_RATE_DEG_PER_DAY, SUN_CYCLE_DAYS, flatCycle(observer));
|
|
348
487
|
}
|
|
349
488
|
/**
|
|
350
489
|
* Moonrise and moonset within the half-open window `[startUtc, endUtc)`,
|
|
@@ -358,7 +497,7 @@ export function moonEvents(startUtc, endUtc, observer) {
|
|
|
358
497
|
assertObserver(observer);
|
|
359
498
|
if (startUtc.getTime() >= endUtc.getTime())
|
|
360
499
|
return [];
|
|
361
|
-
return searchAltitudeEvents(moonSampler(observer), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS,
|
|
500
|
+
return searchAltitudeEvents(moonSampler(observer), MOON_LEVELS, null, utDays(startUtc), utDays(endUtc), MOON_HA_RATE_DEG_PER_DAY, MOON_CYCLE_DAYS, flatCycle(observer));
|
|
362
501
|
}
|
|
363
502
|
// -------------------------------------------------------------- moon phases
|
|
364
503
|
/**
|