feat(vessels, transit, rail): draw fleet models under the 3D toggle

Vessels, Transit and Rail turn nearby icons into fleet models at the
aircraft layers' distances. A vessel is drawn at its AIS length and beam
(static dimensions now reach vessel rows), standing at the sea surface; a
transit vehicle or train stands on its road or rail, raised by its type's
elevation, and is painted in its mode or category colour where the model
marks it.
This commit is contained in:
Sameh Khamis committed 2026-11-13 07:16:25 -08:00
1 parent fcc44d0eaf
commit 7b93045895
18 files changed
+984 -15

No files matched your search

+28
View File
@@ -57,8 +57,13 @@ export function ingestAisStreamEnvelope(envelope) {
message.ReportB?.Valid === false
? undefined
: message.ReportB?.ShipType,
Dimension:
message.ReportB?.Valid === false
? undefined
: message.ReportB?.Dimension,
}
: message;
const dimensions = aisDimensions(fields.Dimension);
// Static reports may be partial (including Class B's separate A/B parts).
// Keep known values in the cache as well as in the current live row.
const staticData = {
@@ -84,6 +89,8 @@ export function ingestAisStreamEnvelope(envelope) {
staticCode(live?.imo) ||
staticCode(previous.imo) ||
'',
length: dimensions.length ?? live?.length ?? previous.length ?? null,
beam: dimensions.beam ?? live?.beam ?? previous.beam ?? null,
};
_aisStreamStatic.set(mmsi, staticData);
mergeAisStaticIntoLiveVessel(mmsi, staticData);
@@ -113,6 +120,8 @@ export function ingestAisStreamEnvelope(envelope) {
imo: stringValue(message.ImoNumber ?? message.IMO ?? staticData.imo),
type: vesselTypeFromAis(message, staticData),
destination: stringValue(message.Destination ?? staticData.destination),
length: staticData.length ?? null,
beam: staticData.beam ?? null,
speed: normalizedSpeedOverGround(message.Sog ?? message.SOG),
course: normalizedCourseOverGround(message.Cog ?? message.COG),
heading: normalizedHeading(message.TrueHeading ?? message.Heading),
@@ -236,6 +245,25 @@ function mergeAisStaticIntoLiveVessel(mmsi, staticData) {
if (staticData.type) existing.type = staticData.type;
if (staticData.destination) existing.destination = staticData.destination;
if (staticData.imo) existing.imo = staticData.imo;
if (staticData.length) existing.length = staticData.length;
if (staticData.beam) existing.beam = staticData.beam;
}
/**
* A static report's hull size from its AIS dimensions: the antenna's
* distances to bow (A), stern (B), port (C) and starboard (D), in metres.
* Zero is AIS's "not available", so a side missing its two parts has none.
* @param {{A?: number, B?: number, C?: number, D?: number}|undefined} dimension
* @returns {{length: number|null, beam: number|null}}
*/
function aisDimensions(dimension) {
const part = (value) => {
const number = numberValue(value);
return number !== null && number > 0 ? number : 0;
};
const length = part(dimension?.A) + part(dimension?.B);
const beam = part(dimension?.C) + part(dimension?.D);
return { length: length > 0 ? length : null, beam: beam > 0 ? beam : null };
}
function vesselNameFromAis(metadata, message, staticData = {}) {