diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index bcb1de592..63041c29a 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1709,8 +1709,8 @@ time. TRACK ACQUIRED stays first. Then: - **Sites**: the Mapped Installations layer's loaded records around the sub-satellite point (`getNearby`, asked only as far as the reach extends over the ground, and never past 3,000 km, so from a high orbit only the - sites within 3,000 km of the point below are listed) that face the satellite and lie within the off-nadir - reach, nearest to nadir first, the focused site always among them. A row + sites within 3,000 km of the point below are listed) that face the + satellite and lie within the off-nadir reach, nearest to nadir first, the focused site always among them. A row reads the site's name over its class and off-nadir angle; its button (`Keep the camera on …`) focuses the site, and on the focused site's row, marked as the subject is (`FOCUS · …`, `Release the camera from …`), ends diff --git a/src/cockpitOrbitalSatellites.js b/src/cockpitOrbitalSatellites.js index b52d4db5c..343530f42 100644 --- a/src/cockpitOrbitalSatellites.js +++ b/src/cockpitOrbitalSatellites.js @@ -22,8 +22,10 @@ function finiteVector(v) { /** * Where the camera is part-way through a hop: along the great circle * between the two positions (never through the Earth), its distance from the - * centre interpolated linearly. Opposite ends turn about a fixed axis - * square to them, so even then the path stays off the Earth. + * centre interpolated linearly. The circle is the one through both ends, so + * the path moves smoothly as the destination does, however far round it + * lies; only exactly opposite ends, which no single circle joins, turn about + * a fixed axis square to them. * @param {{x: number, y: number, z: number}} from * @param {{x: number, y: number, z: number}} to * @param {number} progress Eased, 0..1. @@ -37,39 +39,36 @@ export function hopPosition(from, to, progress) { if (!(ra > 0) || !(rb > 0)) return null; const a = { x: from.x / ra, y: from.y / ra, z: from.z / ra }; const b = { x: to.x / rb, y: to.y / rb, z: to.z / rb }; - const cosine = Math.min(1, Math.max(-1, a.x * b.x + a.y * b.y + a.z * b.z)); - const omega = Math.acos(cosine); + const cosine = a.x * b.x + a.y * b.y + a.z * b.z; + // The direction the circle leaves `a` in: `b` less its part along `a`. + let c = { + x: b.x - cosine * a.x, + y: b.y - cosine * a.y, + z: b.z - cosine * a.z, + }; + let rc = Math.hypot(c.x, c.y, c.z); const r = ra + (rb - ra) * p; - if (Math.PI - omega < 1e-6) { - // No single great circle joins opposite points: turn about the axis - // square to `a` that is least aligned with the Earth's. + const omega = Math.atan2(rc, cosine); + if (!(rc > 1e-12)) { + if (cosine > 0) return { x: a.x * r, y: a.y * r, z: a.z * r }; + // Opposite ends: set off square to `a`, northward (or, near a pole, + // toward the prime meridian). const pole = Math.abs(a.z) < 0.9 ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 }; const along = pole.x * a.x + pole.y * a.y + pole.z * a.z; - const c = { + c = { x: pole.x - along * a.x, y: pole.y - along * a.y, z: pole.z - along * a.z, }; - const rc = Math.hypot(c.x, c.y, c.z); - const cos = Math.cos(p * Math.PI); - const sin = Math.sin(p * Math.PI); - return { - x: (a.x * cos + (c.x / rc) * sin) * r, - y: (a.y * cos + (c.y / rc) * sin) * r, - z: (a.z * cos + (c.z / rc) * sin) * r, - }; - } - let ka = 1 - p; - let kb = p; - if (omega > 1e-9) { - const sine = Math.sin(omega); - ka = Math.sin((1 - p) * omega) / sine; - kb = Math.sin(p * omega) / sine; + rc = Math.hypot(c.x, c.y, c.z); } + const angle = p * omega; + const cos = Math.cos(angle); + const sin = Math.sin(angle) / rc; return { - x: (a.x * ka + b.x * kb) * r, - y: (a.y * ka + b.y * kb) * r, - z: (a.z * ka + b.z * kb) * r, + x: (a.x * cos + c.x * sin) * r, + y: (a.y * cos + c.y * sin) * r, + z: (a.z * cos + c.z * sin) * r, }; } diff --git a/src/cockpitOrbitalSatellites.test.mjs b/src/cockpitOrbitalSatellites.test.mjs index 3a4902b11..60156b89e 100644 --- a/src/cockpitOrbitalSatellites.test.mjs +++ b/src/cockpitOrbitalSatellites.test.mjs @@ -33,9 +33,41 @@ test('a hop to the opposite side goes round the Earth, not through it', () => { const radius = Cesium.Cartesian3.magnitude(from); for (const p of [0.25, 0.5, 0.75]) { const point = hopPosition(from, to, p); - assert.ok(Math.abs(Math.hypot(point.x, point.y, point.z) - radius) < 1e-3, `p=${p}`); + assert.ok( + Math.abs(Math.hypot(point.x, point.y, point.z) - radius) < 1e-3, + `p=${p}`, + ); } const end = hopPosition(from, to, 1); for (const axis of ['x', 'y', 'z']) assert.ok(Math.abs(end[axis] - to[axis]) < 1e-3, axis); }); + +test('a hop to nearly the opposite side moves smoothly as the destination does', () => { + const r = 6_778_137; + const from = { x: r, y: 0, z: 0 }; + const near = (delta) => ({ + x: -r * Math.cos(delta), + y: r * Math.sin(delta), + z: 0, + }); + const mid = (delta) => hopPosition(from, near(delta), 0.5); + const [a, b] = [mid(0.999e-6), mid(1.001e-6)]; + assert.ok( + Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z) < 1, + 'no jump between neighbours', + ); + for (const delta of [1e-3, 1e-6, 1e-9]) { + const to = near(delta); + const end = hopPosition(from, to, 1); + assert.ok( + Math.hypot(end.x - to.x, end.y - to.y, end.z - to.z) < 1e-3, + `lands at ${delta}`, + ); + const half = mid(delta); + assert.ok( + Math.abs(Math.hypot(half.x, half.y, half.z) - r) < 1e-3, + `round at ${delta}`, + ); + } +});