File size: 9,051 Bytes
9f21d0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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);
  });
});