From a9d390c165d731abecb7e3a77259217defd43f96 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 13 Nov 2026 07:34:41 -0800 Subject: [PATCH] Draw a vessel of unknown length at its ship type's typical size Most live vessel feeds send no length, and the resolver then fell through every length rule to the range's last model drawn as built, so a cruise ship or a container ship showed as the smallest model of its range. types.json gives each passenger, cargo, tanker, fishing and service range a typical_len (cargo 300 m, tanker 250 m, passenger 140 m, fishing 40 m, ...); a vessel with no usable length is chosen and drawn at it. --- public/models/fleet/types.json | 8 ++++++++ src/data/fleetModels.js | 14 ++++++++------ src/data/fleetModels.test.mjs | 24 ++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/public/models/fleet/types.json b/public/models/fleet/types.json index 3492f58c9..d0a62f091 100644 --- a/public/models/fleet/types.json +++ b/public/models/fleet/types.json @@ -1149,6 +1149,7 @@ 70, 79 ], + "typical_len": 300, "rules": [ { "min_len": 70, @@ -1188,6 +1189,7 @@ 80, 89 ], + "typical_len": 250, "rules": [ { "name_re": "\\bLNG\\b|\\bGAS\\b", @@ -1214,6 +1216,7 @@ 60, 69 ], + "typical_len": 140, "rules": [ { "min_len": 70, @@ -1243,6 +1246,7 @@ 37, 37 ], + "typical_len": 19, "rules": [ { "min_len": 35, @@ -1273,6 +1277,7 @@ 30, 30 ], + "typical_len": 24, "rules": [ { "min_len": 15, @@ -1343,6 +1348,7 @@ 55, 55 ], + "typical_len": 40, "rules": [ { "min_len": 25, @@ -1358,6 +1364,7 @@ 40, 49 ], + "typical_len": 40, "rules": [ { "min_len": 20, @@ -1373,6 +1380,7 @@ 33, 33 ], + "typical_len": 100, "rules": [ { "min_len": 50, diff --git a/src/data/fleetModels.js b/src/data/fleetModels.js index 7c0204fdd..6a3749561 100644 --- a/src/data/fleetModels.js +++ b/src/data/fleetModels.js @@ -133,8 +133,9 @@ export function hullBeam(beam) { /** * The model for a vessel of AIS ship type `shipType` and the reported length * and beam (metres) and name: the first rule that matches in the first type - * range that has one, else `default_vessel`. Drawn at the reported length, as - * built when it is unknown. + * range that has one, else `default_vessel`. Drawn at the reported length; + * when it is unknown (most live feeds send none), chosen and drawn at the + * range's `typical_len`, else as built. * @param {object} types Parsed `types.json`. * @param {number} [shipType] * @param {number} [length] @@ -145,13 +146,14 @@ export function hullBeam(beam) { export function resolveVessel(types, shipType, length, beam, name) { const L = hullLength(length) ?? 0; const B = hullBeam(beam) ?? 0; - const drawn = L > 0 ? L : null; if (shipType !== undefined && shipType !== null) { for (const group of types.vessels) { if (!(shipType >= group.types[0] && shipType <= group.types[1])) continue; + const len = L > 0 ? L : (group.typical_len ?? 0); + const drawn = len > 0 ? len : null; for (const rule of group.rules) { - if (rule.min_len && L < rule.min_len) continue; - if (rule.max_len && L > rule.max_len) continue; + if (rule.min_len && len < rule.min_len) continue; + if (rule.max_len && len > rule.max_len) continue; if (rule.name_re && !new RegExp(rule.name_re).test(name ?? '')) continue; if (rule.max_bl && !(B > 0 && L > 0 && B / L <= rule.max_bl)) continue; @@ -159,7 +161,7 @@ export function resolveVessel(types, shipType, length, beam, name) { } } } - return { model: types.default_vessel, length: drawn }; + return { model: types.default_vessel, length: L > 0 ? L : null }; } /** diff --git a/src/data/fleetModels.test.mjs b/src/data/fleetModels.test.mjs index 9eb484a6a..f0387e65d 100644 --- a/src/data/fleetModels.test.mjs +++ b/src/data/fleetModels.test.mjs @@ -57,6 +57,7 @@ const sliceTypes = { vessels: [ { types: [70, 79], + typical_len: 300, rules: [ { min_len: 70, max_len: 140, max_bl: 0.12, model: 'inland_cargo' }, { @@ -133,6 +134,29 @@ test('vessels: the first matching rule of the first type range with one, else th }); }); +test("vessels of unknown length are chosen and drawn at their range's typical length", () => { + assert.deepEqual(resolveVessel(sliceTypes, 70), { + model: 'container_ship', + length: 300, + }); + assert.deepEqual(resolveVessel(sliceTypes, 70, 511, 126), { + model: 'container_ship', + length: 300, + }); + assert.deepEqual(resolveVessel(sliceTypes, 70, 90, 15), { + model: 'general_cargo', + length: 90, + }); + assert.deepEqual(resolveVessel(sliceTypes, 31), { + model: 'tug', + length: null, + }); + // Every passenger, cargo and tanker range of the set has one. + for (const code of [60, 70, 80]) { + assert.ok(resolveVessel(types, code).length > 100, String(code)); + } +}); + test("vessels: AIS's out-of-range sizes (511 m long, 126 m abeam) are unknown, drawn as built", () => { assert.deepEqual(resolveVessel(sliceTypes, 52, 511), { model: 'tug',