Spaces:
Configuration error
Configuration error
| import { Cartesian3, Entity, JulianDate, ReferenceFrame, VelocityOrientationProperty } from "@cesium/engine"; | |
| import { describe, expect, test } from "vitest"; | |
| import { GridPositionProperty } from "./GridPositionProperty"; | |
| const ANCHOR_MS = Date.UTC(2018, 11, 8); | |
| const STEP = 45; | |
| const anchor = () => JulianDate.fromDate(new Date(ANCHOR_MS)); | |
| const at = (seconds: number) => JulianDate.addSeconds(anchor(), seconds, new JulianDate()); | |
| /** A straight line through the grid, so an exact answer is known at every instant. */ | |
| const ramp = (fromIndex: number, count: number): Float64Array => { | |
| const xyz = new Float64Array(count * 3); | |
| for (let index = 0; index < count; index += 1) { | |
| const gridIndex = fromIndex + index; | |
| xyz[index * 3] = gridIndex * 100; | |
| xyz[index * 3 + 1] = gridIndex * -200; | |
| xyz[index * 3 + 2] = 7000; | |
| } | |
| return xyz; | |
| }; | |
| const filled = (fromIndex = 0, count = 10): GridPositionProperty => { | |
| const property = new GridPositionProperty(); | |
| property.reset(ANCHOR_MS, STEP); | |
| property.add(fromIndex, ramp(fromIndex, count)); | |
| return property; | |
| }; | |
| describe("GridPositionProperty", () => { | |
| test("is constant, and reads as nothing, until it holds samples", () => { | |
| const property = new GridPositionProperty(); | |
| expect(property.isConstant).toBe(true); | |
| expect(property.getValue(at(0))).toBeUndefined(); | |
| }); | |
| test("reproduces the samples it was given, exactly", () => { | |
| const property = filled(); | |
| for (let index = 0; index < 10; index += 1) { | |
| const value = property.getValue(at(index * STEP)) as Cartesian3; | |
| expect(value.x).toBeCloseTo(index * 100, 6); | |
| expect(value.y).toBeCloseTo(index * -200, 6); | |
| expect(value.z).toBeCloseTo(7000, 6); | |
| } | |
| }); | |
| test("interpolates a straight line without bending it", () => { | |
| // A cubic through collinear points is that line, so any error here is the | |
| // basis functions rather than the accuracy of the fit. | |
| const property = filled(); | |
| const value = property.getValue(at(3.25 * STEP)) as Cartesian3; | |
| expect(value.x).toBeCloseTo(325, 6); | |
| expect(value.y).toBeCloseTo(-650, 6); | |
| }); | |
| test("holds the end value rather than extrapolating past it", () => { | |
| // A clock scrubbed clear of the window reads here for a frame or two before the | |
| // refill lands. A free-running cubic would answer with a position half an orbit | |
| // of divergence away; holding answers with a stale one, which is bounded. | |
| const property = filled(0, 6); | |
| expect((property.getValue(at(5 * STEP)) as Cartesian3).x).toBeCloseTo(500, 6); | |
| expect((property.getValue(at(20 * STEP)) as Cartesian3).x).toBeCloseTo(500, 6); | |
| expect((property.getValue(at(-40 * STEP)) as Cartesian3).x).toBeCloseTo(0, 6); | |
| }); | |
| test("holds the nearest sample when there are too few for a cubic", () => { | |
| const property = new GridPositionProperty(); | |
| property.reset(ANCHOR_MS, STEP); | |
| property.add(4, ramp(4, 2)); | |
| expect((property.getValue(at(4 * STEP)) as Cartesian3).x).toBeCloseTo(400, 6); | |
| expect((property.getValue(at(5 * STEP)) as Cartesian3).x).toBeCloseTo(500, 6); | |
| }); | |
| test("reads the same instant from either end of a seam", () => { | |
| // Two batches, appended in either order, describing one grid. What makes this | |
| // safe is that an index means an instant, so neither batch has to know the | |
| // other's times. | |
| const forwards = filled(0, 6); | |
| forwards.add(6, ramp(6, 6)); | |
| const backwards = filled(6, 6); | |
| backwards.add(0, ramp(0, 6)); | |
| expect(forwards.length).toBe(12); | |
| expect(backwards.length).toBe(12); | |
| for (const seconds of [5.5 * STEP, 6 * STEP, 6.5 * STEP]) { | |
| expect((forwards.getValue(at(seconds)) as Cartesian3).x).toBeCloseTo((backwards.getValue(at(seconds)) as Cartesian3).x, 6); | |
| } | |
| }); | |
| test("overwrites an interval it already holds instead of duplicating it", () => { | |
| const property = filled(0, 10); | |
| const rewritten = new Float64Array(9).fill(42); | |
| expect(property.add(3, rewritten)).toBe(true); | |
| expect(property.length).toBe(10); | |
| expect((property.getValue(at(4 * STEP)) as Cartesian3).x).toBeCloseTo(42, 6); | |
| }); | |
| test("a batch that extends the front and overlaps is taken whole", () => { | |
| const property = filled(6, 4); | |
| expect(property.add(2, ramp(2, 8))).toBe(true); | |
| expect(property.firstIndex).toBe(2); | |
| expect(property.length).toBe(8); | |
| expect((property.getValue(at(3 * STEP)) as Cartesian3).x).toBeCloseTo(300, 6); | |
| expect((property.getValue(at(9 * STEP)) as Cartesian3).x).toBeCloseTo(900, 6); | |
| }); | |
| test("a batch that swallows the window whole keeps all of it", () => { | |
| const property = filled(6, 3); | |
| expect(property.add(4, ramp(4, 10))).toBe(true); | |
| expect(property.firstIndex).toBe(4); | |
| expect(property.length).toBe(10); | |
| expect((property.getValue(at(13 * STEP)) as Cartesian3).x).toBeCloseTo(1300, 6); | |
| }); | |
| test("refuses a batch that would leave a hole", () => { | |
| const property = filled(0, 5); | |
| // Grid indices 5..7 are missing, so this cannot be spliced on without the | |
| // samples between. The caller treats a refusal as a failed fill. | |
| expect(property.add(8, ramp(8, 3))).toBe(false); | |
| expect(property.length).toBe(5); | |
| }); | |
| test("drops from either end without moving the samples that remain", () => { | |
| const property = filled(0, 12); | |
| property.dropBefore(4); | |
| expect(property.firstIndex).toBe(4); | |
| expect(property.length).toBe(8); | |
| property.dropAfter(9); | |
| expect(property.length).toBe(6); | |
| expect((property.getValue(at(7 * STEP)) as Cartesian3).x).toBeCloseTo(700, 6); | |
| }); | |
| test("keeps reading correctly after a window slides and refills", () => { | |
| // The whole lifecycle in one: evict the tail of the window, append the new | |
| // head, and check an instant in the part that was never touched. | |
| const property = filled(0, 12); | |
| property.dropBefore(6); | |
| expect(property.add(12, ramp(12, 6))).toBe(true); | |
| expect(property.firstIndex).toBe(6); | |
| expect(property.length).toBe(12); | |
| expect((property.getValue(at(13.5 * STEP)) as Cartesian3).x).toBeCloseTo(1350, 6); | |
| expect((property.getValue(at(8 * STEP)) as Cartesian3).x).toBeCloseTo(800, 6); | |
| }); | |
| test("a different anchor or step starts the grid over", () => { | |
| const property = filled(); | |
| expect(property.isOnGrid(ANCHOR_MS, STEP)).toBe(true); | |
| expect(property.isOnGrid(ANCHOR_MS + 1, STEP)).toBe(false); | |
| expect(property.isOnGrid(ANCHOR_MS, STEP + 1)).toBe(false); | |
| property.reset(ANCHOR_MS, 60); | |
| expect(property.length).toBe(0); | |
| }); | |
| test("turns a grid index back into the instant it stands for", () => { | |
| const property = filled(); | |
| expect(JulianDate.secondsDifference(property.timeAt(4), anchor())).toBeCloseTo(4 * STEP, 6); | |
| expect(property.indexAtOrAfter(at(4 * STEP))).toBe(4); | |
| expect(property.indexAtOrAfter(at(4.1 * STEP))).toBe(5); | |
| }); | |
| test("converts out of its own reference frame on request", () => { | |
| const property = filled(); | |
| const own = property.getValueInReferenceFrame(at(2 * STEP), ReferenceFrame.FIXED) as Cartesian3; | |
| const other = property.getValueInReferenceFrame(at(2 * STEP), ReferenceFrame.INERTIAL); | |
| expect(own.x).toBeCloseTo(200, 6); | |
| // Cesium's transform data is not loaded in a unit test, so the conversion may | |
| // legitimately decline — what matters is that it is attempted, not assumed. | |
| if (other) { | |
| expect(other.x).not.toBeCloseTo(200, 6); | |
| } | |
| }); | |
| test("drives an entity the way Cesium's own position properties do", () => { | |
| // The interface is not enough to go on: VelocityVectorProperty subscribes via | |
| // `value._definitionChanged` rather than the getter, so a property satisfying | |
| // only the documented shape throws here. Constructing what the app constructs | |
| // is the only check that catches it — every assertion above passed while a | |
| // scene of 5,000 satellites built nothing at all. | |
| const property = filled(); | |
| const entity = new Entity({ name: "grid", position: property as never }); | |
| entity.orientation = new VelocityOrientationProperty(property as never); | |
| expect(entity.position).toBe(property); | |
| expect((entity.position!.getValue(at(2 * STEP)) as Cartesian3).x).toBeCloseTo(200, 6); | |
| // A ramp is straight, so the orientation is well defined and constant along it. | |
| expect(entity.orientation.getValue(at(2 * STEP))).toBeDefined(); | |
| }); | |
| test("raises definitionChanged when what it holds changes", () => { | |
| const property = filled(); | |
| let raised = 0; | |
| property.definitionChanged.addEventListener(() => { | |
| raised += 1; | |
| }); | |
| property.add(10, ramp(10, 4)); | |
| expect(raised).toBe(1); | |
| }); | |
| test("hands back the raw samples for a range", () => { | |
| const property = filled(0, 10); | |
| const positions = property.rawPositions(2, 4); | |
| expect(positions).toHaveLength(3); | |
| expect(positions[0]?.x).toBeCloseTo(200, 6); | |
| // Clamped to what is held rather than padded with zeroes. | |
| expect(property.rawPositions(-5, 100)).toHaveLength(10); | |
| }); | |
| }); | |