Merge pull request #1715 from bilawalsidhu/sameh/vessel-typical-length

Draw a vessel of unknown length at its ship type's typical size
This commit is contained in:
Sameh Khamis authored and GitHub committed 2026-11-13 07:52:52 -08:00
commit 7df581765d
4 files changed
+56 -9

No files matched your search

@@ -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).
+8
View File
@@ -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,
+15 -9
View File
@@ -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 },
);
}
+30
View File
@@ -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.