Compare commits

...
Author SHA1 Message Date
Sameh Khamis 7df581765d Merge pull request #1715 from bilawalsidhu/sameh/vessel-typical-length
Draw a vessel of unknown length at its ship type's typical size
2026-11-13 07:52:52 -08:00
Sameh Khamis 92d34bc4cc 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.
2026-11-13 07:42:27 -08:00
Sameh Khamis 432a29ba59 Add the changelog fragment 2026-11-13 07:34:50 -08:00
Sameh Khamis a9d390c165 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.
2026-11-13 07:34:41 -08:00
4 changed files with 56 additions and 9 deletions

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.