mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #649 from theerayutbo/fix/ais-unavailable-sog-cog
fix: treat AIS "not available" speed and course as unknown
This commit is contained in:
5 files changed
+103
-3
No files matched your search
@@ -1,5 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
- Report AIS speed and course that carry the standard "not available" code as
|
||||
unknown instead of 102.3 knots and 360 degrees. Genuine readings, including a
|
||||
stopped vessel's zero and the highest encodable values, are unchanged.
|
||||
|
||||
- Render native `<select>` option lists in the dark UI palette. The closed
|
||||
controls were already skinned, but the browser-painted popups fell back to the
|
||||
platform light palette, leaving near-white option text on a white surface in
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# God's Eye View Current State
|
||||
|
||||
AIS encodes speed over ground in 0.1-knot units and course over ground in
|
||||
0.1-degree units, reserving the top code of each field for "not available", so
|
||||
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.
|
||||
|
||||
Native `<select>` controls declare a dark colour scheme and explicit option
|
||||
colours, so the browser-painted option list matches the panel it drops out of.
|
||||
The scheme is declared on the controls rather than on the document root, so
|
||||
|
||||
@@ -744,5 +744,6 @@
|
||||
"src/director/interactions/interactions.test.mjs",
|
||||
"scripts/qa-director-interactions.mjs",
|
||||
"src/director/sharing/sharing.test.mjs",
|
||||
"scripts/qa-director-sharing.mjs"
|
||||
"scripts/qa-director-sharing.mjs",
|
||||
"src/data/aisStreamSentinels.test.mjs"
|
||||
]
|
||||
@@ -72,8 +72,8 @@ export function ingestAisStreamEnvelope(envelope) {
|
||||
imo: stringValue(message.ImoNumber ?? message.IMO ?? staticData.imo),
|
||||
type: vesselTypeFromAis(message, staticData),
|
||||
destination: stringValue(message.Destination ?? staticData.destination),
|
||||
speed: numberValue(message.Sog ?? message.SOG),
|
||||
course: numberValue(message.Cog ?? message.COG),
|
||||
speed: normalizedSpeedOverGround(message.Sog ?? message.SOG),
|
||||
course: normalizedCourseOverGround(message.Cog ?? message.COG),
|
||||
heading: normalizedHeading(message.TrueHeading ?? message.Heading),
|
||||
last_position_UTC: normalizeAisTimestamp(
|
||||
metadata.time_utc ?? metadata.TimeUtc,
|
||||
@@ -277,6 +277,28 @@ function normalizedHeading(value) {
|
||||
return heading !== null && heading >= 0 && heading <= 360 ? heading : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speed over ground in knots, or null when the report carries AIS's "not
|
||||
* available" code. ITU-R M.1371 encodes SOG in 0.1-knot units where 1022 means
|
||||
* "102.2 knots or higher" and 1023 means unavailable, so 102.3 is a sentinel
|
||||
* and not a reading. It is finite, so every consumer would otherwise treat it
|
||||
* as a genuine speed.
|
||||
*/
|
||||
function normalizedSpeedOverGround(value) {
|
||||
const speed = numberValue(value);
|
||||
return speed !== null && speed >= 0 && speed < 102.3 ? speed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Course over ground in degrees, or null when the report carries AIS's "not
|
||||
* available" code (3600 in 0.1-degree units, i.e. 360). Valid courses stop at
|
||||
* 359.9; heading keeps its own separate sentinel of 511.
|
||||
*/
|
||||
function normalizedCourseOverGround(value) {
|
||||
const course = numberValue(value);
|
||||
return course !== null && course >= 0 && course < 360 ? course : null;
|
||||
}
|
||||
|
||||
function normalizeAisTimestamp(value) {
|
||||
const text = stringValue(value);
|
||||
if (!text) return new Date().toISOString();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
aisStreamRows,
|
||||
ingestAisStreamEnvelope,
|
||||
} from '../../server/providers/vessels/ais-store.js';
|
||||
|
||||
// ITU-R M.1371 encodes speed over ground in 0.1-knot units and course over
|
||||
// ground in 0.1-degree units, reserving the top code of each field for "not
|
||||
// available": SOG 1023 arrives as 102.3 knots and COG 3600 as 360 degrees.
|
||||
// Both are finite numbers, so they reach consumers as ordinary readings unless
|
||||
// the store rejects them here.
|
||||
const positionReport = (mmsi, body) => ({
|
||||
MessageType: 'PositionReport',
|
||||
Message: { PositionReport: { UserID: Number(mmsi), ...body } },
|
||||
MetaData: {
|
||||
MMSI: mmsi,
|
||||
latitude: 53.40879,
|
||||
longitude: 6.19741,
|
||||
time_utc: '2026-09-17 20:00:00.000000000 +0000 UTC',
|
||||
},
|
||||
});
|
||||
|
||||
const rowFor = (mmsi) => aisStreamRows(50000).find((row) => row.mmsi === mmsi);
|
||||
|
||||
test('speed over ground reported as unavailable is unknown, not 102.3 knots', () => {
|
||||
assert.equal(
|
||||
ingestAisStreamEnvelope(positionReport('244060365', { Sog: 102.3 })),
|
||||
true,
|
||||
);
|
||||
assert.equal(rowFor('244060365').speed, null);
|
||||
});
|
||||
|
||||
test('course over ground reported as unavailable is unknown, not 360 degrees', () => {
|
||||
assert.equal(
|
||||
ingestAisStreamEnvelope(positionReport('245382000', { Cog: 360 })),
|
||||
true,
|
||||
);
|
||||
assert.equal(rowFor('245382000').course, null);
|
||||
});
|
||||
|
||||
test('genuine speed and course readings survive the unavailable check', () => {
|
||||
ingestAisStreamEnvelope(
|
||||
positionReport('341260000', { Sog: 12.9, Cog: 74.6 }),
|
||||
);
|
||||
const row = rowFor('341260000');
|
||||
assert.equal(row.speed, 12.9);
|
||||
assert.equal(row.course, 74.6);
|
||||
});
|
||||
|
||||
test('a stopped vessel keeps its zero speed and zero course', () => {
|
||||
ingestAisStreamEnvelope(positionReport('244374235', { Sog: 0, Cog: 0 }));
|
||||
const row = rowFor('244374235');
|
||||
assert.equal(row.speed, 0);
|
||||
assert.equal(row.course, 0);
|
||||
});
|
||||
|
||||
test('the fastest encodable reading below the unavailable code is kept', () => {
|
||||
ingestAisStreamEnvelope(
|
||||
positionReport('244060366', { Sog: 102.2, Cog: 359.9 }),
|
||||
);
|
||||
const row = rowFor('244060366');
|
||||
assert.equal(row.speed, 102.2);
|
||||
assert.equal(row.course, 359.9);
|
||||
});
|
||||
Reference in new issue
Block a user