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:
Sameh Khamis committed 2026-11-10 18:12:53 -08:00
1 parent 57ff81f723
commit cbbf55cacd
74 files changed
+2481 -586

No files matched your search

+10 -10
View File
@@ -307,7 +307,7 @@ export function createRendering({
if (visual.billboard) {
// The bridge camera sits on this sprite; showing it would fill the view.
visual.billboard.show =
visible && record.mmsi !== state.cockpitSubjectMmsi;
visible && record.id !== state.cockpitSubjectId;
if (visible && doRotations && scene) {
const rot = screenProjectedRotation(
scene,
@@ -445,8 +445,7 @@ export function createRendering({
const selected = state.selectedRecord;
// From the bridge, the selected vessel is the viewer; its card would be
// pinned to the camera position.
const selectedCarded =
selected && selected.mmsi !== state.cockpitSubjectMmsi;
const selectedCarded = selected && selected.id !== state.cockpitSubjectId;
const entries = selectedCarded
? [components.cards.buildSelectedVesselCard(selected)]
: [];
@@ -532,17 +531,17 @@ export function createRendering({
VESSEL_CARD_FADE_DISTANCE_M,
);
if (!card.interactive) return card;
const mmsi = String(card.id || '').startsWith('vessel:')
const id = String(card.id || '').startsWith('vessel:')
? card.id.slice('vessel:'.length)
: '';
const mmsi = state.records.byId.get(id)?.mmsi || '';
return {
...card,
accessibilityLabel: t('Focus vessel {name}, MMSI {mmsi}', {
name: card.title,
mmsi,
}),
accessibilityLabel: mmsi
? t('Focus vessel {name}, MMSI {mmsi}', { name: card.title, mmsi })
: t('Focus {label}', { label: card.title }),
activate: () => {
const record = state.records.byMmsi.get(mmsi);
const record = state.records.byId.get(id);
if (!record) return false;
components.selection.selectAndFocusVessel(record);
return true;
@@ -574,7 +573,8 @@ export function createRendering({
text &&
text !== 'VESSEL' &&
!/^MMSI\s*\d+$/i.test(text) &&
text !== record.mmsi,
text !== record.mmsi &&
text !== record.id,
);
}