Spaces:
Configuration error
Configuration error
| 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); | |
| }); | |
| }); | |