orbit-studio / src /modules /util /GridPositionProperty.test.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
9.05 kB
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);
});
});