From a9d390c165d731abecb7e3a77259217defd43f96 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 13 Nov 2026 07:34:41 -0800 Subject: [PATCH 1/3] 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', From 432a29ba59438273158ecb7791b5ac7cc566b7ce Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 13 Nov 2026 07:34:50 -0800 Subject: [PATCH 2/3] Add the changelog fragment --- changelog.d/1715-vessel-typical-length.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/1715-vessel-typical-length.md diff --git a/changelog.d/1715-vessel-typical-length.md b/changelog.d/1715-vessel-typical-length.md new file mode 100644 index 000000000..9cb589974 --- /dev/null +++ b/changelog.d/1715-vessel-typical-length.md @@ -0,0 +1,3 @@ +- Draw a vessel whose feed sends no length at its ship type's typical size, so + a cruise ship or container ship no longer shows as its type's smallest model + (samehkhamis, #1715). From 92d34bc4cca3f8d93cba159b7d357dafec94c314 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 13 Nov 2026 07:42:27 -0800 Subject: [PATCH 3/3] Stretch a vessel to its beam only beside a reported length A vessel drawn at its type's typical length keeps the model's proportions: a reported beam alone would squeeze a 300 m container ship to that width. --- src/data/fleetModels.js | 10 +++++++--- src/data/fleetModels.test.mjs | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/data/fleetModels.js b/src/data/fleetModels.js index 6a3749561..360c59af3 100644 --- a/src/data/fleetModels.js +++ b/src/data/fleetModels.js @@ -343,18 +343,22 @@ export function aircraftModelSpec(set, { typeCode, type, category } = {}) { * @returns {object} {@link fleetModelSpec} */ export function vesselModelSpec(set, { type, length, beam, name } = {}) { + // A beam counts only beside a reported length: a vessel drawn at its type's + // typical length keeps the model's proportions. + const reported = hullLength(length ?? undefined); + const hullWidth = reported === undefined ? undefined : (beam ?? undefined); const resolved = resolveVessel( set.types, aisShipTypeCode(type), - length ?? undefined, - beam ?? undefined, + reported, + hullWidth, name ?? undefined, ); return specOf( set, resolved, (r) => ({ model: set.types.default_vessel, length: r.length }), - { beam: beam ?? undefined }, + { beam: hullWidth }, ); } diff --git a/src/data/fleetModels.test.mjs b/src/data/fleetModels.test.mjs index f0387e65d..8d773901f 100644 --- a/src/data/fleetModels.test.mjs +++ b/src/data/fleetModels.test.mjs @@ -291,6 +291,12 @@ test('vessel specs read AIS codes and text families, stretched to the reported b const [sx, sy, sz] = box.scaleXYZ; assert.ok(Math.abs(sy - 51 / catalog.container_ship.beam) < 1e-12); assert.ok(Math.abs(sz - (sx + sy) / 2) < 1e-12); + // A beam with no reported length: the type's typical length, in the model's + // own proportions, not a typical-length hull squeezed to that beam. + const unsized = vesselModelSpec(set, { type: '70', length: 0, beam: 14 }); + assert.equal(unsized.model, 'container_ship'); + const [ux, uy, uz] = unsized.scaleXYZ; + assert.ok(Math.abs(uy - ux) < 1e-12 && Math.abs(uz - ux) < 1e-12); assert.equal(vesselModelSpec(set, { type: 'Tug', length: 30 }).model, 'tug'); assert.equal(vesselModelSpec(set, { type: '36' }).model, 'sailboat'); // No type, no size: the generic vessel as built.