@aeroswarm/client 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -18
- package/dist/index-ByNBpjNk.d.ts +75 -0
- package/dist/index-CyxVoJXL.d.ts +733 -0
- package/dist/index.d.ts +18 -688
- package/dist/index.js +436 -16
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +24 -3
- package/dist/react/index.js +439 -35
- package/dist/react/index.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -2
- package/dist/sdk/index.js +96 -12
- package/dist/sdk/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -140,12 +140,13 @@ var AeroWsClient = class {
|
|
|
140
140
|
this._requestSnapshot();
|
|
141
141
|
}
|
|
142
142
|
this.lastSeq = msg.seq;
|
|
143
|
+
const meta = { serverTime: msg.serverTime, seq: msg.seq };
|
|
143
144
|
switch (msg.type) {
|
|
144
145
|
case "snapshot":
|
|
145
|
-
this.callbacks.onSnapshot?.(msg.payload.tracks);
|
|
146
|
+
this.callbacks.onSnapshot?.(msg.payload.tracks, meta);
|
|
146
147
|
break;
|
|
147
148
|
case "track.updated":
|
|
148
|
-
this.callbacks.onTrackUpdate?.(msg.payload.track);
|
|
149
|
+
this.callbacks.onTrackUpdate?.(msg.payload.track, meta);
|
|
149
150
|
break;
|
|
150
151
|
case "track.removed":
|
|
151
152
|
this.callbacks.onTrackRemoved?.(msg.payload);
|
|
@@ -313,6 +314,85 @@ function createAeroTrpcClient(endpoint, token) {
|
|
|
313
314
|
});
|
|
314
315
|
}
|
|
315
316
|
|
|
317
|
+
// src/sdk/api/response.ts
|
|
318
|
+
var ENVELOPE_HEADER = "x-aero-envelope";
|
|
319
|
+
var STATUS_CODES = [
|
|
320
|
+
[401, "UNAUTHORIZED"],
|
|
321
|
+
[403, "FORBIDDEN"],
|
|
322
|
+
[404, "NOT_FOUND"],
|
|
323
|
+
[400, "BAD_REQUEST"],
|
|
324
|
+
[409, "CONFLICT"],
|
|
325
|
+
[429, "RATE_LIMITED"],
|
|
326
|
+
[503, "UNAVAILABLE"],
|
|
327
|
+
[500, "INTERNAL"]
|
|
328
|
+
];
|
|
329
|
+
function codeForStatus(status) {
|
|
330
|
+
const hit = STATUS_CODES.find(([s]) => s === status);
|
|
331
|
+
if (hit) return hit[1];
|
|
332
|
+
return status >= 500 ? "INTERNAL" : "BAD_REQUEST";
|
|
333
|
+
}
|
|
334
|
+
var AeroApiError = class extends Error {
|
|
335
|
+
status;
|
|
336
|
+
code;
|
|
337
|
+
/** Retrying later may succeed (rate limit, server busy or failing). */
|
|
338
|
+
recoverable;
|
|
339
|
+
details;
|
|
340
|
+
constructor(init) {
|
|
341
|
+
super(init.message);
|
|
342
|
+
this.name = "AeroApiError";
|
|
343
|
+
this.status = init.status;
|
|
344
|
+
this.code = init.code;
|
|
345
|
+
this.recoverable = init.recoverable;
|
|
346
|
+
this.details = init.details;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
350
|
+
function isEnvelope(body) {
|
|
351
|
+
return isObject(body) && body.ok === true && "data" in body;
|
|
352
|
+
}
|
|
353
|
+
function isFailure(body) {
|
|
354
|
+
return isObject(body) && body.ok === false;
|
|
355
|
+
}
|
|
356
|
+
function errorFrom(res, body, text, label) {
|
|
357
|
+
const prefix = label ? `${label} \u2192 ${res.status}` : `HTTP ${res.status}`;
|
|
358
|
+
const failure = isFailure(body) ? body : void 0;
|
|
359
|
+
const e = failure?.error;
|
|
360
|
+
if (isObject(e)) {
|
|
361
|
+
const code = typeof e.code === "string" ? e.code : codeForStatus(res.status);
|
|
362
|
+
const message2 = typeof e.message === "string" ? e.message : res.statusText || prefix;
|
|
363
|
+
return new AeroApiError({
|
|
364
|
+
status: res.status,
|
|
365
|
+
code,
|
|
366
|
+
message: `${prefix}: ${message2}`,
|
|
367
|
+
recoverable: typeof e.recoverable === "boolean" ? e.recoverable : res.status >= 500,
|
|
368
|
+
details: e.details
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const message = typeof e === "string" ? e : text.slice(0, 200) || res.statusText;
|
|
372
|
+
return new AeroApiError({
|
|
373
|
+
status: res.status,
|
|
374
|
+
code: codeForStatus(res.status),
|
|
375
|
+
message: message ? `${prefix}: ${message}` : prefix,
|
|
376
|
+
recoverable: res.status >= 500 || res.status === 429,
|
|
377
|
+
details: failure?.detail
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
async function readApiBody(res, label) {
|
|
381
|
+
const text = await res.text();
|
|
382
|
+
let body;
|
|
383
|
+
try {
|
|
384
|
+
body = text ? JSON.parse(text) : void 0;
|
|
385
|
+
} catch {
|
|
386
|
+
body = void 0;
|
|
387
|
+
}
|
|
388
|
+
if (!res.ok || isFailure(body)) throw errorFrom(res, body, text, label);
|
|
389
|
+
return body;
|
|
390
|
+
}
|
|
391
|
+
async function parseApiResponse(res, label) {
|
|
392
|
+
const body = await readApiBody(res, label);
|
|
393
|
+
return isEnvelope(body) ? body.data : body;
|
|
394
|
+
}
|
|
395
|
+
|
|
316
396
|
// src/sdk/api/rest.ts
|
|
317
397
|
var AeroRestClient = class {
|
|
318
398
|
constructor(endpoint, token) {
|
|
@@ -330,8 +410,7 @@ var AeroRestClient = class {
|
|
|
330
410
|
},
|
|
331
411
|
body: body ? JSON.stringify(body) : void 0
|
|
332
412
|
});
|
|
333
|
-
|
|
334
|
-
return res.json();
|
|
413
|
+
return await readApiBody(res, `${method} ${path}`);
|
|
335
414
|
}
|
|
336
415
|
createSource(body) {
|
|
337
416
|
return this.req("POST", "/v1/sources", body);
|
|
@@ -553,9 +632,10 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
553
632
|
async federatedFetch(base, path, query) {
|
|
554
633
|
const url = new URL(`${base}${path}`);
|
|
555
634
|
for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0) url.searchParams.set(k, v);
|
|
556
|
-
const res = await fetch(url, {
|
|
557
|
-
|
|
558
|
-
|
|
635
|
+
const res = await fetch(url, {
|
|
636
|
+
headers: { Authorization: `Bearer ${this.opts.token}`, [ENVELOPE_HEADER]: "1" }
|
|
637
|
+
});
|
|
638
|
+
return parseApiResponse(res, `Federated request ${url.pathname}`);
|
|
559
639
|
}
|
|
560
640
|
tracks = {
|
|
561
641
|
subscribe: (opts, callbacks) => {
|
|
@@ -567,15 +647,15 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
567
647
|
this._status = s;
|
|
568
648
|
callbacks.onStatusChange?.(s);
|
|
569
649
|
},
|
|
570
|
-
onSnapshot: (tracks) => {
|
|
650
|
+
onSnapshot: (tracks, meta) => {
|
|
571
651
|
const labeled = this.cache.applySnapshot(tracks);
|
|
572
652
|
this._lastUpdateAt = Date.now();
|
|
573
|
-
callbacks.onSnapshot?.(labeled);
|
|
653
|
+
callbacks.onSnapshot?.(labeled, meta);
|
|
574
654
|
},
|
|
575
|
-
onTrackUpdate: (track) => {
|
|
655
|
+
onTrackUpdate: (track, meta) => {
|
|
576
656
|
const labeled = this.cache.applyUpdate(track);
|
|
577
657
|
this._lastUpdateAt = Date.now();
|
|
578
|
-
callbacks.onTrackUpdate?.(labeled);
|
|
658
|
+
callbacks.onTrackUpdate?.(labeled, meta);
|
|
579
659
|
},
|
|
580
660
|
onTrackRemoved: (ev) => {
|
|
581
661
|
this.cache.applyRemoved(ev);
|
|
@@ -600,7 +680,7 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
600
680
|
reconnect: () => this.wsClient.reconnect(),
|
|
601
681
|
stream: (opts) => {
|
|
602
682
|
const queue = [];
|
|
603
|
-
let
|
|
683
|
+
let resolve2 = null;
|
|
604
684
|
let done = false;
|
|
605
685
|
this.wsClient.connect({ encoding: "json", ...opts }, {
|
|
606
686
|
onSnapshot: (tracks) => {
|
|
@@ -608,9 +688,9 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
608
688
|
},
|
|
609
689
|
onTrackUpdate: (raw) => {
|
|
610
690
|
const t = this.cache.applyUpdate(raw);
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
691
|
+
if (resolve2) {
|
|
692
|
+
resolve2({ value: t, done: false });
|
|
693
|
+
resolve2 = null;
|
|
614
694
|
} else queue.push(t);
|
|
615
695
|
},
|
|
616
696
|
onStatusChange: (s) => {
|
|
@@ -623,7 +703,7 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
623
703
|
if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
|
|
624
704
|
if (done) return Promise.resolve({ value: void 0, done: true });
|
|
625
705
|
return new Promise((r) => {
|
|
626
|
-
|
|
706
|
+
resolve2 = r;
|
|
627
707
|
});
|
|
628
708
|
},
|
|
629
709
|
return: async () => {
|
|
@@ -786,17 +866,357 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
786
866
|
|
|
787
867
|
// src/shared/utils/index.ts
|
|
788
868
|
import { isRenderableTrack as isRenderableTrack2, haversineKm, isInsideBbox, toCurrentTrack, clamp } from "@aeroswarm/core/utils";
|
|
869
|
+
|
|
870
|
+
// src/shared/motion/index.ts
|
|
871
|
+
var KT_TO_MS = 0.514444;
|
|
872
|
+
var M_PER_DEG = 111320;
|
|
873
|
+
var MAX_PLAUSIBLE_KT = 1500;
|
|
874
|
+
var OFFSET_WINDOW = 20;
|
|
875
|
+
var ALT_EPSILON_FT = 10;
|
|
876
|
+
var HEADING_EPSILON_DEG = 0.5;
|
|
877
|
+
var SURFACE_ALT_FT = 150;
|
|
878
|
+
var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
|
|
879
|
+
var smoothstep = (x) => {
|
|
880
|
+
const c = clamp01(x);
|
|
881
|
+
return c * c * (3 - 2 * c);
|
|
882
|
+
};
|
|
883
|
+
var wrapDeg = (d) => ((d + 180) % 360 + 360) % 360 - 180;
|
|
884
|
+
var norm360 = (d) => (d % 360 + 360) % 360;
|
|
885
|
+
var cosLat = (lat) => Math.max(0.01, Math.cos(lat * Math.PI / 180));
|
|
886
|
+
function validHeading(h) {
|
|
887
|
+
return h != null && Number.isFinite(h) && h >= 0 && h <= 360 ? h % 360 : null;
|
|
888
|
+
}
|
|
889
|
+
function move(lat, lon, bearingDeg, distM) {
|
|
890
|
+
const rad = bearingDeg * Math.PI / 180;
|
|
891
|
+
const dLat = distM * Math.cos(rad) / M_PER_DEG;
|
|
892
|
+
const dLon = distM * Math.sin(rad) / (M_PER_DEG * cosLat(lat + dLat / 2));
|
|
893
|
+
return { lat: lat + dLat, lon: lon + dLon };
|
|
894
|
+
}
|
|
895
|
+
function distanceM(aLat, aLon, bLat, bLon) {
|
|
896
|
+
const dy = (bLat - aLat) * M_PER_DEG;
|
|
897
|
+
const dx = (bLon - aLon) * M_PER_DEG * cosLat((aLat + bLat) / 2);
|
|
898
|
+
return Math.hypot(dx, dy);
|
|
899
|
+
}
|
|
900
|
+
var headingChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(wrapDeg(a - b)) > HEADING_EPSILON_DEG;
|
|
901
|
+
var altitudeChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(a - b) >= ALT_EPSILON_FT;
|
|
902
|
+
var defaultIsSurface = (t) => t.targetType === "vehicle" || t.altitudeFt != null && t.altitudeFt <= SURFACE_ALT_FT;
|
|
903
|
+
function resolve(o) {
|
|
904
|
+
return {
|
|
905
|
+
blendMs: o.blendMs ?? 1500,
|
|
906
|
+
headingSmoothMs: o.headingSmoothMs ?? 800,
|
|
907
|
+
maxExtrapolateMs: o.maxExtrapolateMs ?? { air: 2e4, surface: 8e3 },
|
|
908
|
+
minMovingKt: o.minMovingKt ?? 5,
|
|
909
|
+
snapDistanceM: o.snapDistanceM ?? 5e3,
|
|
910
|
+
frameHz: o.frameHz ?? 30,
|
|
911
|
+
epsilonM: o.epsilonM ?? 0.5,
|
|
912
|
+
extrapolate: o.extrapolate ?? true,
|
|
913
|
+
isSurface: o.isSurface ?? defaultIsSurface,
|
|
914
|
+
clock: o.clock ?? Date.now,
|
|
915
|
+
scheduler: o.scheduler
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function defaultScheduler() {
|
|
919
|
+
const g = globalThis;
|
|
920
|
+
if (typeof g.requestAnimationFrame === "function" && typeof g.cancelAnimationFrame === "function") {
|
|
921
|
+
return {
|
|
922
|
+
request: (cb) => g.requestAnimationFrame(cb),
|
|
923
|
+
cancel: (h) => g.cancelAnimationFrame(h)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
request: (cb) => setTimeout(() => cb(Date.now()), 16),
|
|
928
|
+
cancel: (h) => clearTimeout(h)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
var Motion = class {
|
|
932
|
+
o;
|
|
933
|
+
entries = /* @__PURE__ */ new Map();
|
|
934
|
+
/** Tracks whose picture can still change. Everything else costs nothing per frame. */
|
|
935
|
+
active = /* @__PURE__ */ new Set();
|
|
936
|
+
listeners = /* @__PURE__ */ new Set();
|
|
937
|
+
offsets = [];
|
|
938
|
+
offset = 0;
|
|
939
|
+
handle = null;
|
|
940
|
+
lastFrameAt = Number.NEGATIVE_INFINITY;
|
|
941
|
+
constructor(options) {
|
|
942
|
+
this.o = resolve(options);
|
|
943
|
+
}
|
|
944
|
+
get size() {
|
|
945
|
+
return this.entries.size;
|
|
946
|
+
}
|
|
947
|
+
offsetMs() {
|
|
948
|
+
return this.offset;
|
|
949
|
+
}
|
|
950
|
+
// ── time ────────────────────────────────────────────────────────────────
|
|
951
|
+
/** Browser clock + learned offset = the server's clock. */
|
|
952
|
+
serverNow(at) {
|
|
953
|
+
return (at ?? this.o.clock()) + this.offset;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* `serverTime - browserNow` is the true offset MINUS the network delay, so the
|
|
957
|
+
* largest recent value is the one with the least delay, i.e. the best estimate.
|
|
958
|
+
*/
|
|
959
|
+
observe(serverTime) {
|
|
960
|
+
if (!Number.isFinite(serverTime)) return;
|
|
961
|
+
this.offsets.push(serverTime - this.o.clock());
|
|
962
|
+
if (this.offsets.length > OFFSET_WINDOW) this.offsets.shift();
|
|
963
|
+
this.offset = Math.max(...this.offsets);
|
|
964
|
+
}
|
|
965
|
+
cap(e) {
|
|
966
|
+
const m = this.o.maxExtrapolateMs;
|
|
967
|
+
return e.surface ? m.surface : m.air;
|
|
968
|
+
}
|
|
969
|
+
// ── model ───────────────────────────────────────────────────────────────
|
|
970
|
+
posAt(e, s) {
|
|
971
|
+
const cap = this.cap(e);
|
|
972
|
+
let elapsed = Math.min(Math.max(0, s - e.fixAt), cap);
|
|
973
|
+
if (e.frozen !== null) elapsed = Math.min(elapsed, e.frozen);
|
|
974
|
+
if (!this.o.extrapolate || e.course === null || e.speedMs <= 0) {
|
|
975
|
+
return { lat: e.fixLat, lon: e.fixLon, extrapolating: false };
|
|
976
|
+
}
|
|
977
|
+
const p = move(e.fixLat, e.fixLon, e.course, e.speedMs * elapsed / 1e3);
|
|
978
|
+
return { ...p, extrapolating: e.frozen === null && elapsed < cap };
|
|
979
|
+
}
|
|
980
|
+
sampleAt(e, s) {
|
|
981
|
+
const p = this.posAt(e, s);
|
|
982
|
+
const rest = 1 - smoothstep((s - e.blendStart) / this.o.blendMs);
|
|
983
|
+
let headingDeg = null;
|
|
984
|
+
if (e.hdgTo !== null) {
|
|
985
|
+
const from = e.hdgFrom ?? e.hdgTo;
|
|
986
|
+
const left = 1 - smoothstep((s - e.hdgStart) / this.o.headingSmoothMs);
|
|
987
|
+
headingDeg = norm360(e.hdgTo + wrapDeg(from - e.hdgTo) * left);
|
|
988
|
+
}
|
|
989
|
+
let altitudeFt = null;
|
|
990
|
+
if (e.altTo !== null) {
|
|
991
|
+
const k = smoothstep((s - e.altStart) / this.o.blendMs);
|
|
992
|
+
altitudeFt = e.altFrom === null ? e.altTo : e.altFrom + (e.altTo - e.altFrom) * k;
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
lat: p.lat + e.errLat * rest,
|
|
996
|
+
lon: p.lon + e.errLon * rest,
|
|
997
|
+
headingDeg,
|
|
998
|
+
altitudeFt,
|
|
999
|
+
moving: e.speedMs > 0,
|
|
1000
|
+
extrapolating: p.extrapolating
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
// ── input ───────────────────────────────────────────────────────────────
|
|
1004
|
+
update(t, meta) {
|
|
1005
|
+
if (meta?.serverTime !== void 0) this.observe(meta.serverTime);
|
|
1006
|
+
if (t.lat == null || t.lon == null || !Number.isFinite(t.lat) || !Number.isFinite(t.lon)) {
|
|
1007
|
+
this.remove(t.trackId);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const s = this.serverNow();
|
|
1011
|
+
const e = this.entries.get(t.trackId);
|
|
1012
|
+
if (!e) {
|
|
1013
|
+
this.create(t, s);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.refresh(e, t, s);
|
|
1017
|
+
}
|
|
1018
|
+
moving(t, course) {
|
|
1019
|
+
const kt = t.groundSpeedKt;
|
|
1020
|
+
return course !== null && kt != null && Number.isFinite(kt) && kt >= this.o.minMovingKt && kt <= MAX_PLAUSIBLE_KT;
|
|
1021
|
+
}
|
|
1022
|
+
create(t, s) {
|
|
1023
|
+
const course = validHeading(t.headingDeg);
|
|
1024
|
+
const moving = this.moving(t, course);
|
|
1025
|
+
const e = {
|
|
1026
|
+
id: t.trackId,
|
|
1027
|
+
fixLat: t.lat,
|
|
1028
|
+
fixLon: t.lon,
|
|
1029
|
+
fixAt: t.lastSeenAt,
|
|
1030
|
+
speedMs: moving ? t.groundSpeedKt * KT_TO_MS : 0,
|
|
1031
|
+
course,
|
|
1032
|
+
surface: this.o.isSurface(t),
|
|
1033
|
+
frozen: null,
|
|
1034
|
+
errLat: 0,
|
|
1035
|
+
errLon: 0,
|
|
1036
|
+
blendStart: s,
|
|
1037
|
+
hdgFrom: course,
|
|
1038
|
+
hdgTo: course,
|
|
1039
|
+
hdgStart: s,
|
|
1040
|
+
altFrom: null,
|
|
1041
|
+
altTo: t.altitudeFt,
|
|
1042
|
+
altStart: s,
|
|
1043
|
+
settleAt: 0,
|
|
1044
|
+
emitted: null
|
|
1045
|
+
};
|
|
1046
|
+
if (t.status === "lost") e.frozen = this.frozenAt(e, s);
|
|
1047
|
+
this.entries.set(e.id, e);
|
|
1048
|
+
this.settle(e);
|
|
1049
|
+
this.active.add(e.id);
|
|
1050
|
+
this.ensureLoop();
|
|
1051
|
+
}
|
|
1052
|
+
frozenAt(e, s) {
|
|
1053
|
+
return Math.min(Math.max(0, s - e.fixAt), this.cap(e));
|
|
1054
|
+
}
|
|
1055
|
+
refresh(e, t, s) {
|
|
1056
|
+
e.surface = this.o.isSurface(t);
|
|
1057
|
+
if (t.status === "lost") e.frozen ??= this.frozenAt(e, s);
|
|
1058
|
+
else e.frozen = null;
|
|
1059
|
+
const lat = t.lat;
|
|
1060
|
+
const lon = t.lon;
|
|
1061
|
+
const older = t.lastSeenAt < e.fixAt;
|
|
1062
|
+
const same = t.lastSeenAt === e.fixAt && lat === e.fixLat && lon === e.fixLon;
|
|
1063
|
+
if (older || same) {
|
|
1064
|
+
this.settle(e);
|
|
1065
|
+
this.touch(e);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const shown = this.sampleAt(e, s);
|
|
1069
|
+
const course = validHeading(t.headingDeg) ?? e.course;
|
|
1070
|
+
const moving = this.moving(t, course);
|
|
1071
|
+
e.fixLat = lat;
|
|
1072
|
+
e.fixLon = lon;
|
|
1073
|
+
e.fixAt = t.lastSeenAt;
|
|
1074
|
+
e.speedMs = moving ? t.groundSpeedKt * KT_TO_MS : 0;
|
|
1075
|
+
e.course = course;
|
|
1076
|
+
const predicted = this.posAt(e, s);
|
|
1077
|
+
const jump = distanceM(shown.lat, shown.lon, predicted.lat, predicted.lon);
|
|
1078
|
+
const snap = jump > this.o.snapDistanceM;
|
|
1079
|
+
e.errLat = snap ? 0 : shown.lat - predicted.lat;
|
|
1080
|
+
e.errLon = snap ? 0 : shown.lon - predicted.lon;
|
|
1081
|
+
e.blendStart = s;
|
|
1082
|
+
e.hdgFrom = snap ? course : shown.headingDeg ?? course;
|
|
1083
|
+
e.hdgTo = course;
|
|
1084
|
+
e.hdgStart = s;
|
|
1085
|
+
e.altFrom = snap ? t.altitudeFt : shown.altitudeFt;
|
|
1086
|
+
e.altTo = t.altitudeFt;
|
|
1087
|
+
e.altStart = s;
|
|
1088
|
+
this.settle(e);
|
|
1089
|
+
this.touch(e);
|
|
1090
|
+
}
|
|
1091
|
+
/** The server time after which nothing about this track changes on its own. */
|
|
1092
|
+
settle(e) {
|
|
1093
|
+
const ends = [e.blendStart + this.o.blendMs, e.hdgStart + this.o.headingSmoothMs, e.altStart + this.o.blendMs];
|
|
1094
|
+
if (this.o.extrapolate && e.speedMs > 0 && e.frozen === null) ends.push(e.fixAt + this.cap(e));
|
|
1095
|
+
e.settleAt = Math.max(...ends);
|
|
1096
|
+
}
|
|
1097
|
+
touch(e) {
|
|
1098
|
+
this.active.add(e.id);
|
|
1099
|
+
this.ensureLoop();
|
|
1100
|
+
}
|
|
1101
|
+
remove(trackId) {
|
|
1102
|
+
this.entries.delete(trackId);
|
|
1103
|
+
this.active.delete(trackId);
|
|
1104
|
+
}
|
|
1105
|
+
clear() {
|
|
1106
|
+
this.entries.clear();
|
|
1107
|
+
this.active.clear();
|
|
1108
|
+
}
|
|
1109
|
+
// ── output ──────────────────────────────────────────────────────────────
|
|
1110
|
+
sample(trackId, at) {
|
|
1111
|
+
const e = this.entries.get(trackId);
|
|
1112
|
+
return e ? this.sampleAt(e, this.serverNow(at)) : void 0;
|
|
1113
|
+
}
|
|
1114
|
+
differs(a, b) {
|
|
1115
|
+
if (a === null || a.moving !== b.moving || a.extrapolating !== b.extrapolating) return true;
|
|
1116
|
+
if (distanceM(a.lat, a.lon, b.lat, b.lon) > this.o.epsilonM) return true;
|
|
1117
|
+
return headingChanged(a.headingDeg, b.headingDeg) || altitudeChanged(a.altitudeFt, b.altitudeFt);
|
|
1118
|
+
}
|
|
1119
|
+
tick(at) {
|
|
1120
|
+
const s = this.serverNow(at);
|
|
1121
|
+
const changed = /* @__PURE__ */ new Map();
|
|
1122
|
+
for (const id of [...this.active]) {
|
|
1123
|
+
const e = this.entries.get(id);
|
|
1124
|
+
if (!e) {
|
|
1125
|
+
this.active.delete(id);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const sample = this.sampleAt(e, s);
|
|
1129
|
+
if (this.differs(e.emitted, sample)) {
|
|
1130
|
+
e.emitted = sample;
|
|
1131
|
+
changed.set(id, sample);
|
|
1132
|
+
}
|
|
1133
|
+
if (s >= e.settleAt) this.active.delete(id);
|
|
1134
|
+
}
|
|
1135
|
+
return changed;
|
|
1136
|
+
}
|
|
1137
|
+
// ── frame loop ──────────────────────────────────────────────────────────
|
|
1138
|
+
start(onFrame) {
|
|
1139
|
+
this.listeners.add(onFrame);
|
|
1140
|
+
this.ensureLoop();
|
|
1141
|
+
return () => {
|
|
1142
|
+
this.listeners.delete(onFrame);
|
|
1143
|
+
if (this.listeners.size === 0) this.cancelLoop();
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
ensureLoop() {
|
|
1147
|
+
if (this.handle !== null || this.listeners.size === 0 || this.active.size === 0) return;
|
|
1148
|
+
const scheduler = this.o.scheduler ?? (this.o.scheduler = defaultScheduler());
|
|
1149
|
+
this.handle = scheduler.request((ts) => this.frame(ts));
|
|
1150
|
+
}
|
|
1151
|
+
cancelLoop() {
|
|
1152
|
+
if (this.handle === null) return;
|
|
1153
|
+
(this.o.scheduler ?? defaultScheduler()).cancel(this.handle);
|
|
1154
|
+
this.handle = null;
|
|
1155
|
+
}
|
|
1156
|
+
frame(ts) {
|
|
1157
|
+
this.handle = null;
|
|
1158
|
+
if (this.listeners.size === 0) return;
|
|
1159
|
+
if (ts - this.lastFrameAt >= 1e3 / this.o.frameHz - 1) {
|
|
1160
|
+
this.lastFrameAt = ts;
|
|
1161
|
+
const changed = this.tick();
|
|
1162
|
+
if (changed.size > 0) {
|
|
1163
|
+
const frame = { at: this.serverNow(), changed };
|
|
1164
|
+
for (const fn of [...this.listeners]) fn(frame);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
this.ensureLoop();
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
function createTrackMotion(options = {}) {
|
|
1171
|
+
return new Motion(options);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/shared/source/index.ts
|
|
1175
|
+
function sourceLabel(source) {
|
|
1176
|
+
switch (source) {
|
|
1177
|
+
case "ADSB":
|
|
1178
|
+
case "ASTERIX_CAT021":
|
|
1179
|
+
return "ADSB";
|
|
1180
|
+
case "ASTERIX_CAT010":
|
|
1181
|
+
case "ASTERIX_CAT011":
|
|
1182
|
+
return "SURFACE";
|
|
1183
|
+
case "ASTERIX_CAT020":
|
|
1184
|
+
return "MLAT";
|
|
1185
|
+
case "ASTERIX_CAT048":
|
|
1186
|
+
return "RADAR";
|
|
1187
|
+
case "ASTERIX_CAT062":
|
|
1188
|
+
return "SYSTEM";
|
|
1189
|
+
case "FUSED":
|
|
1190
|
+
return "FUSED";
|
|
1191
|
+
default: {
|
|
1192
|
+
const unreachable = source;
|
|
1193
|
+
return unreachable;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function formatDataSource(track) {
|
|
1198
|
+
const kind = sourceLabel(track.source);
|
|
1199
|
+
const ds = track.dataSource;
|
|
1200
|
+
return ds ? `${kind} ${ds.sac}/${ds.sic}` : kind;
|
|
1201
|
+
}
|
|
789
1202
|
export {
|
|
1203
|
+
AeroApiError,
|
|
790
1204
|
AeroReplayClient,
|
|
791
1205
|
AeroSwarm,
|
|
792
1206
|
AeroWsClient,
|
|
793
1207
|
LABEL_COLORS,
|
|
794
1208
|
TrackCache,
|
|
795
1209
|
clamp,
|
|
1210
|
+
codeForStatus,
|
|
1211
|
+
createTrackMotion,
|
|
1212
|
+
formatDataSource,
|
|
796
1213
|
getLabelCategory,
|
|
797
1214
|
haversineKm,
|
|
798
1215
|
isInsideBbox,
|
|
799
1216
|
isRenderableTrack2 as isRenderableTrack,
|
|
1217
|
+
parseApiResponse,
|
|
1218
|
+
readApiBody,
|
|
1219
|
+
sourceLabel,
|
|
800
1220
|
toCurrentTrack,
|
|
801
1221
|
withLabel
|
|
802
1222
|
};
|