mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
- The trailing visibility frame uses the layer's injectable runtime timer and is cancelled by resetState as well as disable and destroy. - An empty-feed regular pass records the camera pose, so later in-cadence frames do not keep scheduling trailing frames. - New renderGovernor.test.mjs: a parked camera schedules nothing, a stopped camera gets exactly one trailing frame and no self-wake, a forced pass requests its frame, destroy cancels the timer. - The CHANGELOG entry moves to changelog.d/871-vessels-idle-render.md; docs say the hold also covers vessels dimmed under a live target.
587 lines
20 KiB
JavaScript
587 lines
20 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import { t } from '../../i18n/index.js';
|
|
import {
|
|
vesselTypeCss,
|
|
vesselOverlayCohortLimit,
|
|
VESSEL_OVERLAY_SOURCE_ID,
|
|
applyVesselOverlayPolicy,
|
|
VESSEL_CARD_FADE_DISTANCE_M,
|
|
} from '../../data/vesselLabels.js';
|
|
import {
|
|
cameraPoseSignature,
|
|
screenProjectedRotation,
|
|
} from '../../data/iconOrientation.js';
|
|
import {
|
|
VESSEL_LIFT_M,
|
|
DEFAULT_RENDER_ROWS,
|
|
DEFAULT_ACTIVE_LABELS,
|
|
VISIBILITY_UPDATE_MS,
|
|
FOCUS_UPDATE_MS,
|
|
LABEL_GRID_PX,
|
|
CARD_MIN_SEP_PX,
|
|
} from './policy.js';
|
|
|
|
export function createRendering({
|
|
vesselState,
|
|
services,
|
|
parts: components,
|
|
layer,
|
|
options,
|
|
}) {
|
|
const { state } = vesselState;
|
|
let visualRecords = new WeakMap();
|
|
|
|
// The fallback preserves helper calls with externally supplied render records.
|
|
function getVisual(record) {
|
|
return visualRecords.get(record) || record;
|
|
}
|
|
|
|
function prepareRecordVisual(record) {
|
|
let visual = visualRecords.get(record);
|
|
if (!visual) {
|
|
visual = { billboard: record.billboard || null };
|
|
visualRecords.set(record, visual);
|
|
}
|
|
const heightM = components.queries.vesselDatumHeightM(
|
|
components.tracking.currentGeoidN(record.lat, record.lon),
|
|
VESSEL_LIFT_M,
|
|
);
|
|
visual.position = Cesium.Cartesian3.fromDegrees(
|
|
record.lon,
|
|
record.lat,
|
|
heightM,
|
|
);
|
|
visual.surfacePosition = Cesium.Cartesian3.fromDegrees(
|
|
record.lon,
|
|
record.lat,
|
|
0,
|
|
);
|
|
visual.normal = Cesium.Ellipsoid.WGS84.geodeticSurfaceNormal(
|
|
visual.position,
|
|
new Cesium.Cartesian3(),
|
|
);
|
|
return visual;
|
|
}
|
|
|
|
function resetRecordVisuals() {
|
|
visualRecords = new WeakMap();
|
|
}
|
|
|
|
const { registerSpriteCollection } = services.sprites;
|
|
const {
|
|
forgetSpriteFocus,
|
|
focusNowMs,
|
|
getFocusTarget,
|
|
focusPassIsNeeded,
|
|
advanceSpriteFocus,
|
|
focusAlphaNeedsWrite,
|
|
spriteFocusAtRest,
|
|
} = services.focus;
|
|
const {
|
|
holdContinuousRender,
|
|
releaseContinuousRender,
|
|
governorRequestRender,
|
|
} = services.render;
|
|
|
|
function renderRowLimit() {
|
|
const configured = Number(options.maxRows);
|
|
if (Number.isFinite(configured) && configured > 0) {
|
|
return Math.max(500, Math.min(50000, Math.round(configured)));
|
|
}
|
|
return DEFAULT_RENDER_ROWS;
|
|
}
|
|
|
|
function labelRowLimit() {
|
|
const configured = Number(options.maxLabels);
|
|
if (Number.isFinite(configured) && configured >= 0) {
|
|
return Math.max(0, Math.min(renderRowLimit(), Math.round(configured)));
|
|
}
|
|
return Math.min(DEFAULT_ACTIVE_LABELS, renderRowLimit());
|
|
}
|
|
|
|
function ensureCollections(viewer) {
|
|
if (!viewer || state.billboardCollection) return;
|
|
state.billboardCollection = new Cesium.BillboardCollection({
|
|
blendOption: Cesium.BlendOption.TRANSLUCENT,
|
|
});
|
|
state.billboardCollection.show = state.feed.enabled;
|
|
viewer.scene.primitives.add(state.billboardCollection);
|
|
registerSpriteCollection('ais', state.billboardCollection);
|
|
}
|
|
|
|
/**
|
|
* Create the billboard primitive for a freshly added vessel record. Map labels
|
|
* are canvas cards (vesselLabels.js) rebuilt by the declutter pass — no
|
|
* per-record label primitive exists anymore.
|
|
* @param {Object} record - Normalized vessel record.
|
|
* @param {Cesium.EllipsoidalOccluder|null} occluder - Horizon occluder for initial visibility.
|
|
*/
|
|
|
|
function addRecordPrimitives(record, occluder) {
|
|
const visible =
|
|
state.feed.enabled &&
|
|
isVisible(getVisual(record).surfacePosition, occluder);
|
|
getVisual(record).billboard = state.billboardCollection.add({
|
|
position: getVisual(record).position,
|
|
show: visible,
|
|
image: shipIcon(record, false),
|
|
scale: shipScale(record),
|
|
// Screen-projected rotation lands on the next visibility/rotation pass.
|
|
rotation: 0,
|
|
alignedAxis: Cesium.Cartesian3.ZERO,
|
|
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
|
|
verticalOrigin: Cesium.VerticalOrigin.CENTER,
|
|
// Locked height-datum principle #2: contacts are ALWAYS visible —
|
|
// depth-test-free sprites; the EllipsoidalOccluder handles the far side.
|
|
// (The tile sea mesh ≠ the geoid exactly, so a depth-tested chevron at
|
|
// the geoid still clips out behind local tide/mesh noise.)
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
id: record,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Remove a record's billboard primitive from its collection.
|
|
* @param {Object} record - Vessel record to tear down.
|
|
*/
|
|
|
|
function removeRecordPrimitives(record) {
|
|
if (!record) return;
|
|
if (getVisual(record).billboard && state.billboardCollection) {
|
|
forgetSpriteFocus(getVisual(record).billboard);
|
|
state.billboardCollection.remove(getVisual(record).billboard);
|
|
}
|
|
getVisual(record).billboard = null;
|
|
}
|
|
|
|
function shipScale(record) {
|
|
const speed = Number(record.speed || 0);
|
|
if (speed >= 18) return 0.78;
|
|
if (speed >= 8) return 0.68;
|
|
return 0.6;
|
|
}
|
|
|
|
/**
|
|
* Best available real-world direction of travel for a vessel, degrees
|
|
* clockwise from north (true heading preferred, course-over-ground fallback).
|
|
* Screen rotation is computed from this by the shared projected-rotation
|
|
* pass in updateVisibility — never directly from the compass value.
|
|
* @param {Object} record - Vessel record.
|
|
* @returns {number} Course in degrees (0 when unknown).
|
|
*/
|
|
|
|
function vesselCourseDeg(record) {
|
|
const direction = record.heading ?? record.course;
|
|
return Number.isFinite(direction) ? direction : 0;
|
|
}
|
|
|
|
/**
|
|
* Build (and cache) a chevron/delta-wing SVG data URL tinted for the vessel.
|
|
* The shape points north (up) so billboard rotation maps directly to heading.
|
|
* One icon is generated per color+variant and reused across all billboards.
|
|
* @param {Object} record - Vessel record (drives per-type tint).
|
|
* @param {boolean} selected - True for the white/brighter selected variant.
|
|
* @returns {string} SVG data URL.
|
|
*/
|
|
|
|
function shipIcon(record, selected) {
|
|
const cssColor = selected ? '#ffffff' : vesselTypeCss(record.type);
|
|
const key = `${cssColor}:${selected ? 'selected' : 'normal'}`;
|
|
if (vesselState.shipIconCache.has(key))
|
|
return vesselState.shipIconCache.get(key);
|
|
|
|
const stroke = selected ? 'rgba(6,26,32,0.95)' : 'rgba(4,18,24,0.9)';
|
|
const strokeWidth = selected ? 1.1 : 0.7;
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
<g transform="translate(16,16)">
|
|
<path d="M0,-14 L11,10 L4,7 L0,14 L-4,7 L-11,10 Z" fill="${cssColor}" stroke="${stroke}" stroke-width="${strokeWidth}" stroke-linejoin="round"/>
|
|
</g>
|
|
</svg>`;
|
|
const icon = 'data:image/svg+xml;base64,' + btoa(svg);
|
|
vesselState.shipIconCache.set(key, icon);
|
|
return icon;
|
|
}
|
|
|
|
// Vessels move only when a poll lands, so the layer does not hold the
|
|
// continuous render loop. Its passes run in preRender, which idle mode fires
|
|
// only for rendered frames: a forced pass (new data, selection) requests its
|
|
// frame, a frame that moved the camera but fell inside the 800 ms cadence
|
|
// leaves a trailing frame so the view the camera stopped on is always
|
|
// evaluated, and the continuous hold is kept only while focus emphasis is
|
|
// animating. The trailing frame is keyed to camera motion alone: the pass's
|
|
// own label publication requests a frame, and scheduling on that would loop.
|
|
function installRuntime(viewer) {
|
|
if (state.preRenderRemover || !viewer) return;
|
|
state.preRenderRemover = viewer.scene.preRender.addEventListener(() =>
|
|
updateVisibility(),
|
|
);
|
|
}
|
|
|
|
function scheduleTrailingVisibilityPass(now) {
|
|
if (state.trailingVisibilityTimer) return;
|
|
const waitMs = Math.max(
|
|
0,
|
|
VISIBILITY_UPDATE_MS - (now - state.lastVisibilityUpdate),
|
|
);
|
|
state.trailingVisibilityTimer = vesselState._aisRuntime.setTimeout(() => {
|
|
state.trailingVisibilityTimer = null;
|
|
if (state.feed.enabled) governorRequestRender('ais-vessels-visibility');
|
|
}, waitMs);
|
|
}
|
|
|
|
function cancelTrailingVisibilityPass() {
|
|
if (!state.trailingVisibilityTimer) return;
|
|
vesselState._aisRuntime.clearTimeout(state.trailingVisibilityTimer);
|
|
state.trailingVisibilityTimer = null;
|
|
}
|
|
|
|
function updateVisibility(force = false) {
|
|
if (!state.feed.enabled) return;
|
|
if (force) governorRequestRender('ais-vessels-update');
|
|
const now = focusNowMs(performance.now());
|
|
const focusTarget = getFocusTarget();
|
|
const regularPass =
|
|
force || now - state.lastVisibilityUpdate >= VISIBILITY_UPDATE_MS;
|
|
const focusPass =
|
|
focusPassIsNeeded(focusTarget, state.activeFocusCount) &&
|
|
(force || now - state.lastFocusUpdate >= FOCUS_UPDATE_MS);
|
|
if (
|
|
!regularPass &&
|
|
state.viewer?.camera &&
|
|
cameraPoseSignature(state.viewer.camera) !== vesselState._lastCamPoseSig
|
|
)
|
|
scheduleTrailingVisibilityPass(now);
|
|
if (!regularPass && !focusPass) return;
|
|
if (regularPass) state.lastVisibilityUpdate = now;
|
|
if (focusPass) state.lastFocusUpdate = now;
|
|
if (!state.records.all.length) {
|
|
// No records — flush any lingering card entries (vanished-feed case).
|
|
// Still record the pose this pass evaluated, or every later frame that
|
|
// falls inside the cadence would schedule another trailing frame.
|
|
if (regularPass) {
|
|
if (state.viewer?.camera)
|
|
vesselState._lastCamPoseSig = cameraPoseSignature(
|
|
state.viewer.camera,
|
|
);
|
|
updateClusteredLabels([]);
|
|
}
|
|
if (focusPass) {
|
|
state.activeFocusCount = 0;
|
|
releaseContinuousRender('ais-vessels');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const scene = state.viewer?.scene;
|
|
const camera = state.viewer?.camera;
|
|
if (regularPass) {
|
|
// Candidate construction stays on the original 800 ms selector cadence.
|
|
// The 80 ms focus-only pass below never allocates label candidates.
|
|
const poseSig = camera ? cameraPoseSignature(camera) : '';
|
|
const doRotations = force || poseSig !== vesselState._lastCamPoseSig;
|
|
if (doRotations) vesselState._lastCamPoseSig = poseSig;
|
|
const occluder = makeOccluder();
|
|
const labelCandidates = [];
|
|
for (const record of state.records.all) {
|
|
const visual = getVisual(record);
|
|
const visible = isVisible(visual.surfacePosition, occluder);
|
|
if (visual.billboard) {
|
|
visual.billboard.show = visible;
|
|
if (visible && doRotations && scene) {
|
|
const rot = screenProjectedRotation(
|
|
scene,
|
|
visual.position,
|
|
vesselCourseDeg(record),
|
|
visual.billboard.rotation,
|
|
);
|
|
if (
|
|
rot !== null &&
|
|
Math.abs(rot - visual.billboard.rotation) > 0.002
|
|
) {
|
|
visual.billboard.rotation = rot;
|
|
}
|
|
}
|
|
}
|
|
if (visible) labelCandidates.push(record);
|
|
}
|
|
updateClusteredLabels(labelCandidates);
|
|
}
|
|
if (focusPass && scene && camera) {
|
|
const result = applyVesselFocusDeemphasis({
|
|
records: state.records.all,
|
|
target: focusTarget,
|
|
previousActiveCount: state.activeFocusCount,
|
|
nowMs: now,
|
|
screenPositionFor: (position) =>
|
|
Cesium.SceneTransforms.worldToWindowCoordinates(
|
|
scene,
|
|
position,
|
|
vesselState._scratchFocusScreen,
|
|
),
|
|
cameraDistanceFor: (position) =>
|
|
Cesium.Cartesian3.distance(camera.positionWC, position),
|
|
});
|
|
state.activeFocusCount = result.activeCount;
|
|
if (result.activeCount > 0 || result.transitioning)
|
|
holdContinuousRender('ais-vessels');
|
|
else releaseContinuousRender('ais-vessels');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply focus alpha to vessel sprites. Kept as a production wire seam so the
|
|
* animation/deadband contract can be tested without constructing WebGL.
|
|
* @param {object} input
|
|
* @returns {{writes:number,transitioning:boolean,activeCount:number,ran:boolean}}
|
|
*/
|
|
|
|
function applyVesselFocusDeemphasis({
|
|
records,
|
|
target,
|
|
previousActiveCount = 0,
|
|
nowMs,
|
|
screenPositionFor,
|
|
cameraDistanceFor,
|
|
params,
|
|
}) {
|
|
if (!focusPassIsNeeded(target, previousActiveCount)) {
|
|
return { writes: 0, transitioning: false, activeCount: 0, ran: false };
|
|
}
|
|
let writes = 0;
|
|
let transitioning = false;
|
|
let activeCount = 0;
|
|
for (const record of records || []) {
|
|
const visual = getVisual(record);
|
|
const bb = visual?.billboard;
|
|
const position = bb?.position || visual?.position;
|
|
if (!bb || !position) continue;
|
|
// Most of a worldwide feed sits behind the horizon. A hidden sprite with
|
|
// no pending focus state and full alpha would advance to exactly what it
|
|
// already is, so skip it instead of paying its distance and state work
|
|
// every 80 ms while anything is tracked.
|
|
if (
|
|
bb.show === false &&
|
|
spriteFocusAtRest(bb) &&
|
|
!focusAlphaNeedsWrite(bb.color?.alpha, 1, params)
|
|
)
|
|
continue;
|
|
const focus = advanceSpriteFocus(bb, {
|
|
// Hidden/far-side sprites still finish any pending release so the active
|
|
// count remains truthful and they cannot reappear with stale dim alpha.
|
|
screenPosition: bb.show === false ? null : screenPositionFor(position),
|
|
cameraDistance: cameraDistanceFor(position),
|
|
nowMs,
|
|
target,
|
|
params,
|
|
// Vessel artwork is 32 px before billboard scale. Including the
|
|
// ambient chevron's own rendered extent prevents edge-overlap misses.
|
|
spriteHalfWidthPx: (bb.width || 32) * (bb.scale || 1) * 0.5,
|
|
spriteHalfHeightPx: (bb.height || 32) * (bb.scale || 1) * 0.5,
|
|
});
|
|
transitioning ||= focus.transitioning;
|
|
if (focus.active) activeCount += 1;
|
|
if (focusAlphaNeedsWrite(bb.color?.alpha, focus.factor, params)) {
|
|
// Narrow always-visible amendment: the ship chevron remains present at
|
|
// the non-zero floor while it competes with the tracked target. Preserve
|
|
// the billboard's existing base RGB, matching the other layer patterns,
|
|
// rather than repainting every chevron from a hard-coded WHITE base.
|
|
const baseColor = bb.color || Cesium.Color.WHITE;
|
|
bb.color = baseColor.withAlpha(focus.factor);
|
|
writes += 1;
|
|
}
|
|
}
|
|
return { writes, transitioning, activeCount, ran: true };
|
|
}
|
|
|
|
function makeOccluder() {
|
|
const cameraPosition = state.viewer?.camera?.positionWC;
|
|
if (!cameraPosition) return null;
|
|
return new Cesium.EllipsoidalOccluder(
|
|
Cesium.Ellipsoid.WGS84,
|
|
cameraPosition,
|
|
);
|
|
}
|
|
|
|
function isVisible(surfacePosition, occluder) {
|
|
if (!surfacePosition || !occluder) return true;
|
|
return occluder.isPointVisible(surfacePosition);
|
|
}
|
|
|
|
/**
|
|
* Source selector for the shared world-overlay pipeline: the
|
|
* grid declutter picks which vessels get cards — one winner per screen-space
|
|
* grid cell, priority-ranked, capped — and publishes presentation entries to
|
|
* the host, which owns projection, final placement, fade and paint. Runs on the
|
|
* throttled visibility pass and forced refreshes, never per frame. The
|
|
* selected vessel always gets its full-detail card, even when horizon-culled
|
|
* from the ambient candidates; its protected entry bypasses ambient quotas.
|
|
* @param {Array<Object>} records - Horizon-visible vessel records.
|
|
*/
|
|
|
|
function updateClusteredLabels(records) {
|
|
const viewer = state.viewer;
|
|
const scene = viewer?.scene;
|
|
const selected = state.selectedRecord;
|
|
const entries = selected
|
|
? [components.cards.buildSelectedVesselCard(selected)]
|
|
: [];
|
|
const maxLabels = labelRowLimit();
|
|
|
|
if (!scene || !records.length || maxLabels <= 0) {
|
|
state.activeLabelCount = entries.length;
|
|
publishVesselOverlayEntries(entries);
|
|
return;
|
|
}
|
|
|
|
const cells = new Map();
|
|
for (const record of records) {
|
|
const visual = getVisual(record);
|
|
if (record === selected) continue;
|
|
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
|
scene,
|
|
visual.position,
|
|
);
|
|
if (!screen) continue;
|
|
const key = `${Math.floor(screen.x / LABEL_GRID_PX)}:${Math.floor(screen.y / LABEL_GRID_PX)}`;
|
|
const candidate = {
|
|
record,
|
|
score: labelPriority(record, selected),
|
|
x: screen.x,
|
|
y: screen.y,
|
|
};
|
|
const existing = cells.get(key);
|
|
if (!existing || candidate.score > existing.score) {
|
|
cells.set(key, candidate);
|
|
}
|
|
}
|
|
|
|
// Greedy min-separation pass over the priority-ranked cell winners: the
|
|
// selected card's anchor seeds the accepted set so ambient cards keep clear.
|
|
const accepted = [];
|
|
if (selected) {
|
|
const screen = Cesium.SceneTransforms.worldToWindowCoordinates(
|
|
scene,
|
|
getVisual(selected).billboard?.position || getVisual(selected).position,
|
|
);
|
|
if (screen) accepted.push({ x: screen.x, y: screen.y });
|
|
}
|
|
const ranked = [...cells.values()].sort((a, b) => b.score - a.score);
|
|
for (const candidate of ranked) {
|
|
if (entries.length >= maxLabels) break;
|
|
if (
|
|
!components.cards.cardScreenSeparated(
|
|
accepted,
|
|
candidate,
|
|
CARD_MIN_SEP_PX,
|
|
)
|
|
)
|
|
continue;
|
|
accepted.push({ x: candidate.x, y: candidate.y });
|
|
entries.push(components.cards.buildVesselCard(candidate.record));
|
|
}
|
|
state.activeLabelCount = entries.length;
|
|
publishVesselOverlayEntries(entries);
|
|
}
|
|
|
|
/**
|
|
* Publish a complete, bounded source snapshot to the shared host. The source
|
|
* selector remains authoritative for the 118 px grid and 150 px separation;
|
|
* the host then composes this demand with sibling ambient-card sources.
|
|
* @param {Object[]} entries Formatted vessel card entries.
|
|
*/
|
|
|
|
function publishVesselOverlayEntries(entries) {
|
|
const canvas = state.viewer?.scene?.canvas || state.viewer?.canvas;
|
|
const width = Number(canvas?.clientWidth) || 0;
|
|
const height = Number(canvas?.clientHeight) || 0;
|
|
const ambientLimit = vesselOverlayCohortLimit(
|
|
width,
|
|
height,
|
|
labelRowLimit(),
|
|
);
|
|
vesselState._vesselOverlayHost.setEntries(
|
|
VESSEL_OVERLAY_SOURCE_ID,
|
|
entries.map((entry) => {
|
|
const card = applyVesselOverlayPolicy(
|
|
entry,
|
|
VESSEL_CARD_FADE_DISTANCE_M,
|
|
);
|
|
if (!card.interactive) return card;
|
|
const mmsi = String(card.id || '').startsWith('vessel:')
|
|
? card.id.slice('vessel:'.length)
|
|
: '';
|
|
return {
|
|
...card,
|
|
accessibilityLabel: t('Focus vessel {name}, MMSI {mmsi}', {
|
|
name: card.title,
|
|
mmsi,
|
|
}),
|
|
activate: () => {
|
|
const record = state.records.byMmsi.get(mmsi);
|
|
if (!record) return false;
|
|
components.selection.selectAndFocusVessel(record);
|
|
return true;
|
|
},
|
|
};
|
|
}),
|
|
{
|
|
cohortLimit: Math.max(1, ambientLimit),
|
|
collisionCapacity: ambientLimit,
|
|
moving: false,
|
|
},
|
|
);
|
|
}
|
|
|
|
function labelPriority(record, selected) {
|
|
if (record === selected) return 100000;
|
|
let score = 0;
|
|
if (hasUsefulName(record)) score += 1000;
|
|
if (record.speed !== null)
|
|
score += Math.min(400, Math.max(0, record.speed) * 20);
|
|
if (record.heading !== null || record.course !== null) score += 80;
|
|
if (record.type) score += 40;
|
|
return score;
|
|
}
|
|
|
|
function hasUsefulName(record) {
|
|
const text = String(record.name || '').trim();
|
|
return Boolean(
|
|
text &&
|
|
text !== 'VESSEL' &&
|
|
!/^MMSI\s*\d+$/i.test(text) &&
|
|
text !== record.mmsi,
|
|
);
|
|
}
|
|
|
|
function setVisible(show) {
|
|
if (state.billboardCollection) {
|
|
state.billboardCollection.show = show;
|
|
}
|
|
vesselState._vesselOverlayHost.setVisible(VESSEL_OVERLAY_SOURCE_ID, show);
|
|
}
|
|
return {
|
|
getVisual,
|
|
prepareRecordVisual,
|
|
resetRecordVisuals,
|
|
renderRowLimit,
|
|
labelRowLimit,
|
|
ensureCollections,
|
|
addRecordPrimitives,
|
|
removeRecordPrimitives,
|
|
shipScale,
|
|
vesselCourseDeg,
|
|
shipIcon,
|
|
installRuntime,
|
|
updateVisibility,
|
|
cancelTrailingVisibilityPass,
|
|
applyVesselFocusDeemphasis,
|
|
makeOccluder,
|
|
isVisible,
|
|
updateClusteredLabels,
|
|
publishVesselOverlayEntries,
|
|
labelPriority,
|
|
hasUsefulName,
|
|
setVisible,
|
|
};
|
|
}
|