From d58199a21497f3f71b2ef211965c5b14c7045872 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:54:28 +0530 Subject: [PATCH 1/3] =?UTF-8?q?fix(vessels):=20show=20a=20359.7=C2=B0=20co?= =?UTF-8?q?urse=20as=200=C2=B0,=20not=20360=C2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vessel cards and the AIS HUD rounded the direction without wrapping, so a vessel reporting no heading and a course over ground of 359.5° or more read "360°" on its cards and "HDG: 360DEG" in the HUD. The rounded direction now wraps to 0°. --- docs/CURRENT-STATE.md | 4 +++- src/data/aisLiveVessels.test.mjs | 21 +++++++++++++++++++++ src/layers/vessels/cards.js | 9 ++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 0cb46940c..11bc07655 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -503,7 +503,9 @@ those reports arrive as 102.3 knots and 360 degrees. Both are stored as unknown, and vessel cards, the HUD and analyst queries show a missing measurement rather than a reading. Heading keeps its existing separate check for its own sentinel. Genuine readings are unchanged, including a stopped vessel's zero and the -highest speed and course the fields can encode. +highest speed and course the fields can encode. Vessel cards and the HUD wrap +the direction after rounding to whole degrees, so a course of 359.5° or more +reads `0°` (`0DEG` in the HUD) rather than `360°`. For an already tracked vessel, AISStream static messages update name, type, destination and IMO without changing its position, motion, fix timestamps, diff --git a/src/data/aisLiveVessels.test.mjs b/src/data/aisLiveVessels.test.mjs index 96a5cd105..85abb9323 100644 --- a/src/data/aisLiveVessels.test.mjs +++ b/src/data/aisLiveVessels.test.mjs @@ -1322,6 +1322,27 @@ test('buildVesselCard: heading falls back to course; missing parts are omitted', assert.deepEqual(bare.details, []); }); +test('vessel cards read a course that rounds to north as 0°, not 360°', () => { + const card = (course) => buildVesselCard(makeRecord({ heading: null, course })).details[0]; + assert.equal(card(359.4), 'CONTAINER SHIP · 14.5KT · 359°'); + assert.equal(card(359.7), 'CONTAINER SHIP · 14.5KT · 0°'); + assert.equal( + buildSelectedVesselCard(makeRecord({ heading: null, course: 359.7 })).details[0], + 'CONTAINER SHIP · 14.5KT · 0°', + ); +}); + +test('the vessel HUD reads a course that rounds to north as HDG 0DEG', () => { + const harness = installWireHarness(undefined, { selectedRecord: null }); + try { + Object.assign(harness.record, { heading: null, course: 359.7 }); + assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true); + assert.match(harness.hud.textContent, /HDG: 0DEG/); + } finally { + harness.cleanup(); + } +}); + test('buildVesselCard: unnamed vessels title as MMSI; long names truncate', () => { const unnamed = buildVesselCard(makeRecord({ name: 'VESSEL' })); assert.equal(unnamed.title, 'MMSI 353136000'); diff --git a/src/layers/vessels/cards.js b/src/layers/vessels/cards.js index 64e115637..a5671ae54 100644 --- a/src/layers/vessels/cards.js +++ b/src/layers/vessels/cards.js @@ -60,7 +60,10 @@ export function createCards({ if (record.speed !== null && record.speed !== undefined) parts.push(formatSpeed(record.speed)); const direction = record.heading ?? record.course; - if (Number.isFinite(direction)) parts.push(`${Math.round(direction)}°`); + // A course of 359.5° and above rounds to north; modulo keeps it from + // printing 360°. The selected card and the HUD wrap the same way. + if (Number.isFinite(direction)) + parts.push(`${Math.round(direction) % 360}°`); return { id: vesselOverlayEntryId(record), actionable: Boolean(record?.mmsi), @@ -91,7 +94,7 @@ export function createCards({ [ vesselTypeShort(record) || t('VESSEL'), formatSpeed(record.speed), - Number.isFinite(direction) ? `${Math.round(direction)}°` : '--°', + Number.isFinite(direction) ? `${Math.round(direction) % 360}°` : '--°', ].join(' · '), ]; const destination = String(record.destination || '').trim(); @@ -181,7 +184,7 @@ export function createCards({ function formatHeading(heading) { return t('{heading}DEG', { - heading: Number.isFinite(heading) ? Math.round(heading) : '--', + heading: Number.isFinite(heading) ? Math.round(heading) % 360 : '--', }); } From 003a622602c72b7edc8e3245478d4c7473e83212 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:57:12 +0530 Subject: [PATCH 2/3] docs(changelog): add #1432 fragment --- changelog.d/1432-vessel-course-north.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1432-vessel-course-north.md diff --git a/changelog.d/1432-vessel-course-north.md b/changelog.d/1432-vessel-course-north.md new file mode 100644 index 000000000..45d9e5845 --- /dev/null +++ b/changelog.d/1432-vessel-course-north.md @@ -0,0 +1,3 @@ +- Show a vessel course of 359.5° or more as 0°. Vessel cards and the AIS HUD + rounded the course without wrapping, so a ship with no reported heading on + a 359.7° course read "360°" and "HDG: 360DEG" (Aditya, @Rohilalala, #1432). From 0e170e0515b646d5250e2eb28ddb1f1a798b801f Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 20:53:37 -0800 Subject: [PATCH 3/3] test(vessels): read the north-rounding HUD heading from the published readout The vessel HUD now publishes its text through services.readouts, so the test harness exposes hud.readout rather than a DOM node's textContent. --- src/data/aisLiveVessels.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/aisLiveVessels.test.mjs b/src/data/aisLiveVessels.test.mjs index f9a897f65..8e12bff94 100644 --- a/src/data/aisLiveVessels.test.mjs +++ b/src/data/aisLiveVessels.test.mjs @@ -1336,7 +1336,7 @@ test('the vessel HUD reads a course that rounds to north as HDG 0DEG', () => { try { Object.assign(harness.record, { heading: null, course: 359.7 }); assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true); - assert.match(harness.hud.textContent, /HDG: 0DEG/); + assert.match(harness.hud.readout.text, /HDG: 0DEG/); } finally { harness.cleanup(); }