mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04: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
|
# 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
|
- 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
|
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
|
platform light palette, leaving near-white option text on a white surface in
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# God's Eye View Current State
|
# 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
|
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.
|
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
|
The scheme is declared on the controls rather than on the document root, so
|
||||||
|
|||||||
@@ -744,5 +744,6 @@
|
|||||||
"src/director/interactions/interactions.test.mjs",
|
"src/director/interactions/interactions.test.mjs",
|
||||||
"scripts/qa-director-interactions.mjs",
|
"scripts/qa-director-interactions.mjs",
|
||||||
"src/director/sharing/sharing.test.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),
|
imo: stringValue(message.ImoNumber ?? message.IMO ?? staticData.imo),
|
||||||
type: vesselTypeFromAis(message, staticData),
|
type: vesselTypeFromAis(message, staticData),
|
||||||
destination: stringValue(message.Destination ?? staticData.destination),
|
destination: stringValue(message.Destination ?? staticData.destination),
|
||||||
speed: numberValue(message.Sog ?? message.SOG),
|
speed: normalizedSpeedOverGround(message.Sog ?? message.SOG),
|
||||||
course: numberValue(message.Cog ?? message.COG),
|
course: normalizedCourseOverGround(message.Cog ?? message.COG),
|
||||||
heading: normalizedHeading(message.TrueHeading ?? message.Heading),
|
heading: normalizedHeading(message.TrueHeading ?? message.Heading),
|
||||||
last_position_UTC: normalizeAisTimestamp(
|
last_position_UTC: normalizeAisTimestamp(
|
||||||
metadata.time_utc ?? metadata.TimeUtc,
|
metadata.time_utc ?? metadata.TimeUtc,
|
||||||
@@ -277,6 +277,28 @@ function normalizedHeading(value) {
|
|||||||
return heading !== null && heading >= 0 && heading <= 360 ? heading : null;
|
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) {
|
function normalizeAisTimestamp(value) {
|
||||||
const text = stringValue(value);
|
const text = stringValue(value);
|
||||||
if (!text) return new Date().toISOString();
|
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