import { Cartesian3, HeadingPitchRoll, Quaternion, SceneMode, Transforms } from "@cesium/engine"; import { describe, expect, test } from "vitest"; import { SCENE_MODES, SKY_MODE } from "../config/viewModes"; import { cesiumSceneMode, coneDescription, coneOrientation, groundTrackDescription, isLeo, modelUri, orbitPathTimes, orbitTrackTimes, orbitUsesPathGraphic, GEOMETRY_REFRESH_MAX_SECONDS, GEOMETRY_REFRESH_MIN_SECONDS, geometryRefreshSeconds, } from "./satelliteGraphics"; const ISS_PERIOD_MIN = 92.6; describe("isLeo", () => { test("gates on the orbit class itself, so it cannot disagree with the label", () => { expect(isLeo("LEO")).toBe(true); expect(isLeo("MEO")).toBe(false); expect(isLeo("GEO")).toBe(false); // A highly elliptical orbit can have a short period; a swath corridor under // one would be meaningless, and the class already rules it out. expect(isLeo("HEO")).toBe(false); }); }); describe("orbit path times", () => { test("path leads and trails half a period plus overlap", () => { const { leadTime, trailTime } = orbitPathTimes(ISS_PERIOD_MIN); expect(leadTime).toBeCloseTo((ISS_PERIOD_MIN * 60) / 2 + 5); expect(trailTime).toBe(leadTime); }); test("track leads one full period with no trail", () => { expect(orbitTrackTimes(ISS_PERIOD_MIN)).toEqual({ leadTime: ISS_PERIOD_MIN * 60, trailTime: 0 }); }); }); describe("groundTrackDescription", () => { test("converts the swath width to meters", () => { expect(groundTrackDescription("LEO", 290)).toEqual({ widthMeters: 290000 }); }); test("is unavailable for non-LEO satellites", () => { expect(groundTrackDescription("GEO", 290)).toBeUndefined(); }); }); describe("coneDescription", () => { test("converts the FOV to a half angle in radians", () => { const description = coneDescription("LEO", 45); expect(description).toBeDefined(); expect(description!.radiusMeters).toBe(1000000); expect(description!.innerHalfAngleRad).toBe(0); expect(description!.outerHalfAngleRad).toBeCloseTo(Math.PI / 4); }); test("is unavailable for non-LEO satellites", () => { expect(coneDescription("GEO", 45)).toBeUndefined(); }); }); describe("coneOrientation", () => { // Somewhere over Europe at ISS altitude, so the quaternion comes from a position // a satellite could hold. const overhead = Cartesian3.fromDegrees(11.5, 48.1, 420000); test("orients the cone from a known position", () => { const orientation = coneOrientation(overhead); expect(orientation).toBeDefined(); // A rotation, not a degenerate quaternion — the visualizer would draw a cone // pointing anywhere if this came back unnormalized. expect(Quaternion.magnitude(orientation!)).toBeCloseTo(1); }); // See `coneOrientation`: the throw runs inside `DataSourceDisplay.update`, so it // stops Cesium's render loop for the session rather than skipping one cone. test("declines rather than throwing when there is no position", () => { expect(() => coneOrientation(undefined)).not.toThrow(); expect(coneOrientation(undefined)).toBeUndefined(); }); // Checked against Cesium, so the guard above rests on a measured throw rather // than a remembered one. If an upgrade ever makes this tolerate a missing origin, // this fails and the guard can be reconsidered. test("Cesium really does throw on the position the guard withholds", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(() => Transforms.headingPitchRollQuaternion(undefined as any, new HeadingPitchRoll(0, Math.PI, 0))).toThrow(/origin is required/); }); }); describe("modelUri", () => { test("prefers the explicit metadata model URL", () => { expect(modelUri("FOREST-2", "./data/models/custom.glb")).toBe("./data/models/custom.glb"); }); test("falls back to the name-convention path with spaces dashed", () => { expect(modelUri("ISS (ZARYA)")).toBe("./data/models/ISS-(ZARYA).glb"); }); }); describe("orbitUsesPathGraphic", () => { test("tracked satellites always use the path graphic", () => { expect(orbitUsesPathGraphic(true, true)).toBe(true); expect(orbitUsesPathGraphic(true, false)).toBe(true); }); test("untracked satellites use the primitive only when the scene supports it", () => { expect(orbitUsesPathGraphic(false, true)).toBe(false); expect(orbitUsesPathGraphic(false, false)).toBe(true); }); }); describe("geometryRefreshSeconds", () => { test("a handful of tracks refresh at the floor, where the lag would be visible", () => { expect(geometryRefreshSeconds(0)).toBe(GEOMETRY_REFRESH_MIN_SECONDS); expect(geometryRefreshSeconds(8)).toBe(GEOMETRY_REFRESH_MIN_SECONDS); expect(geometryRefreshSeconds(100)).toBe(GEOMETRY_REFRESH_MIN_SECONDS); }); test("scales with the count between the bounds", () => { expect(geometryRefreshSeconds(500)).toBe(5); }); test("clamps at the ceiling, where a rebuild costs more than the lag", () => { expect(geometryRefreshSeconds(1000)).toBe(GEOMETRY_REFRESH_MAX_SECONDS); expect(geometryRefreshSeconds(5000)).toBe(GEOMETRY_REFRESH_MAX_SECONDS); }); }); describe("cesiumSceneMode", () => { test("maps the three projections and refuses the one that is not", () => { expect(cesiumSceneMode("3D")).toBe(SceneMode.SCENE3D); expect(cesiumSceneMode("2D")).toBe(SceneMode.SCENE2D); expect(cesiumSceneMode("Columbus")).toBe(SceneMode.COLUMBUS_VIEW); // Sky renders in 3D but is a camera placement, so morphing on its behalf is // SkyView's business and this must not answer for it. expect(cesiumSceneMode("Sky")).toBeUndefined(); expect(cesiumSceneMode("nonsense")).toBeUndefined(); }); test("every projection name in the app's vocabulary maps", () => { // A mode added to SCENE_MODES without a projection here would silently stop // morphing, so the list is checked rather than restated. const unmapped = SCENE_MODES.filter((mode) => mode !== SKY_MODE && cesiumSceneMode(mode) === undefined); expect(unmapped).toEqual([]); }); });