mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
fix(tools): ship types read as their family; letterless destinations are unknown
AIS sends ship types as ITU-R M.1371 codes, so vessels_in_area grouped
"70", "74" and "79" as three types, and type: "tanker" matched nothing on
feeds that send codes. Codes now read as the family the vessel cards already
show (CARGO, TANKER, TUG), for rows, group_by and the type filter. The code
table moves to src/data/vesselTypes.js, which has no i18n import, so the
server-side tools can use it; vesselLabels re-exports it.
A destination with no letters ("0", "---") now counts as unknown.
Live, Singapore: "most by type: CARGO (3), TANKER (3), PASSENGER (1)"; "how
many tankers near Singapore" found 3.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d06e485c12
commit
d39256e700
6 files changed
+126
-48
No files matched your search
@@ -4,5 +4,6 @@
|
||||
of estimating from the returned rows. Airlines come from the ICAO designator
|
||||
in each callsign (SWA1234 counts as SWA), so the breakdown works on feeds
|
||||
that report no operator; operator and type are counted only where the feed
|
||||
reports them; AIS ship type 0 ("not available") counts as unknown
|
||||
reports them. Ship types count by family, so AIS codes 70 to 79 are all
|
||||
cargo, and type 0 ("not available") counts as unknown
|
||||
(quanticsoul4772, #1056).
|
||||
@@ -1040,6 +1040,7 @@
|
||||
"src/layers/vessels/tracking.js",
|
||||
"src/data/iconOrientation.js",
|
||||
"src/data/vesselLabels.js",
|
||||
"src/data/vesselTypes.js",
|
||||
"src/layers/vessels/records.js",
|
||||
"src/layers/vessels/recordPolicy.js",
|
||||
"src/layers/vessels/snapshotRenderer.js",
|
||||
@@ -1841,6 +1842,7 @@
|
||||
"src/data/trafficQueue.js",
|
||||
"src/data/trailRenderer.js",
|
||||
"src/data/vesselLabels.js",
|
||||
"src/data/vesselTypes.js",
|
||||
"src/firstRunExperience.js",
|
||||
"src/hud.js",
|
||||
"src/hudLocality.js",
|
||||
@@ -2629,6 +2631,7 @@
|
||||
"src/data/trafficQueue.js",
|
||||
"src/data/trailRenderer.js",
|
||||
"src/data/vesselLabels.js",
|
||||
"src/data/vesselTypes.js",
|
||||
"src/keylessGeocoder.js",
|
||||
"src/layers/aircraft/classification.js",
|
||||
"src/layers/alpr/index.js",
|
||||
@@ -3782,6 +3785,7 @@
|
||||
"src/tools/queries/maritime.js",
|
||||
"src/tools/queries/share.js",
|
||||
"src/data/idSwitches.js",
|
||||
"src/data/vesselTypes.js",
|
||||
"src/data/layerState.js",
|
||||
"src/data/layerStateTokenReservations.json",
|
||||
"src/sources/gbfsStations.js",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
*/
|
||||
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { normalizeVesselType } from './vesselTypes.js';
|
||||
|
||||
export { normalizeVesselType };
|
||||
|
||||
export const VESSEL_OVERLAY_SOURCE_ID = 'ais-live-vessels';
|
||||
/** Existing selector grid size; one ambient winner is retained per cell. */
|
||||
@@ -43,31 +46,6 @@ const TYPE_STYLES = [
|
||||
];
|
||||
const DEFAULT_STYLE = { css: '#39d5ff', accent: '57, 213, 255' };
|
||||
|
||||
const NUMERIC_TYPE_SPECIALS = {
|
||||
30: 'FISHING',
|
||||
31: 'TOWING',
|
||||
32: 'TOWING',
|
||||
33: 'DREDGER',
|
||||
34: 'DIVE OPS',
|
||||
35: 'MILITARY',
|
||||
36: 'SAILING',
|
||||
37: 'PLEASURE',
|
||||
50: 'PILOT',
|
||||
51: 'SAR',
|
||||
52: 'TUG',
|
||||
53: 'PORT TENDER',
|
||||
54: 'ANTI-POLLUTION',
|
||||
55: 'LAW ENFORCE',
|
||||
58: 'MEDICAL',
|
||||
};
|
||||
const NUMERIC_TYPE_FAMILIES = {
|
||||
4: 'HIGH-SPEED',
|
||||
6: 'PASSENGER',
|
||||
7: 'CARGO',
|
||||
8: 'TANKER',
|
||||
9: 'OTHER',
|
||||
};
|
||||
|
||||
// Card text for the type tokens above. The tokens stay English: TYPE_STYLES
|
||||
// and tests match on them.
|
||||
const TYPE_DISPLAY_KEYS = Object.freeze({
|
||||
@@ -104,21 +82,6 @@ export function displayVesselType(type) {
|
||||
return key ? t(key) : normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an AIS type to display text: bare numeric ship-type codes map to
|
||||
* family names ("71" → "CARGO"); text types pass through unchanged.
|
||||
* @param {string} type Raw AIS type.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeVesselType(type) {
|
||||
const text = String(type || '').trim();
|
||||
if (!text || !/^\d{1,2}$/.test(text)) return text;
|
||||
const code = Number(text);
|
||||
if (code <= 0) return '';
|
||||
if (NUMERIC_TYPE_SPECIALS[code]) return NUMERIC_TYPE_SPECIALS[code];
|
||||
return NUMERIC_TYPE_FAMILIES[Math.floor(code / 10)] || 'OTHER';
|
||||
}
|
||||
|
||||
/** AIS ship type → CSS hex hue for the billboard chevron. */
|
||||
export function vesselTypeCss(type) {
|
||||
return styleForType(type).css;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @module vesselTypes
|
||||
* @description AIS ship-type codes (ITU-R M.1371) to family names, with no
|
||||
* browser or i18n dependencies so the server-side query tools can use it.
|
||||
*/
|
||||
|
||||
const NUMERIC_TYPE_SPECIALS = {
|
||||
30: 'FISHING',
|
||||
31: 'TOWING',
|
||||
32: 'TOWING',
|
||||
33: 'DREDGER',
|
||||
34: 'DIVE OPS',
|
||||
35: 'MILITARY',
|
||||
36: 'SAILING',
|
||||
37: 'PLEASURE',
|
||||
50: 'PILOT',
|
||||
51: 'SAR',
|
||||
52: 'TUG',
|
||||
53: 'PORT TENDER',
|
||||
54: 'ANTI-POLLUTION',
|
||||
55: 'LAW ENFORCE',
|
||||
58: 'MEDICAL',
|
||||
};
|
||||
const NUMERIC_TYPE_FAMILIES = {
|
||||
4: 'HIGH-SPEED',
|
||||
6: 'PASSENGER',
|
||||
7: 'CARGO',
|
||||
8: 'TANKER',
|
||||
9: 'OTHER',
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve an AIS type to display text: bare numeric ship-type codes map to
|
||||
* family names ("71" → "CARGO"); text types pass through unchanged.
|
||||
* @param {string} type Raw AIS type.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function normalizeVesselType(type) {
|
||||
const text = String(type || '').trim();
|
||||
if (!text || !/^\d{1,2}$/.test(text)) return text;
|
||||
const code = Number(text);
|
||||
if (code <= 0) return '';
|
||||
if (NUMERIC_TYPE_SPECIALS[code]) return NUMERIC_TYPE_SPECIALS[code];
|
||||
return NUMERIC_TYPE_FAMILIES[Math.floor(code / 10)] || 'OTHER';
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
numericSummary,
|
||||
thinEvenly,
|
||||
} from '../results.js';
|
||||
import { normalizeVesselType } from '../../data/vesselTypes.js';
|
||||
|
||||
const MMSI_SCHEMA = Object.freeze({
|
||||
type: 'string',
|
||||
@@ -32,11 +33,17 @@ const ALL_VESSELS = 1_000_000;
|
||||
const round = (value, digits) =>
|
||||
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
|
||||
const text = (value) => (typeof value === 'string' && value ? value : null);
|
||||
// AIS ship type 0 is "not available" (ITU-R M.1371); the source keeps it as
|
||||
// '0', so it is reported as unknown rather than counted as a type.
|
||||
const vesselType = (value) => {
|
||||
const type = text(value);
|
||||
return type === '0' ? null : type;
|
||||
// AIS sends ship types as ITU-R M.1371 codes ("70" to "79" are all cargo), so
|
||||
// a code reads as its family ("CARGO"), which is what group_by counts and the
|
||||
// type filter matches; text types pass through. Type 0 is "not available" and
|
||||
// reads as unknown.
|
||||
const vesselType = (value) => normalizeVesselType(text(value) ?? '') || null;
|
||||
|
||||
// Crews type the destination by hand; one with no letters ("0", "---") names
|
||||
// no port and reads as unknown.
|
||||
const vesselDestination = (value) => {
|
||||
const destination = text(value);
|
||||
return destination && /\p{L}/u.test(destination) ? destination : null;
|
||||
};
|
||||
|
||||
function vesselRow(record, from) {
|
||||
@@ -46,7 +53,7 @@ function vesselRow(record, from) {
|
||||
name: text(record.name),
|
||||
imo: text(record.imo),
|
||||
type: vesselType(record.type),
|
||||
destination: text(record.destination),
|
||||
destination: vesselDestination(record.destination),
|
||||
lat: round(record.latitude, 5),
|
||||
lon: round(record.longitude, 5),
|
||||
speed_mps: round(record.speedMps, 1),
|
||||
@@ -134,7 +141,8 @@ export const vesselsInArea = defineTool({
|
||||
.filter(
|
||||
(record) =>
|
||||
areaContains(area, { lat: record.latitude, lon: record.longitude }) &&
|
||||
(!wanted || String(record.type).toLowerCase().includes(wanted)),
|
||||
(!wanted ||
|
||||
(vesselType(record.type) ?? '').toLowerCase().includes(wanted)),
|
||||
)
|
||||
.map((record) => vesselRow(record, center))
|
||||
.sort((a, b) => a.distance_km - b.distance_km);
|
||||
|
||||
@@ -263,6 +263,63 @@ test('AIS type 0 (not available) is counted as unknown, not as a type', async ()
|
||||
);
|
||||
});
|
||||
|
||||
test('numeric AIS ship-type codes group and filter by their family', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
vessels: vessels({
|
||||
records: [
|
||||
vessel('366000001', 37.78, -122.4, { type: '70' }),
|
||||
vessel('366000002', 37.79, -122.41, { type: '74' }),
|
||||
vessel('366000003', 37.77, -122.39, { type: '79' }),
|
||||
vessel('366000004', 37.76, -122.38, { type: '80' }),
|
||||
vessel('366000005', 37.75, -122.37, { type: '52' }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
const grouped = await catalog.call('vessels_in_area', {
|
||||
area: bay,
|
||||
group_by: 'type',
|
||||
});
|
||||
assert.deepEqual(grouped.data.breakdown.groups, [
|
||||
{ value: 'CARGO', count: 3 },
|
||||
{ value: 'TANKER', count: 1 },
|
||||
{ value: 'TUG', count: 1 },
|
||||
]);
|
||||
const tankers = await catalog.call('vessels_in_area', {
|
||||
area: bay,
|
||||
type: 'tanker',
|
||||
});
|
||||
assert.deepEqual(
|
||||
tankers.data.rows.map((row) => [row.mmsi, row.type]),
|
||||
[['366000004', 'TANKER']],
|
||||
);
|
||||
});
|
||||
|
||||
test('a destination with no letters counts as unknown', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
vessels: vessels({
|
||||
records: [
|
||||
vessel('366000001', 37.78, -122.4, { destination: '0' }),
|
||||
vessel('366000002', 37.79, -122.41, { destination: '---' }),
|
||||
vessel('366000003', 37.77, -122.39, { destination: 'NLRTM' }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
const result = await catalog.call('vessels_in_area', {
|
||||
area: bay,
|
||||
group_by: 'destination',
|
||||
});
|
||||
assert.deepEqual(result.data.breakdown.groups, [
|
||||
{ value: 'NLRTM', count: 1 },
|
||||
]);
|
||||
assert.equal(result.data.breakdown.unknown_count, 2);
|
||||
});
|
||||
|
||||
test('vessels in an area can be counted by destination with a speed summary', async () => {
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
|
||||
Reference in new issue
Block a user