@aeroswarm/client 0.1.12 → 0.1.13
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 +35 -1
- package/dist/index-BXiTmKdS.d.ts +701 -0
- package/dist/index.d.ts +71 -688
- package/dist/index.js +317 -11
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +24 -2
- package/dist/react/index.js +355 -30
- package/dist/react/index.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -2
- package/dist/sdk/index.js +7 -6
- package/dist/sdk/index.js.map +1 -1
- package/package.json +1 -1
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);
|
|
@@ -567,15 +568,15 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
567
568
|
this._status = s;
|
|
568
569
|
callbacks.onStatusChange?.(s);
|
|
569
570
|
},
|
|
570
|
-
onSnapshot: (tracks) => {
|
|
571
|
+
onSnapshot: (tracks, meta) => {
|
|
571
572
|
const labeled = this.cache.applySnapshot(tracks);
|
|
572
573
|
this._lastUpdateAt = Date.now();
|
|
573
|
-
callbacks.onSnapshot?.(labeled);
|
|
574
|
+
callbacks.onSnapshot?.(labeled, meta);
|
|
574
575
|
},
|
|
575
|
-
onTrackUpdate: (track) => {
|
|
576
|
+
onTrackUpdate: (track, meta) => {
|
|
576
577
|
const labeled = this.cache.applyUpdate(track);
|
|
577
578
|
this._lastUpdateAt = Date.now();
|
|
578
|
-
callbacks.onTrackUpdate?.(labeled);
|
|
579
|
+
callbacks.onTrackUpdate?.(labeled, meta);
|
|
579
580
|
},
|
|
580
581
|
onTrackRemoved: (ev) => {
|
|
581
582
|
this.cache.applyRemoved(ev);
|
|
@@ -600,7 +601,7 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
600
601
|
reconnect: () => this.wsClient.reconnect(),
|
|
601
602
|
stream: (opts) => {
|
|
602
603
|
const queue = [];
|
|
603
|
-
let
|
|
604
|
+
let resolve2 = null;
|
|
604
605
|
let done = false;
|
|
605
606
|
this.wsClient.connect({ encoding: "json", ...opts }, {
|
|
606
607
|
onSnapshot: (tracks) => {
|
|
@@ -608,9 +609,9 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
608
609
|
},
|
|
609
610
|
onTrackUpdate: (raw) => {
|
|
610
611
|
const t = this.cache.applyUpdate(raw);
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
612
|
+
if (resolve2) {
|
|
613
|
+
resolve2({ value: t, done: false });
|
|
614
|
+
resolve2 = null;
|
|
614
615
|
} else queue.push(t);
|
|
615
616
|
},
|
|
616
617
|
onStatusChange: (s) => {
|
|
@@ -623,7 +624,7 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
623
624
|
if (queue.length) return Promise.resolve({ value: queue.shift(), done: false });
|
|
624
625
|
if (done) return Promise.resolve({ value: void 0, done: true });
|
|
625
626
|
return new Promise((r) => {
|
|
626
|
-
|
|
627
|
+
resolve2 = r;
|
|
627
628
|
});
|
|
628
629
|
},
|
|
629
630
|
return: async () => {
|
|
@@ -786,6 +787,310 @@ var AeroSwarm = class _AeroSwarm {
|
|
|
786
787
|
|
|
787
788
|
// src/shared/utils/index.ts
|
|
788
789
|
import { isRenderableTrack as isRenderableTrack2, haversineKm, isInsideBbox, toCurrentTrack, clamp } from "@aeroswarm/core/utils";
|
|
790
|
+
|
|
791
|
+
// src/shared/motion/index.ts
|
|
792
|
+
var KT_TO_MS = 0.514444;
|
|
793
|
+
var M_PER_DEG = 111320;
|
|
794
|
+
var MAX_PLAUSIBLE_KT = 1500;
|
|
795
|
+
var OFFSET_WINDOW = 20;
|
|
796
|
+
var ALT_EPSILON_FT = 10;
|
|
797
|
+
var HEADING_EPSILON_DEG = 0.5;
|
|
798
|
+
var SURFACE_ALT_FT = 150;
|
|
799
|
+
var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
|
|
800
|
+
var smoothstep = (x) => {
|
|
801
|
+
const c = clamp01(x);
|
|
802
|
+
return c * c * (3 - 2 * c);
|
|
803
|
+
};
|
|
804
|
+
var wrapDeg = (d) => ((d + 180) % 360 + 360) % 360 - 180;
|
|
805
|
+
var norm360 = (d) => (d % 360 + 360) % 360;
|
|
806
|
+
var cosLat = (lat) => Math.max(0.01, Math.cos(lat * Math.PI / 180));
|
|
807
|
+
function validHeading(h) {
|
|
808
|
+
return h != null && Number.isFinite(h) && h >= 0 && h <= 360 ? h % 360 : null;
|
|
809
|
+
}
|
|
810
|
+
function move(lat, lon, bearingDeg, distM) {
|
|
811
|
+
const rad = bearingDeg * Math.PI / 180;
|
|
812
|
+
const dLat = distM * Math.cos(rad) / M_PER_DEG;
|
|
813
|
+
const dLon = distM * Math.sin(rad) / (M_PER_DEG * cosLat(lat + dLat / 2));
|
|
814
|
+
return { lat: lat + dLat, lon: lon + dLon };
|
|
815
|
+
}
|
|
816
|
+
function distanceM(aLat, aLon, bLat, bLon) {
|
|
817
|
+
const dy = (bLat - aLat) * M_PER_DEG;
|
|
818
|
+
const dx = (bLon - aLon) * M_PER_DEG * cosLat((aLat + bLat) / 2);
|
|
819
|
+
return Math.hypot(dx, dy);
|
|
820
|
+
}
|
|
821
|
+
var headingChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(wrapDeg(a - b)) > HEADING_EPSILON_DEG;
|
|
822
|
+
var altitudeChanged = (a, b) => a === null || b === null ? a !== b : Math.abs(a - b) >= ALT_EPSILON_FT;
|
|
823
|
+
var defaultIsSurface = (t) => t.targetType === "vehicle" || t.altitudeFt != null && t.altitudeFt <= SURFACE_ALT_FT;
|
|
824
|
+
function resolve(o) {
|
|
825
|
+
return {
|
|
826
|
+
blendMs: o.blendMs ?? 1500,
|
|
827
|
+
headingSmoothMs: o.headingSmoothMs ?? 800,
|
|
828
|
+
maxExtrapolateMs: o.maxExtrapolateMs ?? { air: 2e4, surface: 8e3 },
|
|
829
|
+
minMovingKt: o.minMovingKt ?? 5,
|
|
830
|
+
snapDistanceM: o.snapDistanceM ?? 5e3,
|
|
831
|
+
frameHz: o.frameHz ?? 30,
|
|
832
|
+
epsilonM: o.epsilonM ?? 0.5,
|
|
833
|
+
extrapolate: o.extrapolate ?? true,
|
|
834
|
+
isSurface: o.isSurface ?? defaultIsSurface,
|
|
835
|
+
clock: o.clock ?? Date.now,
|
|
836
|
+
scheduler: o.scheduler
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function defaultScheduler() {
|
|
840
|
+
const g = globalThis;
|
|
841
|
+
if (typeof g.requestAnimationFrame === "function" && typeof g.cancelAnimationFrame === "function") {
|
|
842
|
+
return {
|
|
843
|
+
request: (cb) => g.requestAnimationFrame(cb),
|
|
844
|
+
cancel: (h) => g.cancelAnimationFrame(h)
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
request: (cb) => setTimeout(() => cb(Date.now()), 16),
|
|
849
|
+
cancel: (h) => clearTimeout(h)
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
var Motion = class {
|
|
853
|
+
o;
|
|
854
|
+
entries = /* @__PURE__ */ new Map();
|
|
855
|
+
/** Tracks whose picture can still change. Everything else costs nothing per frame. */
|
|
856
|
+
active = /* @__PURE__ */ new Set();
|
|
857
|
+
listeners = /* @__PURE__ */ new Set();
|
|
858
|
+
offsets = [];
|
|
859
|
+
offset = 0;
|
|
860
|
+
handle = null;
|
|
861
|
+
lastFrameAt = Number.NEGATIVE_INFINITY;
|
|
862
|
+
constructor(options) {
|
|
863
|
+
this.o = resolve(options);
|
|
864
|
+
}
|
|
865
|
+
get size() {
|
|
866
|
+
return this.entries.size;
|
|
867
|
+
}
|
|
868
|
+
offsetMs() {
|
|
869
|
+
return this.offset;
|
|
870
|
+
}
|
|
871
|
+
// ── time ────────────────────────────────────────────────────────────────
|
|
872
|
+
/** Browser clock + learned offset = the server's clock. */
|
|
873
|
+
serverNow(at) {
|
|
874
|
+
return (at ?? this.o.clock()) + this.offset;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* `serverTime - browserNow` is the true offset MINUS the network delay, so the
|
|
878
|
+
* largest recent value is the one with the least delay, i.e. the best estimate.
|
|
879
|
+
*/
|
|
880
|
+
observe(serverTime) {
|
|
881
|
+
if (!Number.isFinite(serverTime)) return;
|
|
882
|
+
this.offsets.push(serverTime - this.o.clock());
|
|
883
|
+
if (this.offsets.length > OFFSET_WINDOW) this.offsets.shift();
|
|
884
|
+
this.offset = Math.max(...this.offsets);
|
|
885
|
+
}
|
|
886
|
+
cap(e) {
|
|
887
|
+
const m = this.o.maxExtrapolateMs;
|
|
888
|
+
return e.surface ? m.surface : m.air;
|
|
889
|
+
}
|
|
890
|
+
// ── model ───────────────────────────────────────────────────────────────
|
|
891
|
+
posAt(e, s) {
|
|
892
|
+
const cap = this.cap(e);
|
|
893
|
+
let elapsed = Math.min(Math.max(0, s - e.fixAt), cap);
|
|
894
|
+
if (e.frozen !== null) elapsed = Math.min(elapsed, e.frozen);
|
|
895
|
+
if (!this.o.extrapolate || e.course === null || e.speedMs <= 0) {
|
|
896
|
+
return { lat: e.fixLat, lon: e.fixLon, extrapolating: false };
|
|
897
|
+
}
|
|
898
|
+
const p = move(e.fixLat, e.fixLon, e.course, e.speedMs * elapsed / 1e3);
|
|
899
|
+
return { ...p, extrapolating: e.frozen === null && elapsed < cap };
|
|
900
|
+
}
|
|
901
|
+
sampleAt(e, s) {
|
|
902
|
+
const p = this.posAt(e, s);
|
|
903
|
+
const rest = 1 - smoothstep((s - e.blendStart) / this.o.blendMs);
|
|
904
|
+
let headingDeg = null;
|
|
905
|
+
if (e.hdgTo !== null) {
|
|
906
|
+
const from = e.hdgFrom ?? e.hdgTo;
|
|
907
|
+
const left = 1 - smoothstep((s - e.hdgStart) / this.o.headingSmoothMs);
|
|
908
|
+
headingDeg = norm360(e.hdgTo + wrapDeg(from - e.hdgTo) * left);
|
|
909
|
+
}
|
|
910
|
+
let altitudeFt = null;
|
|
911
|
+
if (e.altTo !== null) {
|
|
912
|
+
const k = smoothstep((s - e.altStart) / this.o.blendMs);
|
|
913
|
+
altitudeFt = e.altFrom === null ? e.altTo : e.altFrom + (e.altTo - e.altFrom) * k;
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
lat: p.lat + e.errLat * rest,
|
|
917
|
+
lon: p.lon + e.errLon * rest,
|
|
918
|
+
headingDeg,
|
|
919
|
+
altitudeFt,
|
|
920
|
+
moving: e.speedMs > 0,
|
|
921
|
+
extrapolating: p.extrapolating
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
// ── input ───────────────────────────────────────────────────────────────
|
|
925
|
+
update(t, meta) {
|
|
926
|
+
if (meta?.serverTime !== void 0) this.observe(meta.serverTime);
|
|
927
|
+
if (t.lat == null || t.lon == null || !Number.isFinite(t.lat) || !Number.isFinite(t.lon)) {
|
|
928
|
+
this.remove(t.trackId);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const s = this.serverNow();
|
|
932
|
+
const e = this.entries.get(t.trackId);
|
|
933
|
+
if (!e) {
|
|
934
|
+
this.create(t, s);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
this.refresh(e, t, s);
|
|
938
|
+
}
|
|
939
|
+
moving(t, course) {
|
|
940
|
+
const kt = t.groundSpeedKt;
|
|
941
|
+
return course !== null && kt != null && Number.isFinite(kt) && kt >= this.o.minMovingKt && kt <= MAX_PLAUSIBLE_KT;
|
|
942
|
+
}
|
|
943
|
+
create(t, s) {
|
|
944
|
+
const course = validHeading(t.headingDeg);
|
|
945
|
+
const moving = this.moving(t, course);
|
|
946
|
+
const e = {
|
|
947
|
+
id: t.trackId,
|
|
948
|
+
fixLat: t.lat,
|
|
949
|
+
fixLon: t.lon,
|
|
950
|
+
fixAt: t.lastSeenAt,
|
|
951
|
+
speedMs: moving ? t.groundSpeedKt * KT_TO_MS : 0,
|
|
952
|
+
course,
|
|
953
|
+
surface: this.o.isSurface(t),
|
|
954
|
+
frozen: null,
|
|
955
|
+
errLat: 0,
|
|
956
|
+
errLon: 0,
|
|
957
|
+
blendStart: s,
|
|
958
|
+
hdgFrom: course,
|
|
959
|
+
hdgTo: course,
|
|
960
|
+
hdgStart: s,
|
|
961
|
+
altFrom: null,
|
|
962
|
+
altTo: t.altitudeFt,
|
|
963
|
+
altStart: s,
|
|
964
|
+
settleAt: 0,
|
|
965
|
+
emitted: null
|
|
966
|
+
};
|
|
967
|
+
if (t.status === "lost") e.frozen = this.frozenAt(e, s);
|
|
968
|
+
this.entries.set(e.id, e);
|
|
969
|
+
this.settle(e);
|
|
970
|
+
this.active.add(e.id);
|
|
971
|
+
this.ensureLoop();
|
|
972
|
+
}
|
|
973
|
+
frozenAt(e, s) {
|
|
974
|
+
return Math.min(Math.max(0, s - e.fixAt), this.cap(e));
|
|
975
|
+
}
|
|
976
|
+
refresh(e, t, s) {
|
|
977
|
+
e.surface = this.o.isSurface(t);
|
|
978
|
+
if (t.status === "lost") e.frozen ??= this.frozenAt(e, s);
|
|
979
|
+
else e.frozen = null;
|
|
980
|
+
const lat = t.lat;
|
|
981
|
+
const lon = t.lon;
|
|
982
|
+
const older = t.lastSeenAt < e.fixAt;
|
|
983
|
+
const same = t.lastSeenAt === e.fixAt && lat === e.fixLat && lon === e.fixLon;
|
|
984
|
+
if (older || same) {
|
|
985
|
+
this.settle(e);
|
|
986
|
+
this.touch(e);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const shown = this.sampleAt(e, s);
|
|
990
|
+
const course = validHeading(t.headingDeg) ?? e.course;
|
|
991
|
+
const moving = this.moving(t, course);
|
|
992
|
+
e.fixLat = lat;
|
|
993
|
+
e.fixLon = lon;
|
|
994
|
+
e.fixAt = t.lastSeenAt;
|
|
995
|
+
e.speedMs = moving ? t.groundSpeedKt * KT_TO_MS : 0;
|
|
996
|
+
e.course = course;
|
|
997
|
+
const predicted = this.posAt(e, s);
|
|
998
|
+
const jump = distanceM(shown.lat, shown.lon, predicted.lat, predicted.lon);
|
|
999
|
+
const snap = jump > this.o.snapDistanceM;
|
|
1000
|
+
e.errLat = snap ? 0 : shown.lat - predicted.lat;
|
|
1001
|
+
e.errLon = snap ? 0 : shown.lon - predicted.lon;
|
|
1002
|
+
e.blendStart = s;
|
|
1003
|
+
e.hdgFrom = snap ? course : shown.headingDeg ?? course;
|
|
1004
|
+
e.hdgTo = course;
|
|
1005
|
+
e.hdgStart = s;
|
|
1006
|
+
e.altFrom = snap ? t.altitudeFt : shown.altitudeFt;
|
|
1007
|
+
e.altTo = t.altitudeFt;
|
|
1008
|
+
e.altStart = s;
|
|
1009
|
+
this.settle(e);
|
|
1010
|
+
this.touch(e);
|
|
1011
|
+
}
|
|
1012
|
+
/** The server time after which nothing about this track changes on its own. */
|
|
1013
|
+
settle(e) {
|
|
1014
|
+
const ends = [e.blendStart + this.o.blendMs, e.hdgStart + this.o.headingSmoothMs, e.altStart + this.o.blendMs];
|
|
1015
|
+
if (this.o.extrapolate && e.speedMs > 0 && e.frozen === null) ends.push(e.fixAt + this.cap(e));
|
|
1016
|
+
e.settleAt = Math.max(...ends);
|
|
1017
|
+
}
|
|
1018
|
+
touch(e) {
|
|
1019
|
+
this.active.add(e.id);
|
|
1020
|
+
this.ensureLoop();
|
|
1021
|
+
}
|
|
1022
|
+
remove(trackId) {
|
|
1023
|
+
this.entries.delete(trackId);
|
|
1024
|
+
this.active.delete(trackId);
|
|
1025
|
+
}
|
|
1026
|
+
clear() {
|
|
1027
|
+
this.entries.clear();
|
|
1028
|
+
this.active.clear();
|
|
1029
|
+
}
|
|
1030
|
+
// ── output ──────────────────────────────────────────────────────────────
|
|
1031
|
+
sample(trackId, at) {
|
|
1032
|
+
const e = this.entries.get(trackId);
|
|
1033
|
+
return e ? this.sampleAt(e, this.serverNow(at)) : void 0;
|
|
1034
|
+
}
|
|
1035
|
+
differs(a, b) {
|
|
1036
|
+
if (a === null || a.moving !== b.moving || a.extrapolating !== b.extrapolating) return true;
|
|
1037
|
+
if (distanceM(a.lat, a.lon, b.lat, b.lon) > this.o.epsilonM) return true;
|
|
1038
|
+
return headingChanged(a.headingDeg, b.headingDeg) || altitudeChanged(a.altitudeFt, b.altitudeFt);
|
|
1039
|
+
}
|
|
1040
|
+
tick(at) {
|
|
1041
|
+
const s = this.serverNow(at);
|
|
1042
|
+
const changed = /* @__PURE__ */ new Map();
|
|
1043
|
+
for (const id of [...this.active]) {
|
|
1044
|
+
const e = this.entries.get(id);
|
|
1045
|
+
if (!e) {
|
|
1046
|
+
this.active.delete(id);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
const sample = this.sampleAt(e, s);
|
|
1050
|
+
if (this.differs(e.emitted, sample)) {
|
|
1051
|
+
e.emitted = sample;
|
|
1052
|
+
changed.set(id, sample);
|
|
1053
|
+
}
|
|
1054
|
+
if (s >= e.settleAt) this.active.delete(id);
|
|
1055
|
+
}
|
|
1056
|
+
return changed;
|
|
1057
|
+
}
|
|
1058
|
+
// ── frame loop ──────────────────────────────────────────────────────────
|
|
1059
|
+
start(onFrame) {
|
|
1060
|
+
this.listeners.add(onFrame);
|
|
1061
|
+
this.ensureLoop();
|
|
1062
|
+
return () => {
|
|
1063
|
+
this.listeners.delete(onFrame);
|
|
1064
|
+
if (this.listeners.size === 0) this.cancelLoop();
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
ensureLoop() {
|
|
1068
|
+
if (this.handle !== null || this.listeners.size === 0 || this.active.size === 0) return;
|
|
1069
|
+
const scheduler = this.o.scheduler ?? (this.o.scheduler = defaultScheduler());
|
|
1070
|
+
this.handle = scheduler.request((ts) => this.frame(ts));
|
|
1071
|
+
}
|
|
1072
|
+
cancelLoop() {
|
|
1073
|
+
if (this.handle === null) return;
|
|
1074
|
+
(this.o.scheduler ?? defaultScheduler()).cancel(this.handle);
|
|
1075
|
+
this.handle = null;
|
|
1076
|
+
}
|
|
1077
|
+
frame(ts) {
|
|
1078
|
+
this.handle = null;
|
|
1079
|
+
if (this.listeners.size === 0) return;
|
|
1080
|
+
if (ts - this.lastFrameAt >= 1e3 / this.o.frameHz - 1) {
|
|
1081
|
+
this.lastFrameAt = ts;
|
|
1082
|
+
const changed = this.tick();
|
|
1083
|
+
if (changed.size > 0) {
|
|
1084
|
+
const frame = { at: this.serverNow(), changed };
|
|
1085
|
+
for (const fn of [...this.listeners]) fn(frame);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
this.ensureLoop();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
function createTrackMotion(options = {}) {
|
|
1092
|
+
return new Motion(options);
|
|
1093
|
+
}
|
|
789
1094
|
export {
|
|
790
1095
|
AeroReplayClient,
|
|
791
1096
|
AeroSwarm,
|
|
@@ -793,6 +1098,7 @@ export {
|
|
|
793
1098
|
LABEL_COLORS,
|
|
794
1099
|
TrackCache,
|
|
795
1100
|
clamp,
|
|
1101
|
+
createTrackMotion,
|
|
796
1102
|
getLabelCategory,
|
|
797
1103
|
haversineKm,
|
|
798
1104
|
isInsideBbox,
|