mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
feat(vessels): key vessels by their source's stable id, and share a selected vessel
AIS Vessels are keyed by the id their live source gives, with the MMSI as an attribute: an MMSI can be shared, spoofed or reused, so two vessels with one MMSI are now two vessels. Looking a vessel up or selecting it by MMSI still finds it. A share link made while a vessel is selected opens that vessel. A UUID vessel id travels as 16 bytes in a new thing field (13). A shared thing that is not reporting now says so in a sentence per kind, with its id (ICAO, NORAD, MMSI or vessel UUID) on a second line.
This commit is contained in:
1 parent
57ff81f723
commit
cbbf55cacd
74 files changed
+2481
-586
No files matched your search
@@ -39,13 +39,15 @@ export function createSelection({
|
||||
if (!state.feed.enabled) return;
|
||||
// The bridge view owns the selection: a click on open water must not
|
||||
// deselect the vessel the camera is riding.
|
||||
if (state.cockpitSubjectMmsi) return;
|
||||
if (state.cockpitSubjectId) return;
|
||||
const picked = pickContact
|
||||
? pickContact(viewer.scene, click.position)
|
||||
: viewer.scene.pick(click.position);
|
||||
const pickedId = resolvePickId(picked);
|
||||
let record = pickedId ? state.records.byMmsi.get(pickedId) : null;
|
||||
let record = pickedId ? state.records.byId.get(pickedId) : null;
|
||||
const rawId = picked?.id ?? picked?.primitive?.id;
|
||||
// Every vessel record carries an `mmsi` field ('' when unknown); its
|
||||
// key is `id`.
|
||||
const ownRecordPick =
|
||||
rawId && typeof rawId === 'object' && Object.hasOwn(rawId, 'mmsi');
|
||||
|
||||
@@ -85,10 +87,10 @@ export function createSelection({
|
||||
if (cycloneBackground && cardHit?.sourceId !== VESSEL_OVERLAY_SOURCE_ID)
|
||||
return;
|
||||
if (!record && cardHit) {
|
||||
const mmsi = String(cardHit.entryId || '').startsWith('vessel:')
|
||||
const id = String(cardHit.entryId || '').startsWith('vessel:')
|
||||
? cardHit.entryId.slice('vessel:'.length)
|
||||
: null;
|
||||
record = mmsi ? state.records.byMmsi.get(mmsi) || null : null;
|
||||
record = id ? state.records.byId.get(id) || null : null;
|
||||
// A stale card id is not empty terrain and must not clear a newer
|
||||
// selection. The next paint will evict its hit rectangle.
|
||||
if (!record) return;
|
||||
@@ -100,8 +102,8 @@ export function createSelection({
|
||||
selectAndFocusVessel(record);
|
||||
} else {
|
||||
const transition = components.queries.reduceVesselSelection({
|
||||
selectedMmsi: state.selectedRecord?.mmsi,
|
||||
pickedMmsi: null,
|
||||
selectedId: state.selectedRecord?.id,
|
||||
pickedId: null,
|
||||
gesture: 'click',
|
||||
});
|
||||
if (transition.action === 'deselect') clearVesselInspection();
|
||||
@@ -123,17 +125,17 @@ export function createSelection({
|
||||
/** Select one live vessel and request one UI-owned camera transfer. */
|
||||
|
||||
function selectAndFocusVessel(record) {
|
||||
if (!record?.mmsi) return false;
|
||||
if (!record?.id) return false;
|
||||
const transition = components.queries.reduceVesselSelection({
|
||||
selectedMmsi: state.selectedRecord?.mmsi,
|
||||
pickedMmsi: record.mmsi,
|
||||
selectedId: state.selectedRecord?.id,
|
||||
pickedId: record.id,
|
||||
gesture: 'click',
|
||||
});
|
||||
if (transition.action === 'select') selectVessel(record);
|
||||
requestWorldFocus({
|
||||
kind: 'vessel',
|
||||
id: record.mmsi,
|
||||
label: record.name || record.mmsi,
|
||||
id: record.id,
|
||||
label: record.name || record.mmsi || record.id,
|
||||
position:
|
||||
components.rendering.getVisual(record).billboard?.position ||
|
||||
components.rendering.getVisual(record).position,
|
||||
@@ -160,7 +162,7 @@ export function createSelection({
|
||||
function onVesselKeyDown(event) {
|
||||
if (!state.feed.enabled || event.key !== 'Escape') return;
|
||||
const transition = components.queries.reduceVesselSelection({
|
||||
selectedMmsi: state.selectedRecord?.mmsi,
|
||||
selectedId: state.selectedRecord?.id,
|
||||
gesture: 'escape',
|
||||
});
|
||||
if (transition.action === 'deselect') {
|
||||
@@ -169,8 +171,8 @@ export function createSelection({
|
||||
}
|
||||
|
||||
function selectVessel(record) {
|
||||
if (!record?.mmsi) return;
|
||||
const reuseTrail = state.trailMmsi === record.mmsi;
|
||||
if (!record?.id) return;
|
||||
const reuseTrail = state.trailId === record.id;
|
||||
clearSelection({ preserveTrail: reuseTrail });
|
||||
state.selectedRecord = record;
|
||||
record.missedRefreshes = 0;
|
||||
@@ -188,7 +190,7 @@ export function createSelection({
|
||||
selectEntityContext(record);
|
||||
}
|
||||
// Track-history trail (PRD F3/F4): seed with the current position + async
|
||||
// backfill from the server-side per-MMSI ring buffer.
|
||||
// backfill from the source's recent track for this vessel.
|
||||
if (reuseTrail) {
|
||||
components.tracking.appendSelectedVesselTrailFix(record);
|
||||
} else {
|
||||
@@ -204,10 +206,10 @@ export function createSelection({
|
||||
*/
|
||||
|
||||
function registerSelectedContext(record) {
|
||||
if (!record?.mmsi) return null;
|
||||
if (!record?.id) return null;
|
||||
try {
|
||||
return registerEntityContext(record, {
|
||||
id: `ais-${record.mmsi}`,
|
||||
id: `ais-${record.id}`,
|
||||
layerId: 'ais-live-vessels',
|
||||
layerName: 'Live AIS Vessels',
|
||||
source: aisLiveVesselsLayer.source,
|
||||
@@ -215,7 +217,8 @@ export function createSelection({
|
||||
latitude: record.lat,
|
||||
longitude: record.lon,
|
||||
properties: {
|
||||
mmsi: record.mmsi,
|
||||
vesselId: record.id,
|
||||
mmsi: record.mmsi || null,
|
||||
type: record.type,
|
||||
speedKt: record.speed,
|
||||
course: record.course,
|
||||
|
||||
Reference in new issue
Block a user