mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Draw no buoy for a virtual aid to navigation
A virtual aid (AIS message 21's VirtualAtoN flag) is a mark broadcast where nothing physical stands, so drawing it as a buoy showed a mark that is not there. The vessel source record now carries `virtual` (false unless a source says true; any source may set it), AISStream sets it from VirtualAtoN, and the vessel layer keeps such an aid's disc: vesselModelSpec returns null for it and the layer gathers no model item for it. Its card, selected card and readout add VIRTUAL after its kind.
This commit is contained in:
1 parent
9ae121c7c0
commit
52507a0755
31 files changed
+160
-18
No files matched your search
@@ -1916,7 +1916,23 @@ test('the HUD of a selected aid to navigation shows no speed or heading', () =>
|
||||
assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true);
|
||||
assert.match(harness.hud.readout.text, /\nPort hand Mark\n/);
|
||||
assert.doesNotMatch(harness.hud.readout.text, /SPD|HDG|KT/);
|
||||
harness.record.virtual = true;
|
||||
aisLiveVesselsLayer.selectById(harness.record.mmsi);
|
||||
assert.match(harness.hud.readout.text, /\nPort hand Mark · VIRTUAL\n/);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test('a virtual aid to navigation card says it is virtual', () => {
|
||||
const aid = makeRecord({
|
||||
mmsi: '993672001',
|
||||
type: 'Isolated danger',
|
||||
virtual: true,
|
||||
});
|
||||
assert.deepEqual(buildVesselCard(aid).details, ['ISOLATED DANGER · VIRTUAL']);
|
||||
assert.equal(
|
||||
buildSelectedVesselCard(aid).details[0],
|
||||
'ISOLATED DANGER · VIRTUAL',
|
||||
);
|
||||
});
|
||||
@@ -388,3 +388,25 @@ test('a capped row request keeps every vessel ahead of aids to navigation', () =
|
||||
aisStreamRows(vesselCount).every((row) => !row.mmsi.startsWith('99')),
|
||||
);
|
||||
});
|
||||
|
||||
test('a virtual aid to navigation is marked virtual; a real one carries no flag', () => {
|
||||
const aidReport = (mmsi, VirtualAtoN) => ({
|
||||
MessageType: 'AidsToNavigationReport',
|
||||
Message: {
|
||||
AidsToNavigationReport: { UserID: Number(mmsi), Type: 28, VirtualAtoN },
|
||||
},
|
||||
MetaData: {
|
||||
MMSI: mmsi,
|
||||
latitude: 37.79,
|
||||
longitude: -122.52,
|
||||
time_utc: '2026-09-17 20:00:00.000000000 +0000 UTC',
|
||||
},
|
||||
});
|
||||
ingestAisStreamEnvelope(aidReport('993672004', true));
|
||||
ingestAisStreamEnvelope(aidReport('993672005', false));
|
||||
assert.equal(rowFor('993672004').virtual, true);
|
||||
assert.equal('virtual' in rowFor('993672005'), false);
|
||||
// A later real report of the same aid drops the flag.
|
||||
ingestAisStreamEnvelope(aidReport('993672004', false));
|
||||
assert.equal('virtual' in rowFor('993672004'), false);
|
||||
});
|
||||
+11
-4
@@ -443,13 +443,20 @@ export function aircraftModelSpec(
|
||||
* ({@link isAidToNavigation}: its type names a buoy, beacon, mark, RACON or
|
||||
* light, or its MMSI is an aid's) is the buoy of its mark
|
||||
* ({@link aidToNavigationModel}), else {@link DEFAULT_AID_MODEL}, drawn as
|
||||
* built and flagged `fixed`: it has no heading to turn to.
|
||||
* built and flagged `fixed`: it has no heading to turn to. A virtual aid
|
||||
* (`virtual`) is broadcast where nothing physical stands, so it has no model.
|
||||
* @param {{catalog: object, types: object}} set
|
||||
* @param {{type?: string|number|null, mmsi?: string|number|null,
|
||||
* length?: number|null, beam?: number|null, name?: string|null}} vessel
|
||||
* @returns {object} {@link fleetModelSpec}, with `fixed` for an aid
|
||||
* virtual?: boolean, length?: number|null, beam?: number|null,
|
||||
* name?: string|null}} vessel
|
||||
* @returns {object|null} {@link fleetModelSpec}, with `fixed` for an aid;
|
||||
* null for a virtual aid
|
||||
*/
|
||||
export function vesselModelSpec(set, { type, mmsi, length, beam, name } = {}) {
|
||||
export function vesselModelSpec(
|
||||
set,
|
||||
{ type, mmsi, virtual, length, beam, name } = {},
|
||||
) {
|
||||
if (virtual === true) return null;
|
||||
if (isAidToNavigation({ type, mmsi })) {
|
||||
const model = aidToNavigationModel(type);
|
||||
const known = model && set.catalog[model] ? model : DEFAULT_AID_MODEL;
|
||||
|
||||
@@ -423,6 +423,15 @@ test('aids to navigation are the buoy of the mark their type names', () => {
|
||||
assert.equal(unnamed.model, DEFAULT_AID_MODEL);
|
||||
assert.equal(unnamed.fixed, true);
|
||||
assert.equal(aidToNavigationModel('Fixed structure off shore'), undefined);
|
||||
// A virtual aid is broadcast where nothing stands: it keeps its disc.
|
||||
assert.equal(
|
||||
vesselModelSpec(set, {
|
||||
type: 'Cardinal Mark N',
|
||||
mmsi: '993672001',
|
||||
virtual: true,
|
||||
}),
|
||||
null,
|
||||
);
|
||||
// Vessels, including those that work on buoys, keep their hulls.
|
||||
for (const vessel of [
|
||||
{ type: 'Buoy Tender', mmsi: '366999001', length: 60 },
|
||||
|
||||
@@ -140,11 +140,13 @@ function typeNamesAid(text) {
|
||||
* Whether a vessel record is an aid to navigation (a buoy, beacon, mark,
|
||||
* RACON or light) rather than a vessel: its type is one of the kinds an aid
|
||||
* report names or uses a word for one, or its MMSI is an aid-to-navigation
|
||||
* station's. A numeric AIS ship type never names one.
|
||||
* @param {{type?: string|number|null, mmsi?: string|number|null}} vessel
|
||||
* station's, or it is a virtual aid. A numeric AIS ship type never names one.
|
||||
* @param {{type?: string|number|null, mmsi?: string|number|null,
|
||||
* virtual?: boolean}} vessel
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAidToNavigation({ type, mmsi } = {}) {
|
||||
export function isAidToNavigation({ type, mmsi, virtual } = {}) {
|
||||
if (virtual === true) return true;
|
||||
const text = typeof type === 'string' ? type.trim() : String(type ?? '');
|
||||
if (text && typeNamesAid(text)) return true;
|
||||
if (mmsi === null || mmsi === undefined || mmsi === '') return false;
|
||||
|
||||
@@ -36,6 +36,11 @@ test('aids to navigation are known by their type, else their MMSI', () => {
|
||||
'Emergency wreck marking buoy',
|
||||
])
|
||||
assert.equal(isAidToNavigation({ type, mmsi: '' }), true, type);
|
||||
// A virtual aid is an aid whatever its type or MMSI.
|
||||
assert.equal(
|
||||
isAidToNavigation({ type: '', mmsi: '', virtual: true }),
|
||||
true,
|
||||
);
|
||||
// AIS aid-to-navigation stations are numbered 99MIDxxxx.
|
||||
assert.equal(isAidToNavigation({ type: '', mmsi: '993672001' }), true);
|
||||
assert.equal(isAidToNavigation({ type: '0', mmsi: ' 992351234 ' }), true);
|
||||
|
||||
@@ -777,6 +777,7 @@
|
||||
"OTHER": "أخرى",
|
||||
"VESSEL": "سفينة",
|
||||
"AID TO NAVIGATION": "مساعدة ملاحية",
|
||||
"VIRTUAL": "افتراضي",
|
||||
"POS: LIVE": "الموضع: مباشر",
|
||||
"POS: {time}Z": "الموضع: {time}Z",
|
||||
"AIS: --": "AIS: --",
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "SCHIFF",
|
||||
"AID TO NAVIGATION": "SEEZEICHEN",
|
||||
"VIRTUAL": "VIRTUELL",
|
||||
"POS: LIVE": "POS: LIVE",
|
||||
"POS: {time}Z": "POS: {time}Z",
|
||||
"awaiting first AIS position…": "warten auf erste AIS-Position…",
|
||||
|
||||
@@ -651,6 +651,7 @@
|
||||
"OTHER": "OTRO",
|
||||
"VESSEL": "BUQUE",
|
||||
"AID TO NAVIGATION": "AYUDA A LA NAVEGACIÓN",
|
||||
"VIRTUAL": "VIRTUAL",
|
||||
"POS: LIVE": "POS: EN VIVO",
|
||||
"POS: {time}Z": "POS: {time}Z",
|
||||
"AIS: --": "AIS: --",
|
||||
|
||||
@@ -1292,6 +1292,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD : {speed} HDG : {heading}",
|
||||
"VESSEL": "NAVIRE",
|
||||
"AID TO NAVIGATION": "AIDE À LA NAVIGATION",
|
||||
"VIRTUAL": "VIRTUEL",
|
||||
"{speed}KT": "{speed} ND",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS : DIRECT",
|
||||
|
||||
@@ -1150,6 +1150,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "אונייה",
|
||||
"AID TO NAVIGATION": "עזר ניווט",
|
||||
"VIRTUAL": "וירטואלי",
|
||||
"{speed}KT": "{speed} קשר",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS: חי",
|
||||
|
||||
@@ -1292,6 +1292,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} VEL: {speed} HDG: {heading}",
|
||||
"VESSEL": "NAVE",
|
||||
"AID TO NAVIGATION": "AIUTO ALLA NAVIGAZIONE",
|
||||
"VIRTUAL": "VIRTUALE",
|
||||
"{speed}KT": "{speed}KT",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS: LIVE",
|
||||
|
||||
@@ -1254,6 +1254,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "船舶",
|
||||
"AID TO NAVIGATION": "航路標識",
|
||||
"VIRTUAL": "仮想",
|
||||
"{speed}KT": "{speed} kt",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS: ライブ",
|
||||
|
||||
@@ -1290,6 +1290,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "EMBARCAÇÃO",
|
||||
"AID TO NAVIGATION": "AUXÍLIO À NAVEGAÇÃO",
|
||||
"VIRTUAL": "VIRTUAL",
|
||||
"{speed}KT": "{speed} KT",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS: AO VIVO",
|
||||
|
||||
@@ -395,6 +395,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} СКОР: {speed} КУРС: {heading}",
|
||||
"VESSEL": "СУДНО",
|
||||
"AID TO NAVIGATION": "СРЕДСТВО НАВИГАЦИИ",
|
||||
"VIRTUAL": "ВИРТУАЛЬНЫЙ",
|
||||
"AIS: --": "AIS: --",
|
||||
"awaiting first AIS position…": "ожидание первой позиции AIS…",
|
||||
"awaiting usable AIS positions…": "ожидание пригодных позиций AIS…",
|
||||
|
||||
@@ -383,6 +383,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "FARTYG",
|
||||
"AID TO NAVIGATION": "SJÖMÄRKE",
|
||||
"VIRTUAL": "VIRTUELL",
|
||||
"POS: LIVE": "POS: LIVE",
|
||||
"POS: {time}Z": "POS: {time}Z",
|
||||
"awaiting first AIS position…": "väntar på första AIS-positionen …",
|
||||
|
||||
@@ -1310,6 +1310,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} ШВИД: {speed} КУРС: {heading}",
|
||||
"VESSEL": "СУДНО",
|
||||
"AID TO NAVIGATION": "ЗАСІБ НАВІГАЦІЇ",
|
||||
"VIRTUAL": "ВІРТУАЛЬНИЙ",
|
||||
"{speed}KT": "{speed} ВУЗ",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "ПОЗ.: ОНЛАЙН",
|
||||
|
||||
@@ -1230,6 +1230,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "船舶",
|
||||
"AID TO NAVIGATION": "助航标志",
|
||||
"VIRTUAL": "虚拟",
|
||||
"{speed}KT": "{speed} kn",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS:实时",
|
||||
|
||||
@@ -1225,6 +1225,7 @@
|
||||
"{type} SPD: {speed} HDG: {heading}": "{type} SPD: {speed} HDG: {heading}",
|
||||
"VESSEL": "船舶",
|
||||
"AID TO NAVIGATION": "助航標誌",
|
||||
"VIRTUAL": "虛擬",
|
||||
"{speed}KT": "{speed} kn",
|
||||
"{heading}DEG": "{heading}°",
|
||||
"POS: LIVE": "POS:實時",
|
||||
|
||||
@@ -411,7 +411,8 @@
|
||||
"MMSI: {id}",
|
||||
"NORAD: {id}",
|
||||
"Regional",
|
||||
"{datasets} · {tiles} · {packs}"
|
||||
"{datasets} · {tiles} · {packs}",
|
||||
"VIRTUAL"
|
||||
],
|
||||
"fr": [
|
||||
"1 min/s",
|
||||
@@ -1067,7 +1068,8 @@
|
||||
"ICAO: {id}",
|
||||
"MMSI: {id}",
|
||||
"NORAD: {id}",
|
||||
"Regional"
|
||||
"Regional",
|
||||
"VIRTUAL"
|
||||
],
|
||||
"ru": [
|
||||
"1900–1945",
|
||||
|
||||
@@ -24,7 +24,10 @@ export function createCards({
|
||||
text: [
|
||||
t('AIS: {name}', { name: trimHudValue(record.name, 32) }),
|
||||
aid
|
||||
? trimHudValue(record.type || t('AID TO NAVIGATION'), 24)
|
||||
? aidKind(
|
||||
trimHudValue(record.type || t('AID TO NAVIGATION'), 24),
|
||||
record,
|
||||
)
|
||||
: t('{type} SPD: {speed} HDG: {heading}', {
|
||||
type: trimHudValue(record.type || t('VESSEL'), 24),
|
||||
speed: formatSpeed(record.speed),
|
||||
@@ -60,6 +63,7 @@ export function createCards({
|
||||
const aid = isAidToNavigation(record);
|
||||
const type = vesselTypeShort(record);
|
||||
if (type) parts.push(type);
|
||||
if (record.virtual === true) parts.push(t('VIRTUAL'));
|
||||
if (!aid && record.speed !== null && record.speed !== undefined)
|
||||
parts.push(formatSpeed(record.speed));
|
||||
const direction = record.heading ?? record.course;
|
||||
@@ -95,7 +99,7 @@ export function createCards({
|
||||
const direction = record.heading ?? record.course;
|
||||
const details = [
|
||||
aid
|
||||
? vesselTypeShort(record) || t('AID TO NAVIGATION')
|
||||
? aidKind(vesselTypeShort(record) || t('AID TO NAVIGATION'), record)
|
||||
: [
|
||||
vesselTypeShort(record) || t('VESSEL'),
|
||||
formatSpeed(record.speed),
|
||||
@@ -125,6 +129,12 @@ export function createCards({
|
||||
};
|
||||
}
|
||||
|
||||
/** An aid's kind, marked virtual when nothing physical stands there. */
|
||||
|
||||
function aidKind(kind, record) {
|
||||
return record.virtual === true ? `${kind} · ${t('VIRTUAL')}` : kind;
|
||||
}
|
||||
|
||||
/** A card's accent: an aid's disc colour, else its ship type's. */
|
||||
|
||||
function vesselAccent(record, aid) {
|
||||
|
||||
@@ -218,6 +218,7 @@ export function createIngestion({
|
||||
speed: record.speedMps == null ? null : record.speedMps / 0.514444,
|
||||
course: record.courseDeg,
|
||||
heading: record.headingDeg,
|
||||
virtual: record.virtual === true,
|
||||
last_position_epoch:
|
||||
record.observedAtMs == null ? null : record.observedAtMs / 1000,
|
||||
last_position_UTC:
|
||||
|
||||
@@ -536,6 +536,29 @@ test('an aid to navigation is drawn as a disc, a vessel as its chevron', async (
|
||||
return atob(primitive.image.split(',')[1]);
|
||||
};
|
||||
assert.match(svgOf('993672001'), /<circle/);
|
||||
// The source's virtual flag reaches the layer's record.
|
||||
const recordOf = (id) => [...primitives].find((p) => p.id?.id === id).id;
|
||||
assert.equal(recordOf('993672001').virtual, false);
|
||||
assert.equal(recordOf('366999002').virtual, false);
|
||||
assert.doesNotMatch(svgOf('993672001'), /<path/);
|
||||
assert.match(svgOf('366999002'), /<path/);
|
||||
});
|
||||
|
||||
test('a virtual aid to navigation keeps its disc and its virtual flag', async () => {
|
||||
const aid = {
|
||||
...observation('993672009'),
|
||||
mmsi: '993672009',
|
||||
type: 'Isolated danger',
|
||||
virtual: true,
|
||||
};
|
||||
const { layer, primitives } = setup({
|
||||
async getSnapshot() {
|
||||
return snapshot([aid]);
|
||||
},
|
||||
});
|
||||
await layer.update();
|
||||
const primitive = [...primitives].find((p) => p.id?.id === '993672009');
|
||||
assert.ok(primitive);
|
||||
assert.equal(primitive.id.virtual, true);
|
||||
assert.match(atob(primitive.image.split(',')[1]), /<circle/);
|
||||
});
|
||||
@@ -31,6 +31,8 @@ export function normalizeVessel(row) {
|
||||
speed: finiteNumber(row.speed),
|
||||
course: finiteNumber(row.course),
|
||||
heading: finiteNumber(row.heading),
|
||||
// A virtual aid to navigation: a mark with nothing physical there.
|
||||
virtual: row.virtual === true,
|
||||
lastPositionUtc: String(row.last_position_UTC || ''),
|
||||
lastPositionEpoch: finiteNumber(row.last_position_epoch),
|
||||
missedRefreshes: 0,
|
||||
@@ -160,6 +162,7 @@ export class VesselRecords {
|
||||
record.speed = next.speed;
|
||||
record.course = next.course;
|
||||
record.heading = next.heading;
|
||||
record.virtual = next.virtual;
|
||||
record.lastPositionUtc = next.lastPositionUtc;
|
||||
record.lastPositionEpoch = next.lastPositionEpoch;
|
||||
record.missedRefreshes = 0;
|
||||
|
||||
@@ -274,8 +274,13 @@ export function createRendering({
|
||||
);
|
||||
}
|
||||
|
||||
/** The model item for `record`, its spec rebuilt only when its type or size changes. */
|
||||
/**
|
||||
* The model item for `record`, its spec rebuilt only when its type or size
|
||||
* changes; null for a virtual aid to navigation, which keeps its disc: no
|
||||
* buoy stands there (vesselModelSpec).
|
||||
*/
|
||||
function modelItem(record, set) {
|
||||
if (record.virtual === true) return null;
|
||||
let item = modelItems.get(record.id);
|
||||
if (!item || item.record !== record)
|
||||
item = { record, pickId: record, tint: null, modelOwnsVisual: false };
|
||||
@@ -309,7 +314,8 @@ export function createRendering({
|
||||
limitSq
|
||||
)
|
||||
continue;
|
||||
into.set(record.id, modelItem(record, set));
|
||||
const item = modelItem(record, set);
|
||||
if (item) into.set(record.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +351,10 @@ export function createRendering({
|
||||
gatherModelItems(set, next);
|
||||
} else {
|
||||
for (const [id, item] of modelItems)
|
||||
if (state.records.byId.get(id) === item.record)
|
||||
next.set(id, modelItem(item.record, set));
|
||||
if (state.records.byId.get(id) === item.record) {
|
||||
const kept = modelItem(item.record, set);
|
||||
if (kept) next.set(id, kept);
|
||||
}
|
||||
}
|
||||
for (const [id, item] of modelItems)
|
||||
if (next.get(id) !== item) handBackVisual(item.record, occluder);
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
* snapshots. A vessel record's `id` is whatever its source gives, such as a
|
||||
* provider id; its `mmsi` is an optional attribute ('' or absent when
|
||||
* unknown), never its key, since vessels can share an MMSI. A source that
|
||||
* only has MMSIs uses the MMSI as the id.
|
||||
* only has MMSIs uses the MMSI as the id. A vessel record's `virtual` is true
|
||||
* for a virtual aid to navigation, a mark broadcast where nothing physical
|
||||
* stands (AIS message 21's virtual flag), else false; any source may set it.
|
||||
*
|
||||
* A source may also declare a timeline (see readSnapshotTimeline): it can
|
||||
* answer a snapshot at a given time, so the layer follows the world clock.
|
||||
|
||||
@@ -221,6 +221,18 @@ test('vessel identity, opaque reference and heading/course remain distinct, unkn
|
||||
assert.equal(row.altitudeDatum, 'sea-surface');
|
||||
assert.equal(row.observedAtMs, now);
|
||||
assert.equal('extra' in row, false);
|
||||
// Only a source's own true marks a virtual aid to navigation.
|
||||
assert.equal(row.virtual, false);
|
||||
const base = { mmsi: '993672001', lat: 30, lon: -97 };
|
||||
assert.equal(
|
||||
normalizeVesselObservation({ ...base, virtual: true }).virtual,
|
||||
true,
|
||||
);
|
||||
for (const virtual of ['true', 1, false, null])
|
||||
assert.equal(
|
||||
normalizeVesselObservation({ ...base, virtual }).virtual,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
normalizeVesselObservation({ mmsi: 'x', lat: null, lon: null }),
|
||||
null,
|
||||
|
||||
@@ -55,6 +55,8 @@ export function normalizeVesselObservation(row, reference = null, id = null) {
|
||||
epoch(row.last_position_epoch, 1000) ??
|
||||
epoch(Date.parse(row.last_position_UTC)),
|
||||
altitudeDatum: 'sea-surface',
|
||||
// A virtual aid to navigation: a mark broadcast with nothing there.
|
||||
virtual: row.virtual === true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user