@aeroswarm/live-server 0.1.1 → 0.1.2
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 +58 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +245 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,64 @@ wss.on("connection", (ws, req) => {
|
|
|
75
75
|
});
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
## Several sensors, one aircraft (fusion)
|
|
79
|
+
|
|
80
|
+
Radar, MLAT and ADS-B often see the same aircraft. Each feed makes its own
|
|
81
|
+
track id, and `TrackState` replaces a track whenever another id arrives for the
|
|
82
|
+
same `icao24`, so two feeds knock each other out on every update. Put the fuser
|
|
83
|
+
between the feeds and `state.ingest`:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createTrackFuser } from "@aeroswarm/live-server";
|
|
87
|
+
|
|
88
|
+
const fuser = createTrackFuser(); // or createTrackFuser({ spatial: true, ... })
|
|
89
|
+
|
|
90
|
+
trackSource.start((ev) => {
|
|
91
|
+
if (ev.kind !== "track") return;
|
|
92
|
+
const fused = fuser.ingest(ev.event as FusedTrackEvent); // null = nothing new to show
|
|
93
|
+
if (fused) { persist(fused); state.ingest(fused); }
|
|
94
|
+
});
|
|
95
|
+
state.on("removed", ({ trackId }) => fuser.forget(trackId));
|
|
96
|
+
setInterval(() => fuser.sweep(), 30_000); // forget sensors that went silent
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
What you get: one canonical `trackId` per aircraft, `sources` listing every
|
|
100
|
+
contributing sensor (`lifecycle: "merged"`), and the position of the preferred
|
|
101
|
+
sensor. A sensor that lets go of the track (`lifecycle: "dropped"`) does not
|
|
102
|
+
remove the aircraft while another sensor still sees it. With one sensor the
|
|
103
|
+
output is the same as without the fuser, with a stable track id.
|
|
104
|
+
|
|
105
|
+
**Matching.** By Mode-S address (`icao24`). Targets without one (surface
|
|
106
|
+
vehicles, SSR-only radar) get a synthetic id from sensor + track number
|
|
107
|
+
(`r50-176-42`), which means nothing to another sensor; they are only joined when
|
|
108
|
+
`spatial` is on, by distance and time. A candidate that is ambiguous (more than
|
|
109
|
+
one), or that carries a different Mode-S address, is never merged.
|
|
110
|
+
|
|
111
|
+
**Which sensor's position is shown.** The first match in the priority list for
|
|
112
|
+
the current state, where "on the ground" means some fresh report says
|
|
113
|
+
`onGround`. Entries are a protocol (`asterix-cat010`) or a feed's source id.
|
|
114
|
+
The default prefers MLAT in both lists, then ADS-B, the system track and radar.
|
|
115
|
+
A sensor silent for `maxSourceAgeMs` stops competing.
|
|
116
|
+
|
|
117
|
+
| Option | Default | |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `surfacePriority` | MLAT (CAT010/011/020), CAT021, CAT062, CAT048 | best first |
|
|
120
|
+
| `airbornePriority` | MLAT (CAT020/010/011), CAT021, CAT062, CAT048 | best first |
|
|
121
|
+
| `maxSourceAgeMs` | `5000` | silent sensor stops competing |
|
|
122
|
+
| `forgetAfterMs` | `600000` | sensor forgotten after this long silent |
|
|
123
|
+
| `spatial` | `false` | also match tracks that share no Mode-S address |
|
|
124
|
+
| `matchRadiusM` | `150` | spatial matching: distance |
|
|
125
|
+
| `matchMaxSkewMs` | `3000` | spatial matching: time difference |
|
|
126
|
+
| `disagreeM` | `300` | sensors further apart are counted as disagreeing |
|
|
127
|
+
| `now` | `Date.now` | clock, for tests |
|
|
128
|
+
|
|
129
|
+
`fuser.stats()` returns `{ aircraft, merged, spatialMatches, ambiguous,
|
|
130
|
+
disagreements }`. `confidence.sourceAgreement` on a merged track falls from 1 to
|
|
131
|
+
0 as its sensors drift apart.
|
|
132
|
+
|
|
133
|
+
Fusion applies to what the host feeds it: events that arrive some other way
|
|
134
|
+
(HTTP, Kafka) are not fused unless the host passes them through `ingest` too.
|
|
135
|
+
|
|
78
136
|
## Protocol
|
|
79
137
|
|
|
80
138
|
JSON envelopes `{ type, seq, serverTime, payload }`, `seq` strictly increasing across the server.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CurrentTrack, FusedTrackEvent, FlightPlan, FlightPlanEvent } from '@aeroswarm/core/types';
|
|
2
|
+
import { FusedTrackEvent as FusedTrackEvent$1 } from '@aeroswarm/core';
|
|
2
3
|
|
|
3
4
|
/** Minimal socket surface. Satisfied by Bun's `ServerWebSocket` and the `ws` library. */
|
|
4
5
|
interface LiveSocket {
|
|
@@ -189,6 +190,70 @@ declare class MovementDetector {
|
|
|
189
190
|
}
|
|
190
191
|
declare function createMovementDetector(): MovementDetector;
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Several sensors (radar, MLAT, ...) often see the same aircraft. Each feed
|
|
195
|
+
* produces its own track id, so without this layer they replace one another
|
|
196
|
+
* on every update. The fuser keeps ONE canonical track per aircraft and picks
|
|
197
|
+
* which sensor's position to show.
|
|
198
|
+
*
|
|
199
|
+
* feed events ──► fuser.ingest ──► one event per aircraft ──► storage, live picture
|
|
200
|
+
*
|
|
201
|
+
* Matching: by Mode-S address when both have one. Tracks without it (surface
|
|
202
|
+
* vehicles, SSR-only radar: ids like "r50-176-42") only match across sensors
|
|
203
|
+
* when `spatial` is on, by position and time.
|
|
204
|
+
*/
|
|
205
|
+
interface FusionOptions {
|
|
206
|
+
/** Preferred sources while the aircraft is on the ground, best first. Each entry is a protocol ("asterix-cat010") or a source id. */
|
|
207
|
+
surfacePriority: string[];
|
|
208
|
+
/** Same, for an aircraft in the air. */
|
|
209
|
+
airbornePriority: string[];
|
|
210
|
+
/** A sensor silent for longer than this stops competing for the position. */
|
|
211
|
+
maxSourceAgeMs: number;
|
|
212
|
+
/** A sensor's contribution is forgotten after this long without an update. */
|
|
213
|
+
forgetAfterMs: number;
|
|
214
|
+
/** Also match tracks that share no Mode-S address, by distance and time. */
|
|
215
|
+
spatial: boolean;
|
|
216
|
+
matchRadiusM: number;
|
|
217
|
+
matchMaxSkewMs: number;
|
|
218
|
+
/** Two sensors further apart than this are reported as disagreeing. */
|
|
219
|
+
disagreeM: number;
|
|
220
|
+
now?: () => number;
|
|
221
|
+
}
|
|
222
|
+
/** MLAT first (CAT010 surface, CAT020 wide-area), then ADS-B, the system track, radar. */
|
|
223
|
+
declare const DEFAULT_SURFACE_PRIORITY: readonly string[];
|
|
224
|
+
/** MLAT is preferred in the air too. Put "asterix-cat062"/"asterix-cat048" first to prefer radar. */
|
|
225
|
+
declare const DEFAULT_AIRBORNE_PRIORITY: readonly string[];
|
|
226
|
+
/** Every option, with the defaults a host gets when it passes none. */
|
|
227
|
+
declare function defaultFusionOptions(): FusionOptions;
|
|
228
|
+
interface FusionStats {
|
|
229
|
+
/** Canonical tracks held. */
|
|
230
|
+
aircraft: number;
|
|
231
|
+
/** Of those, how many are fed by more than one sensor right now. */
|
|
232
|
+
merged: number;
|
|
233
|
+
/** Tracks joined by position because they shared no id. */
|
|
234
|
+
spatialMatches: number;
|
|
235
|
+
/** New tracks with several possible partners, left unmerged. */
|
|
236
|
+
ambiguous: number;
|
|
237
|
+
/** Emitted updates where the sensors were further apart than `disagreeM`. */
|
|
238
|
+
disagreements: number;
|
|
239
|
+
}
|
|
240
|
+
interface TrackFuser {
|
|
241
|
+
/** The event to store and show, or null when it adds nothing new. */
|
|
242
|
+
ingest(event: FusedTrackEvent$1): FusedTrackEvent$1 | null;
|
|
243
|
+
/** Forget a canonical track, e.g. when the live picture removed it. */
|
|
244
|
+
forget(trackId: string): void;
|
|
245
|
+
/** Drop sensors that went silent and tracks with none left. */
|
|
246
|
+
sweep(): void;
|
|
247
|
+
stats(): FusionStats;
|
|
248
|
+
}
|
|
249
|
+
/** Ids the ingest package builds from sensor + track number when there is no Mode-S address. */
|
|
250
|
+
declare const isSyntheticId: (icao24: string) => boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Options not given (or `undefined`) take their defaults, so a host passes only
|
|
253
|
+
* what it wants to change: `createTrackFuser({ spatial: true })`.
|
|
254
|
+
*/
|
|
255
|
+
declare function createTrackFuser(options?: Partial<FusionOptions>): TrackFuser;
|
|
256
|
+
|
|
192
257
|
/** Result of `authenticate`. `airportId` pins the connection to that airport
|
|
193
258
|
* (a sensor token); leave it undefined for viewer tokens that may request any. */
|
|
194
259
|
interface LiveAuth {
|
|
@@ -233,4 +298,4 @@ interface LiveServer {
|
|
|
233
298
|
}
|
|
234
299
|
declare function createLiveServer(opts: LiveServerOptions): LiveServer;
|
|
235
300
|
|
|
236
|
-
export { type Bbox, type FlightPlanFilter, FlightPlanStore, type FlightPlanStoreOptions, type LiveAuth, type LiveMessage, type LiveOpenContext, type LiveServer, type LiveServerOptions, type LiveServerStats, type LiveSocket, type Logger, MovementDetector, type MovementEvent, type MovementSample, type MovementType, type RemoveReason, type RemovedListener, type TrackFilter, type TrackListener, TrackState, type TrackStateOptions, type UpdateInfo, createFlightPlanStore, createLiveServer, createMovementDetector, createTrackState };
|
|
301
|
+
export { type Bbox, DEFAULT_AIRBORNE_PRIORITY, DEFAULT_SURFACE_PRIORITY, type FlightPlanFilter, FlightPlanStore, type FlightPlanStoreOptions, type FusionOptions, type FusionStats, type LiveAuth, type LiveMessage, type LiveOpenContext, type LiveServer, type LiveServerOptions, type LiveServerStats, type LiveSocket, type Logger, MovementDetector, type MovementEvent, type MovementSample, type MovementType, type RemoveReason, type RemovedListener, type TrackFilter, type TrackFuser, type TrackListener, TrackState, type TrackStateOptions, type UpdateInfo, createFlightPlanStore, createLiveServer, createMovementDetector, createTrackFuser, createTrackState, defaultFusionOptions, isSyntheticId };
|
package/dist/index.js
CHANGED
|
@@ -567,6 +567,245 @@ function createMovementDetector() {
|
|
|
567
567
|
return new MovementDetector();
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
// src/fusion.ts
|
|
571
|
+
var DEFAULT_SURFACE_PRIORITY = [
|
|
572
|
+
"asterix-cat010",
|
|
573
|
+
"asterix-cat011",
|
|
574
|
+
"asterix-cat020",
|
|
575
|
+
"asterix-cat021",
|
|
576
|
+
"asterix-cat062",
|
|
577
|
+
"asterix-cat048"
|
|
578
|
+
];
|
|
579
|
+
var DEFAULT_AIRBORNE_PRIORITY = [
|
|
580
|
+
"asterix-cat020",
|
|
581
|
+
"asterix-cat010",
|
|
582
|
+
"asterix-cat011",
|
|
583
|
+
"asterix-cat021",
|
|
584
|
+
"asterix-cat062",
|
|
585
|
+
"asterix-cat048"
|
|
586
|
+
];
|
|
587
|
+
function defaultFusionOptions() {
|
|
588
|
+
return {
|
|
589
|
+
surfacePriority: [...DEFAULT_SURFACE_PRIORITY],
|
|
590
|
+
airbornePriority: [...DEFAULT_AIRBORNE_PRIORITY],
|
|
591
|
+
maxSourceAgeMs: 5e3,
|
|
592
|
+
forgetAfterMs: 6e5,
|
|
593
|
+
spatial: false,
|
|
594
|
+
matchRadiusM: 150,
|
|
595
|
+
matchMaxSkewMs: 3e3,
|
|
596
|
+
disagreeM: 300
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
var SYNTHETIC_ID = /^[rtv]\d+-\d+-\d+$/;
|
|
600
|
+
var EARTH_M = 6371e3;
|
|
601
|
+
var isSyntheticId = (icao24) => SYNTHETIC_ID.test(icao24);
|
|
602
|
+
function distanceM(a, b) {
|
|
603
|
+
const rad = Math.PI / 180;
|
|
604
|
+
const dLat = (b.lat - a.lat) * rad;
|
|
605
|
+
const dLon = (b.lon - a.lon) * rad;
|
|
606
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * rad) * Math.cos(b.lat * rad) * Math.sin(dLon / 2) ** 2;
|
|
607
|
+
return 2 * EARTH_M * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
608
|
+
}
|
|
609
|
+
var sourceOf = (event) => event.sources[0];
|
|
610
|
+
var keyFor = (event) => {
|
|
611
|
+
const icao24 = event.identity.icao24;
|
|
612
|
+
return isSyntheticId(icao24) ? `s:${sourceOf(event)}:${icao24}` : `m:${icao24}`;
|
|
613
|
+
};
|
|
614
|
+
function rank(list, event) {
|
|
615
|
+
const idx = list.findIndex(
|
|
616
|
+
(s) => s === event.protocol || s === sourceOf(event)
|
|
617
|
+
);
|
|
618
|
+
return idx < 0 ? list.length : idx;
|
|
619
|
+
}
|
|
620
|
+
function pickPrimary(fresh, list) {
|
|
621
|
+
const sorted = [...fresh].sort(
|
|
622
|
+
(a, b) => rank(list, a.event) - rank(list, b.event) || b.at - a.at
|
|
623
|
+
);
|
|
624
|
+
return sorted[0] ?? null;
|
|
625
|
+
}
|
|
626
|
+
function mergeIdentity(primary, others) {
|
|
627
|
+
const out = { ...primary.event.identity };
|
|
628
|
+
for (const other of others) {
|
|
629
|
+
const id = other.event.identity;
|
|
630
|
+
out.callsign ??= id.callsign;
|
|
631
|
+
out.squawk ??= id.squawk;
|
|
632
|
+
out.targetType ??= id.targetType;
|
|
633
|
+
out.vehicleFleet ??= id.vehicleFleet;
|
|
634
|
+
if (isSyntheticId(out.icao24) && !isSyntheticId(id.icao24)) {
|
|
635
|
+
out.icao24 = id.icao24;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
function maxSpreadM(primary, others) {
|
|
641
|
+
const base = primary.event.position;
|
|
642
|
+
if (!base) return 0;
|
|
643
|
+
let max = 0;
|
|
644
|
+
for (const other of others) {
|
|
645
|
+
const pos = other.event.position;
|
|
646
|
+
if (pos) max = Math.max(max, distanceM(base, pos));
|
|
647
|
+
}
|
|
648
|
+
return max;
|
|
649
|
+
}
|
|
650
|
+
var Fuser = class {
|
|
651
|
+
opts;
|
|
652
|
+
now;
|
|
653
|
+
byKey = /* @__PURE__ */ new Map();
|
|
654
|
+
byId = /* @__PURE__ */ new Map();
|
|
655
|
+
spatialMatches = 0;
|
|
656
|
+
ambiguous = 0;
|
|
657
|
+
disagreements = 0;
|
|
658
|
+
constructor(opts) {
|
|
659
|
+
this.opts = opts;
|
|
660
|
+
this.now = opts.now ?? (() => Date.now());
|
|
661
|
+
}
|
|
662
|
+
ingest(event) {
|
|
663
|
+
const now = this.now();
|
|
664
|
+
const src = sourceOf(event);
|
|
665
|
+
const key = keyFor(event);
|
|
666
|
+
const known = this.byKey.get(key);
|
|
667
|
+
if (event.lifecycle === "dropped") return this.drop(known, event, src, now);
|
|
668
|
+
const entry = known ?? this.matchSpatially(event, src, now) ?? this.create(event);
|
|
669
|
+
this.link(entry, key, event);
|
|
670
|
+
entry.contributions.set(src, { event, sourceId: src, at: now });
|
|
671
|
+
return this.output(entry, src, now, false);
|
|
672
|
+
}
|
|
673
|
+
forget(trackId) {
|
|
674
|
+
const entry = this.byId.get(trackId);
|
|
675
|
+
if (entry) this.remove(entry);
|
|
676
|
+
}
|
|
677
|
+
sweep() {
|
|
678
|
+
const now = this.now();
|
|
679
|
+
for (const entry of [...this.byId.values()]) {
|
|
680
|
+
for (const [src, c] of entry.contributions) {
|
|
681
|
+
if (now - c.at > this.opts.forgetAfterMs)
|
|
682
|
+
entry.contributions.delete(src);
|
|
683
|
+
}
|
|
684
|
+
if (entry.contributions.size === 0) this.remove(entry);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
stats() {
|
|
688
|
+
const now = this.now();
|
|
689
|
+
let merged = 0;
|
|
690
|
+
for (const entry of this.byId.values()) {
|
|
691
|
+
if (this.fresh(entry, now).length > 1) merged++;
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
aircraft: this.byId.size,
|
|
695
|
+
merged,
|
|
696
|
+
spatialMatches: this.spatialMatches,
|
|
697
|
+
ambiguous: this.ambiguous,
|
|
698
|
+
disagreements: this.disagreements
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
fresh(entry, now) {
|
|
702
|
+
return [...entry.contributions.values()].filter(
|
|
703
|
+
(c) => now - c.at <= this.opts.maxSourceAgeMs
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
create(event) {
|
|
707
|
+
const entry = {
|
|
708
|
+
id: event.trackId,
|
|
709
|
+
keys: /* @__PURE__ */ new Set(),
|
|
710
|
+
contributions: /* @__PURE__ */ new Map(),
|
|
711
|
+
realIcao24: null,
|
|
712
|
+
lastPrimary: "",
|
|
713
|
+
lastSources: ""
|
|
714
|
+
};
|
|
715
|
+
this.byId.set(entry.id, entry);
|
|
716
|
+
return entry;
|
|
717
|
+
}
|
|
718
|
+
link(entry, key, event) {
|
|
719
|
+
entry.keys.add(key);
|
|
720
|
+
this.byKey.set(key, entry);
|
|
721
|
+
if (!isSyntheticId(event.identity.icao24)) {
|
|
722
|
+
entry.realIcao24 = event.identity.icao24;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
remove(entry) {
|
|
726
|
+
for (const key of entry.keys) this.byKey.delete(key);
|
|
727
|
+
this.byId.delete(entry.id);
|
|
728
|
+
}
|
|
729
|
+
drop(entry, event, src, now) {
|
|
730
|
+
if (!entry) return event;
|
|
731
|
+
entry.contributions.delete(src);
|
|
732
|
+
if (entry.contributions.size > 0) return this.output(entry, src, now, true);
|
|
733
|
+
this.remove(entry);
|
|
734
|
+
return { ...event, trackId: entry.id };
|
|
735
|
+
}
|
|
736
|
+
/** The partner for a track nobody else shares an id with, or null. */
|
|
737
|
+
matchSpatially(event, src, now) {
|
|
738
|
+
const pos = event.position;
|
|
739
|
+
if (!this.opts.spatial || !pos) return null;
|
|
740
|
+
const hasMode = !isSyntheticId(event.identity.icao24);
|
|
741
|
+
const found = [];
|
|
742
|
+
for (const entry of this.byId.values()) {
|
|
743
|
+
if (entry.contributions.has(src) || hasMode && entry.realIcao24)
|
|
744
|
+
continue;
|
|
745
|
+
if (this.isNear(entry, pos, now)) found.push(entry);
|
|
746
|
+
}
|
|
747
|
+
if (found.length > 1) this.ambiguous++;
|
|
748
|
+
if (found.length !== 1) return null;
|
|
749
|
+
this.spatialMatches++;
|
|
750
|
+
return found[0] ?? null;
|
|
751
|
+
}
|
|
752
|
+
isNear(entry, pos, now) {
|
|
753
|
+
for (const c of entry.contributions.values()) {
|
|
754
|
+
const other = c.event.position;
|
|
755
|
+
if (!other || now - c.at > this.opts.matchMaxSkewMs) continue;
|
|
756
|
+
if (distanceM(pos, other) <= this.opts.matchRadiusM) return true;
|
|
757
|
+
}
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* The event for the aircraft as it stands now. Null when `src` is not the
|
|
762
|
+
* sensor being shown and nothing else changed, so a lower-priority sensor
|
|
763
|
+
* does not make the picture jump.
|
|
764
|
+
*/
|
|
765
|
+
output(entry, src, now, force) {
|
|
766
|
+
const fresh = this.fresh(entry, now);
|
|
767
|
+
const onGround = fresh.some((c) => c.event.position?.onGround === true);
|
|
768
|
+
const list = onGround ? this.opts.surfacePriority : this.opts.airbornePriority;
|
|
769
|
+
const primary = pickPrimary(fresh, list);
|
|
770
|
+
if (!primary) return null;
|
|
771
|
+
const sources = fresh.map((c) => c.sourceId).sort().join(",");
|
|
772
|
+
const changed = force || primary.sourceId === src || primary.sourceId !== entry.lastPrimary || sources !== entry.lastSources;
|
|
773
|
+
entry.lastPrimary = primary.sourceId;
|
|
774
|
+
entry.lastSources = sources;
|
|
775
|
+
return changed ? this.build(entry, primary, fresh) : null;
|
|
776
|
+
}
|
|
777
|
+
build(entry, primary, fresh) {
|
|
778
|
+
const others = fresh.filter((c) => c !== primary).sort((a, b) => b.at - a.at);
|
|
779
|
+
const { event } = primary;
|
|
780
|
+
const out = {
|
|
781
|
+
...event,
|
|
782
|
+
trackId: entry.id,
|
|
783
|
+
sources: [primary.sourceId, ...others.map((c) => c.sourceId)],
|
|
784
|
+
identity: mergeIdentity(primary, others),
|
|
785
|
+
lifecycle: others.length > 0 ? "merged" : event.lifecycle
|
|
786
|
+
};
|
|
787
|
+
const velocity = event.velocity ?? others.find((c) => c.event.velocity)?.event.velocity;
|
|
788
|
+
if (velocity) out.velocity = velocity;
|
|
789
|
+
if (others.length > 0) this.scoreAgreement(out, primary, others);
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
scoreAgreement(out, primary, others) {
|
|
793
|
+
const spread = maxSpreadM(primary, others);
|
|
794
|
+
if (spread > this.opts.disagreeM) this.disagreements++;
|
|
795
|
+
out.confidence = {
|
|
796
|
+
...out.confidence,
|
|
797
|
+
sourceAgreement: Math.max(0, 1 - spread / this.opts.disagreeM)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
function createTrackFuser(options = {}) {
|
|
802
|
+
const base = defaultFusionOptions();
|
|
803
|
+
const given = Object.fromEntries(
|
|
804
|
+
Object.entries(options).filter(([, v]) => v !== void 0)
|
|
805
|
+
);
|
|
806
|
+
return new Fuser({ ...base, ...given });
|
|
807
|
+
}
|
|
808
|
+
|
|
570
809
|
// ../core/dist/chunk-ZYX6U3ZK.js
|
|
571
810
|
import { z } from "zod";
|
|
572
811
|
var positionSchema = z.object({
|
|
@@ -919,11 +1158,16 @@ function createLiveServer(opts) {
|
|
|
919
1158
|
};
|
|
920
1159
|
}
|
|
921
1160
|
export {
|
|
1161
|
+
DEFAULT_AIRBORNE_PRIORITY,
|
|
1162
|
+
DEFAULT_SURFACE_PRIORITY,
|
|
922
1163
|
FlightPlanStore,
|
|
923
1164
|
MovementDetector,
|
|
924
1165
|
TrackState,
|
|
925
1166
|
createFlightPlanStore,
|
|
926
1167
|
createLiveServer,
|
|
927
1168
|
createMovementDetector,
|
|
928
|
-
|
|
1169
|
+
createTrackFuser,
|
|
1170
|
+
createTrackState,
|
|
1171
|
+
defaultFusionOptions,
|
|
1172
|
+
isSyntheticId
|
|
929
1173
|
};
|