orbit-studio / src /modules /skyGeometry.test.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
5.64 kB
import { Cartesian3 } from "@cesium/engine";
import { describe, expect, test } from "vitest";
import { enuDirection, levelBasis, normalizeAzimuth, offsetObserver, rollBasis, rollOf } from "./skyGeometry";
const angles = [
[0, 0],
[37, 15],
[180, -30],
[270, 60],
[45, 89],
[300, 90],
] as const;
describe("enuDirection", () => {
test("names the cardinal directions", () => {
expect(enuDirection(0, 0)).toMatchObject({ x: expect.closeTo(0, 12), y: expect.closeTo(1, 12), z: expect.closeTo(0, 12) });
expect(enuDirection(90, 0)).toMatchObject({ x: expect.closeTo(1, 12), y: expect.closeTo(0, 12), z: expect.closeTo(0, 12) });
expect(enuDirection(0, 90)).toMatchObject({ x: expect.closeTo(0, 12), y: expect.closeTo(0, 12), z: expect.closeTo(1, 12) });
});
test("scales by the distance without turning", () => {
const unit = enuDirection(123, 45);
const far = enuDirection(123, 45, 1000);
expect(Cartesian3.magnitude(far)).toBeCloseTo(1000, 9);
expect(Cartesian3.angleBetween(unit, far)).toBeCloseTo(0, 9);
});
});
describe("levelBasis", () => {
test("keeps the horizon level at every elevation", () => {
for (const [azimuth, elevation] of angles) {
expect(levelBasis(azimuth, elevation).right.z, `az=${azimuth} el=${elevation}`).toBeCloseTo(0, 12);
}
});
test("is orthonormal against the direction it belongs to", () => {
for (const [azimuth, elevation] of angles) {
const direction = enuDirection(azimuth, elevation);
const { up, right } = levelBasis(azimuth, elevation);
const label = `az=${azimuth} el=${elevation}`;
expect(Cartesian3.magnitude(up), label).toBeCloseTo(1, 12);
expect(Cartesian3.magnitude(right), label).toBeCloseTo(1, 12);
expect(Cartesian3.dot(direction, up), label).toBeCloseTo(0, 12);
expect(Cartesian3.dot(direction, right), label).toBeCloseTo(0, 12);
expect(Cartesian3.dot(up, right), label).toBeCloseTo(0, 12);
}
});
});
describe("rollBasis and rollOf", () => {
// The regression this file exists for. Composing roll and decomposing it were
// written in two modules from the same formula, and disagreed in sign: feeding
// a device's measured roll back into the camera basis mirrored the view. They
// are each other's inverse now, and this is what holds them there.
test("are exact inverses", () => {
for (const [azimuth, elevation] of angles) {
for (const roll of [-170, -90, -30, 0, 30, 90, 170]) {
const { up } = rollBasis(azimuth, elevation, roll);
expect(rollOf(azimuth, elevation, up), `az=${azimuth} el=${elevation} roll=${roll}`).toBeCloseTo(roll, 9);
}
}
});
test("reports no roll for the level basis", () => {
for (const [azimuth, elevation] of angles) {
expect(rollOf(azimuth, elevation, levelBasis(azimuth, elevation).up), `az=${azimuth} el=${elevation}`).toBeCloseTo(0, 12);
}
});
test("turns the basis about the view axis and nothing else", () => {
const direction = enuDirection(120, 40);
const rolled = rollBasis(120, 40, 30);
expect(Cartesian3.dot(direction, rolled.up)).toBeCloseTo(0, 12);
expect(Cartesian3.dot(direction, rolled.right)).toBeCloseTo(0, 12);
expect(Cartesian3.dot(rolled.up, rolled.right)).toBeCloseTo(0, 12);
});
});
describe("normalizeAzimuth", () => {
test("wraps onto [0, 360)", () => {
expect(normalizeAzimuth(0)).toBe(0);
expect(normalizeAzimuth(360)).toBe(0);
expect(normalizeAzimuth(-90)).toBe(270);
expect(normalizeAzimuth(450)).toBe(90);
expect(normalizeAzimuth(-720.5)).toBeCloseTo(359.5, 9);
});
});
describe("offsetObserver", () => {
const MUNICH = { lat: 48.1372, lon: 11.5756 };
const distance = (from: { lat: number; lon: number }, to: { lat: number; lon: number }): number =>
Cartesian3.distance(Cartesian3.fromDegrees(from.lon, from.lat), Cartesian3.fromDegrees(to.lon, to.lat));
test("moves the distance asked for, in the direction asked for", () => {
expect(offsetObserver(MUNICH, 0, 100).lat).toBeGreaterThan(MUNICH.lat);
expect(offsetObserver(MUNICH, 100, 0).lon).toBeGreaterThan(MUNICH.lon);
expect(distance(MUNICH, offsetObserver(MUNICH, 0, 1000))).toBeCloseTo(1000, 1);
expect(distance(MUNICH, offsetObserver(MUNICH, 750, -750))).toBeCloseTo(Math.hypot(750, 750), 1);
});
test("standing still stays put", () => {
const still = offsetObserver(MUNICH, 0, 0);
expect(still.lat).toBeCloseTo(MUNICH.lat, 9);
expect(still.lon).toBeCloseTo(MUNICH.lon, 9);
});
test("keeps its scale near the poles, where degrees of longitude do not", () => {
// 100 m east at 89.9°N is half a degree of longitude — the parallel there is
// 11 km around. A fixed 111,320 m per degree would move 0.0009° instead, and
// the observer would barely leave the spot.
const polar = { lat: 89.9, lon: 0 };
expect(distance(polar, offsetObserver(polar, 100, 0))).toBeCloseTo(100, 1);
expect(offsetObserver(polar, 100, 0).lon).toBeCloseTo(0.513, 2);
});
test("crosses the antimeridian without wrapping arithmetic", () => {
const east = { lat: 0, lon: 179.9999 };
const crossed = offsetObserver(east, 1000, 0);
expect(crossed.lon).toBeLessThan(-179.9);
expect(distance(east, crossed)).toBeCloseTo(1000, 1);
});
test("walks over the pole rather than off the top of the coordinates", () => {
const near = { lat: 89.999, lon: 0 };
const over = offsetObserver(near, 0, 1000);
expect(over.lat).toBeLessThanOrEqual(90);
expect(Math.abs(over.lon)).toBeCloseTo(180, 3);
expect(distance(near, over)).toBeCloseTo(1000, 1);
});
});