mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view. Co-authored-by: Gustavo Beneduzi <[email protected]> Co-authored-by: Daniel Slay <[email protected]> Co-authored-by: Rehaan Delmotra <[email protected]> Co-authored-by: Cursor Agent <[email protected]>
159 lines
4.9 KiB
JavaScript
159 lines
4.9 KiB
JavaScript
/**
|
|
* @module vesselLabels
|
|
* @description AIS type formatting and shared world-overlay policy retained
|
|
* after the vessel cards moved out of their dedicated canvas renderer.
|
|
*/
|
|
|
|
export const VESSEL_OVERLAY_SOURCE_ID = 'ais-live-vessels';
|
|
/** Existing selector grid size; one ambient winner is retained per cell. */
|
|
export const VESSEL_LABEL_GRID_PX = 118;
|
|
/** Existing environment-default ceiling for ambient vessel rows. */
|
|
export const VESSEL_DEFAULT_LABEL_LIMIT = 900;
|
|
/** Existing configured absolute ceiling; viewport grid demand is usually lower. */
|
|
export const VESSEL_OVERLAY_MAX_COHORT = VESSEL_DEFAULT_LABEL_LIMIT;
|
|
/** Existing ambient vessel-card distance fade reaches zero at 5000 km. */
|
|
export const VESSEL_CARD_FADE_DISTANCE_M = 5_000_000;
|
|
|
|
/**
|
|
* AIS type family → chevron hue + card accent. Single source of truth for
|
|
* vessel type colors so billboard chevrons and host cards cannot drift apart.
|
|
*/
|
|
const TYPE_STYLES = [
|
|
{ pattern: /tanker/i, css: '#ffb347', accent: '255, 179, 71' },
|
|
{
|
|
pattern: /cargo|container|bulk|carrier/i,
|
|
css: '#39d5ff',
|
|
accent: '57, 213, 255',
|
|
},
|
|
{
|
|
pattern: /passenger|ferry|cruise/i,
|
|
css: '#ff7adf',
|
|
accent: '255, 122, 223',
|
|
},
|
|
{ pattern: /fishing/i, css: '#7cff9b', accent: '124, 255, 155' },
|
|
{
|
|
pattern: /tug|tow|pilot|supply|service/i,
|
|
css: '#f7f0a3',
|
|
accent: '247, 240, 163',
|
|
},
|
|
];
|
|
const DEFAULT_STYLE = { css: '#39d5ff', accent: '57, 213, 255' };
|
|
|
|
const NUMERIC_TYPE_SPECIALS = {
|
|
30: 'FISHING',
|
|
31: 'TOWING',
|
|
32: 'TOWING',
|
|
33: 'DREDGER',
|
|
34: 'DIVE OPS',
|
|
35: 'MILITARY',
|
|
36: 'SAILING',
|
|
37: 'PLEASURE',
|
|
50: 'PILOT',
|
|
51: 'SAR',
|
|
52: 'TUG',
|
|
53: 'PORT TENDER',
|
|
54: 'ANTI-POLLUTION',
|
|
55: 'LAW ENFORCE',
|
|
58: 'MEDICAL',
|
|
};
|
|
const NUMERIC_TYPE_FAMILIES = {
|
|
4: 'HIGH-SPEED',
|
|
6: 'PASSENGER',
|
|
7: 'CARGO',
|
|
8: 'TANKER',
|
|
9: 'OTHER',
|
|
};
|
|
|
|
/**
|
|
* Resolve an AIS type to display text: bare numeric ship-type codes map to
|
|
* family names ("71" → "CARGO"); text types pass through unchanged.
|
|
* @param {string} type Raw AIS type.
|
|
* @returns {string}
|
|
*/
|
|
export function normalizeVesselType(type) {
|
|
const text = String(type || '').trim();
|
|
if (!text || !/^\d{1,2}$/.test(text)) return text;
|
|
const code = Number(text);
|
|
if (code <= 0) return '';
|
|
if (NUMERIC_TYPE_SPECIALS[code]) return NUMERIC_TYPE_SPECIALS[code];
|
|
return NUMERIC_TYPE_FAMILIES[Math.floor(code / 10)] || 'OTHER';
|
|
}
|
|
|
|
/** AIS ship type → CSS hex hue for the billboard chevron. */
|
|
export function vesselTypeCss(type) {
|
|
return styleForType(type).css;
|
|
}
|
|
|
|
/** AIS ship type → "r, g, b" accent string for the host card. */
|
|
export function accentForVesselType(type) {
|
|
return styleForType(type).accent;
|
|
}
|
|
|
|
function styleForType(type) {
|
|
const text = normalizeVesselType(type);
|
|
return TYPE_STYLES.find((entry) => entry.pattern.test(text)) || DEFAULT_STYLE;
|
|
}
|
|
|
|
/**
|
|
* Derive the source's ambient cohort from the shipped selector grid. This is
|
|
* an upper bound; the existing greedy 150 px separation usually yields fewer.
|
|
* Selected vessels are protected and do not consume this budget.
|
|
* @param {number} width CSS viewport width.
|
|
* @param {number} height CSS viewport height.
|
|
* @param {number} [rowLimit=900] Configured source row ceiling.
|
|
* @returns {number}
|
|
*/
|
|
export function vesselOverlayCohortLimit(
|
|
width,
|
|
height,
|
|
rowLimit = VESSEL_DEFAULT_LABEL_LIMIT,
|
|
) {
|
|
const w = Number(width);
|
|
const h = Number(height);
|
|
const requested = Number(rowLimit);
|
|
if (!(w > 0) || !(h > 0) || !(requested > 0)) return 0;
|
|
const gridCapacity =
|
|
Math.ceil(w / VESSEL_LABEL_GRID_PX) * Math.ceil(h / VESSEL_LABEL_GRID_PX);
|
|
return Math.min(
|
|
VESSEL_OVERLAY_MAX_COHORT,
|
|
Math.floor(requested),
|
|
gridCapacity,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add host-owned layout, fade, collision and paint-lane fields to a formatted
|
|
* vessel card. Ambient and selected cards share `ambient-card`, so the host's
|
|
* protected selected rectangle excludes ambient cards while bypassing quotas.
|
|
* @param {Object} card Source-formatted vessel card.
|
|
* @param {number} [fadeDistance=5000000] Ambient distance-fade endpoint.
|
|
* @returns {Object}
|
|
*/
|
|
export function applyVesselOverlayPolicy(
|
|
card,
|
|
fadeDistance = VESSEL_CARD_FADE_DISTANCE_M,
|
|
) {
|
|
const selected = card?.selected === true;
|
|
const rawGap = Number(card?.gapPx) || 10;
|
|
const gapPx = Math.max(selected ? 28 : 12, rawGap + 8);
|
|
return {
|
|
...card,
|
|
variant: selected ? 'selected' : 'card',
|
|
protected: selected,
|
|
collisionGroup: 'ambient-card',
|
|
cardStyle: 'tactical',
|
|
gapPx,
|
|
leaderOffsetPx: Math.max(2, gapPx - 6),
|
|
// Selected detail cards may move beside the contact to clear solid panels.
|
|
verticalOnly: !selected,
|
|
viewportMargin: 4,
|
|
maxDistance: selected ? Number.POSITIVE_INFINITY : fadeDistance,
|
|
distanceFadeStartRatio: 0.7,
|
|
edgeFade: 'keyhole',
|
|
horizonCull: true,
|
|
terrainOcclusion: false,
|
|
// Only MMSI-keyed cards can resolve back to one actionable vessel.
|
|
interactive: card?.actionable === true,
|
|
};
|
|
}
|