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). 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..360c59af3 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 }; } /** @@ -341,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 9eb484a6a..8d773901f 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', @@ -267,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.