mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Let a host application place live contacts
createApplicationCatalog takes an optional placement provider. Its layer(kind) hands Flights, Military Flights, AIS Vessels, Transit and Rail a place(id, wallMs), and optionally observe(id, fix) and forget(id). While live, each layer tells it every fix it accepts and draws a contact where it places it: position, height (the layer's own when none is given), course and speed. A null answer, a provider that throws, a kind it leaves out, or no provider at all keeps the layer's own motion unchanged, and following the world clock the layers never ask it. A transit vehicle or train it holds still lets the render loop idle and is asked again each second; vessels ask for frames only while it places one, and stop when the layer turns off. Tests show the layers draw and reconcile exactly as before without a provider and with one that places nothing, draw where a provider places them, and idle while it holds them still.
This commit is contained in:
1 parent
e53cfef1d1
commit
6f830a2e70
46 files changed
+1417
-34
No files matched your search
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* A host-supplied placement provider: an application embedding the catalog
|
||||
* may decide where its moving contacts are drawn while live. Each layer that
|
||||
* takes one tells it every fix it accepts (`observe`) and asks it, each time
|
||||
* it draws a contact, where to draw it now (`place`). A layer given none, or
|
||||
* asking about a contact the provider does not place (`null`), draws the
|
||||
* contact with its own motion, exactly as without a provider. A provider
|
||||
* that throws is treated as having no answer for that call.
|
||||
*
|
||||
* Layers consult a provider only while live: following the world clock, the
|
||||
* layer's own motion places every contact.
|
||||
*
|
||||
* @typedef {'aircraft'|'military'|'vessels'|'transit'|'rail'} PlacementKind
|
||||
*
|
||||
* @typedef {object} PlacementFix
|
||||
* @property {number} timeMs The fix's own time, Unix ms.
|
||||
* @property {number} lat
|
||||
* @property {number} lon
|
||||
* @property {number} [altitudeM] The height the layer draws the fix at,
|
||||
* metres above the ellipsoid; NaN when the layer keeps its own height.
|
||||
* @property {number} [speedMps] Reported ground speed, NaN for none.
|
||||
* @property {number} [courseDeg] Reported course over ground, NaN for none.
|
||||
* @property {number} [headingDeg] Reported heading, NaN for none.
|
||||
* @property {boolean} [onGround] Reported on the ground (aircraft).
|
||||
* @property {string} [feedId] The feed the contact belongs to (transit, rail).
|
||||
*
|
||||
* @typedef {object} Placement
|
||||
* @property {number} lat
|
||||
* @property {number} lon
|
||||
* @property {number} altitudeM Metres above the ellipsoid, as the observed
|
||||
* fixes' `altitudeM`; NaN keeps the layer's own height.
|
||||
* @property {number} courseDeg Direction of travel, NaN for none.
|
||||
* @property {number} speedMps NaN for none.
|
||||
* @property {boolean} extrapolating Past the contact's newest fix.
|
||||
*
|
||||
* @typedef {object} LayerPlacement What a layer is handed: never throws.
|
||||
* @property {(id: string, fix: PlacementFix) => void} observe
|
||||
* @property {(id: string, wallMs: number) => Placement|null} place The
|
||||
* answer is one object reused by every call: read it before the next.
|
||||
* @property {(id: string) => void} forget The contact left the layer.
|
||||
*
|
||||
* @typedef {object} PlacementProvider
|
||||
* @property {(kind: PlacementKind) => object|null|undefined} layer The
|
||||
* placement for one kind of contact: an object with `place(id, wallMs)`
|
||||
* and optionally `observe(id, fix)` and `forget(id)`, or null to leave that
|
||||
* kind to its layer's own motion.
|
||||
*/
|
||||
|
||||
/** The kinds of contact a provider may place. */
|
||||
export const PLACEMENT_KINDS = Object.freeze([
|
||||
'aircraft',
|
||||
'military',
|
||||
'vessels',
|
||||
'transit',
|
||||
'rail',
|
||||
]);
|
||||
|
||||
const finiteOr = (value, fallback) =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||
|
||||
/**
|
||||
* One kind's placement from a provider, guarded so a layer can call it every
|
||||
* frame: null when there is no provider or it does not place `kind`.
|
||||
* @param {PlacementProvider|null|undefined} provider
|
||||
* @param {PlacementKind} kind
|
||||
* @returns {LayerPlacement|null}
|
||||
*/
|
||||
export function layerPlacement(provider, kind) {
|
||||
if (!PLACEMENT_KINDS.includes(kind))
|
||||
throw new TypeError(`Unknown placement kind: ${kind}`);
|
||||
if (provider == null) return null;
|
||||
if (typeof provider.layer !== 'function')
|
||||
throw new TypeError('A placement provider needs a layer(kind) function');
|
||||
const placement = provider.layer(kind);
|
||||
if (placement == null) return null;
|
||||
if (typeof placement.place !== 'function')
|
||||
throw new TypeError(`The ${kind} placement needs a place(id, wallMs)`);
|
||||
const observe =
|
||||
typeof placement.observe === 'function' ? placement.observe : null;
|
||||
const forget =
|
||||
typeof placement.forget === 'function' ? placement.forget : null;
|
||||
// Asked for every drawn contact every frame: one answer, reused.
|
||||
const answer = {
|
||||
lat: NaN,
|
||||
lon: NaN,
|
||||
altitudeM: NaN,
|
||||
courseDeg: NaN,
|
||||
speedMps: NaN,
|
||||
extrapolating: false,
|
||||
};
|
||||
return Object.freeze({
|
||||
observe(id, fix) {
|
||||
if (!observe) return;
|
||||
try {
|
||||
observe.call(placement, id, fix);
|
||||
} catch {
|
||||
// A provider that fails to take a fix only answers less.
|
||||
}
|
||||
},
|
||||
place(id, wallMs) {
|
||||
let placed;
|
||||
try {
|
||||
placed = placement.place(id, wallMs);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!placed ||
|
||||
!Number.isFinite(placed.lat) ||
|
||||
!Number.isFinite(placed.lon) ||
|
||||
Math.abs(placed.lat) > 90
|
||||
)
|
||||
return null;
|
||||
answer.lat = placed.lat;
|
||||
answer.lon = placed.lon;
|
||||
answer.altitudeM = finiteOr(placed.altitudeM, NaN);
|
||||
answer.courseDeg = finiteOr(placed.courseDeg, NaN);
|
||||
answer.speedMps = finiteOr(placed.speedMps, NaN);
|
||||
answer.extrapolating = placed.extrapolating === true;
|
||||
return answer;
|
||||
},
|
||||
forget(id) {
|
||||
if (!forget) return;
|
||||
try {
|
||||
forget.call(placement, id);
|
||||
} catch {
|
||||
// Nothing to undo.
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user