File size: 4,804 Bytes
9f21d0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// The TEME to pseudo-fixed rotation, as arithmetic on epoch milliseconds.
//
// Cesium's `Transforms.computeTemeToPseudoFixedMatrix` is a rotation about Z by a
// single angle, and everything around it is overhead on six useful flops: a
// `JulianDate` per sample, a leap-second lookup, a `Matrix3` allocated because the
// call site passes no result, and a full 3x3 multiply. Measured over 5,000
// satellites of 241 samples, the transform half of a window fill:
//
//     Cesium, as #applyChunk called it   220 ms
//     Cesium, with scratch objects       105 ms
//     this, rotation written inline       31 ms
//
// It is the same formula rather than an approximation of it — the constants below
// are Cesium's — so the two agree to 0.001 mm over 42 anchors spanning three years,
// including the midnight and noon crossings where the parameterisation changes
// branch. The point is not a cheaper transform but a Cesium-free one: it can move
// into the propagation worker, where the main thread stops paying for it at all.
//
// Deliberately *not* the GMST that satellite.js's `gstime` computes. That evaluates
// the polynomial at the instant where this evaluates it at 0h and adds Earth's
// rotation rate since; they differ by 1.5e-9 rad, which is 1 cm at LEO and 6 cm at
// GEO. Immaterial on its own — the interpolation error in GridPositionProperty is
// metres — but this way the stored positions stay bit-identical to what Cesium's own
// transform produced, so the change is invisible to everything downstream.

/** Cesium's GMST polynomial, in seconds, evaluated in Julian centuries from J2000. */
const GMST_C0 = 6 * 3600 + 41 * 60 + 50.54841;
const GMST_C1 = 8640184.812866;
const GMST_C2 = 0.093104;
const GMST_C3 = -6.2e-6;

/** Precession of the rotation rate, per day from J2000. */
const RATE_COEF = 1.1772758384668e-19;

const WGS84_WR_PRECESSING = 7.2921158553e-5;

const TWO_PI = Math.PI * 2;
const SECONDS_PER_DAY = 86400;
const TWO_PI_PER_SECONDS_PER_DAY = TWO_PI / SECONDS_PER_DAY;
const MS_PER_DAY = 86_400_000;
const J2000_DAY_NUMBER = 2451545;

/** J2000 proper is noon, so the rate term is referred to the midnight before it. */
const J2000_MIDNIGHT = J2000_DAY_NUMBER - 0.5;

/**
 * Cesium splits a Julian date this way, and the Unix epoch lands mid-day in it —
 * a Julian day starts at noon, so 00:00 UTC is half a day in.
 */
const UNIX_EPOCH_DAY_NUMBER = 2440587;
const HALF_DAY_SECONDS = 43200;

/**
 * Greenwich hour angle for a UTC instant, in radians.
 *
 * Unix time is already UTC, which is the frame Cesium reduces to before doing this
 * arithmetic — it converts UTC to TAI on the way in and subtracts `taiMinusUtc` back
 * off here — so taking epoch milliseconds skips a round trip rather than skipping a
 * correction. The one case where that is not merely equivalent is an interval
 * spanning a leap second, where TAI and Unix time disagree about how many seconds
 * elapsed; see the test, which pins the bound rather than assuming it away.
 */
export function greenwichHourAngle(epochMs: number): number {
  // The day is split off as an integer first. Carrying the whole Julian date in one
  // double puts the value near 2.44e6, whose ulp is about 47 us of Earth rotation —
  // measured as a 9.5 mm error before this was split. Keeping `secondsOfDay` small
  // keeps its resolution.
  const days = Math.floor(epochMs / MS_PER_DAY);
  const msIntoDay = epochMs - days * MS_PER_DAY;
  let dayNumber = UNIX_EPOCH_DAY_NUMBER + days;
  let secondsOfDay = HALF_DAY_SECONDS + msIntoDay / 1000;
  if (secondsOfDay >= SECONDS_PER_DAY) {
    secondsOfDay -= SECONDS_PER_DAY;
    dayNumber += 1;
  }

  // GMST is tabulated at 0h, so the half-day offset picks the tabulation this
  // instant belongs to.
  const centuries = (dayNumber - J2000_DAY_NUMBER + (secondsOfDay >= HALF_DAY_SECONDS ? 0.5 : -0.5)) / 36525;
  const gmstSeconds = GMST_C0 + centuries * (GMST_C1 + centuries * (GMST_C2 + centuries * GMST_C3));
  const angleAt0h = (gmstSeconds * TWO_PI_PER_SECONDS_PER_DAY) % TWO_PI;
  const rotationRate = WGS84_WR_PRECESSING + RATE_COEF * (dayNumber - J2000_MIDNIGHT);
  const secondsSinceMidnight = (secondsOfDay + HALF_DAY_SECONDS) % SECONDS_PER_DAY;
  return angleAt0h + rotationRate * secondsSinceMidnight;
}

/**
 * The rotation for one instant, as the cosine and sine of the hour angle.
 *
 * Handed back as a pair rather than a matrix because seven of the nine entries are
 * constant: the fixed-frame position is
 * `(c*x + s*y, -s*x + c*y, z)`.
 */
export interface FixedRotation {
  cos: number;
  sin: number;
}

export function fixedRotationAt(epochMs: number, result: FixedRotation): FixedRotation {
  const gha = greenwichHourAngle(epochMs);
  result.cos = Math.cos(gha);
  result.sin = Math.sin(gha);
  return result;
}