release: bump version to 0.48.0
This commit is contained in:
7
frontend/public/earth/js/controls.js
vendored
7
frontend/public/earth/js/controls.js
vendored
@@ -271,7 +271,12 @@ function closeTransientMobileOverlays({ except = null } = {}) {
|
||||
setMobileDrawerOpen("layer-toggles", false);
|
||||
}
|
||||
|
||||
if (except !== "media" && except !== "search" && isTVPanelVisible()) {
|
||||
if (
|
||||
except !== "media"
|
||||
&& except !== "search"
|
||||
&& except !== "settings"
|
||||
&& isTVPanelVisible()
|
||||
) {
|
||||
setTVPanelVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,30 @@ let typewriterToken = 0;
|
||||
let pendingMobileDetailState = null;
|
||||
let mobileDetailsListenerBound = false;
|
||||
let renderedMobileDetailKey = null;
|
||||
const IDENTIFIER_FIELD_KEYS = new Set([
|
||||
'mmsi',
|
||||
'mmsi_display',
|
||||
'imo',
|
||||
'imo_display',
|
||||
'callsign',
|
||||
]);
|
||||
const MAX_VESSEL_MEDIA_TILES = 4;
|
||||
|
||||
function formatInfoCardValue(field, rawValue) {
|
||||
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
||||
return '-';
|
||||
}
|
||||
let value = rawValue;
|
||||
if (IDENTIFIER_FIELD_KEYS.has(field.key)) {
|
||||
value = String(value);
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
if (field.unit && value !== '-') {
|
||||
value = value + ' ' + field.unit;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getNewsSummaryText(data) {
|
||||
return (data?.summary || data?.title || '').trim() || '暂无摘要';
|
||||
@@ -109,13 +133,7 @@ function renderMobileDetailContent(type, config, data) {
|
||||
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
if (field.unit && value !== '-') value = value + ' ' + field.unit;
|
||||
const value = formatInfoCardValue(field, data[field.key]);
|
||||
html += `
|
||||
<div class="earth-mobile-detail-row">
|
||||
<span class="earth-mobile-detail-row-label">${field.label}</span>
|
||||
@@ -166,29 +184,95 @@ function ensureMobileDetailsListener() {
|
||||
function renderDefaultCardContent(content, config, data) {
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
|
||||
if (field.unit && value !== '-') {
|
||||
value = value + ' ' + field.unit;
|
||||
}
|
||||
|
||||
const value = formatInfoCardValue(field, data[field.key]);
|
||||
const sourceLabel = getFieldSourceLabel(data, field.key);
|
||||
html += `
|
||||
<div class="info-card-property">
|
||||
<span class="info-card-label">${field.label}</span>
|
||||
<span class="info-card-value">${value}</span>
|
||||
<span class="info-card-value">${value}${sourceLabel}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (config.className === 'vessel') {
|
||||
html += renderVesselEnrichmentSection(data?.enrichment);
|
||||
}
|
||||
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
function getFieldSourceLabel(data, fieldKey) {
|
||||
const sources = data && typeof data === 'object' ? data.field_sources : null;
|
||||
if (!sources || typeof sources !== 'object') return '';
|
||||
const source = sources[fieldKey];
|
||||
if (!source) return '';
|
||||
return ` <span class="info-card-source-tag" title="字段来源">${source}</span>`;
|
||||
}
|
||||
|
||||
function renderVesselEnrichmentSection(enrichment) {
|
||||
if (!enrichment || typeof enrichment !== 'object') return '';
|
||||
const profile = enrichment.profile;
|
||||
const media = enrichment.media;
|
||||
if (!profile && !media) {
|
||||
return `
|
||||
<div class="info-card-enrichment info-card-enrichment--empty">
|
||||
<div class="info-card-enrichment-title">船舶资料</div>
|
||||
<div class="info-card-enrichment-status">资料缓存中</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
let inner = '';
|
||||
if (profile?.payload && typeof profile.payload === 'object') {
|
||||
inner += renderEnrichmentPayloadRows(profile.payload);
|
||||
inner += renderEnrichmentMeta('资料', profile);
|
||||
}
|
||||
if (media?.payload && typeof media.payload === 'object') {
|
||||
if (Array.isArray(media.payload.images) && media.payload.images.length > 0) {
|
||||
const tiles = media.payload.images
|
||||
.slice(0, MAX_VESSEL_MEDIA_TILES)
|
||||
.map((url) => `<img class="info-card-enrichment-thumb" src="${String(url)}" alt="vessel media" />`)
|
||||
.join('');
|
||||
inner += `<div class="info-card-enrichment-media">${tiles}</div>`;
|
||||
}
|
||||
inner += renderEnrichmentMeta('媒体', media);
|
||||
}
|
||||
if (!inner) {
|
||||
inner = '<div class="info-card-enrichment-status">资料缓存中</div>';
|
||||
}
|
||||
return `
|
||||
<div class="info-card-enrichment">
|
||||
<div class="info-card-enrichment-title">船舶资料</div>
|
||||
${inner}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEnrichmentPayloadRows(payload) {
|
||||
let rows = '';
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
if (value === null || value === undefined || value === '') continue;
|
||||
if (typeof value === 'object') continue;
|
||||
rows += `
|
||||
<div class="info-card-property">
|
||||
<span class="info-card-label">${key}</span>
|
||||
<span class="info-card-value">${String(value)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderEnrichmentMeta(label, record) {
|
||||
const parts = [];
|
||||
if (record.source) parts.push(`来源 ${record.source}`);
|
||||
if (record.fetched_at) parts.push(`更新 ${record.fetched_at}`);
|
||||
if (record.confidence !== null && record.confidence !== undefined) {
|
||||
parts.push(`置信 ${Number(record.confidence).toFixed(2)}`);
|
||||
}
|
||||
if (!parts.length) return '';
|
||||
return `<div class="info-card-enrichment-meta">${label}:${parts.join(' · ')}</div>`;
|
||||
}
|
||||
|
||||
// ── Mobile popup ─────────────────────────────────────────────
|
||||
|
||||
function getMobilePopupTitle(type, data) {
|
||||
|
||||
@@ -98,6 +98,8 @@ function registerBuiltinLayerStartupTasks() {
|
||||
|
||||
function registerVesselStartupTask() {
|
||||
registerLayerStartupTask("vessels", (context) => async (layer) => {
|
||||
if (!context.getShowVessels()) return;
|
||||
|
||||
context.setLoadingMessage(
|
||||
resolveStartupMessage(layer, "load", "正在加载船只..."),
|
||||
);
|
||||
|
||||
@@ -180,9 +180,12 @@ import {
|
||||
getVesselLegendItems,
|
||||
getVesselMarkers,
|
||||
getVesselPointerIntersections as getVesselIconPointerIntersections,
|
||||
getVesselRealtimeStats,
|
||||
loadVessels,
|
||||
setVesselMarkerState,
|
||||
showVesselTrack,
|
||||
startVesselRealtime,
|
||||
stopVesselRealtime,
|
||||
toggleVessels,
|
||||
updateVesselVisualState,
|
||||
} from "./vessels.js";
|
||||
@@ -1489,6 +1492,13 @@ async function loadEarthStatsSummary({ shouldApply = () => true } = {}) {
|
||||
satelliteCount: toCount(stats.satellite_count),
|
||||
computeCenterCount: toCount(stats.compute_center_count),
|
||||
vesselCount: toCount(stats.vessel_count),
|
||||
vesselRawUniqueMmsi: toCount(stats.vessel_raw_unique_mmsi),
|
||||
vesselLegacyUniqueMmsi: toCount(stats.vessel_legacy_unique_mmsi),
|
||||
aisstreamConnectionState: stats.aisstream_connection_state || null,
|
||||
aisstreamLastSeenAt: stats.aisstream_last_seen_at || null,
|
||||
aisstreamLagSeconds: Number.isFinite(Number(stats.aisstream_lag_seconds))
|
||||
? Number(stats.aisstream_lag_seconds)
|
||||
: null,
|
||||
bgpEventCount: toCount(stats.bgp_event_count),
|
||||
bgpIncidentCount: toCount(stats.bgp_incident_count),
|
||||
bgpAnomalyCount: toCount(stats.bgp_anomaly_count),
|
||||
@@ -2154,6 +2164,36 @@ function updateVesselHud(result = {}) {
|
||||
setEarthStatValue("vessel-count", `${count} 艘`);
|
||||
}
|
||||
|
||||
function formatRelativeTime(value) {
|
||||
const date = value instanceof Date ? value : value ? new Date(value) : null;
|
||||
if (!date || Number.isNaN(date.getTime())) return null;
|
||||
const elapsedSeconds = Math.max(0, Math.round((Date.now() - date.getTime()) / 1000));
|
||||
if (elapsedSeconds < 5) return "刚刚";
|
||||
if (elapsedSeconds < 60) return `${elapsedSeconds} 秒前`;
|
||||
const elapsedMinutes = Math.round(elapsedSeconds / 60);
|
||||
if (elapsedMinutes < 60) return `${elapsedMinutes} 分钟前`;
|
||||
return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function formatVesselLiveSummary() {
|
||||
const stream = getVesselRealtimeStats();
|
||||
if (stream.connected) {
|
||||
const lastUpdate = formatRelativeTime(stream.lastUpdateAt);
|
||||
if (stream.updates > 0) {
|
||||
return `AISStream 实时已连接 · ${stream.updates} 次更新${lastUpdate ? ` · ${lastUpdate}` : ""}`;
|
||||
}
|
||||
return "AISStream 实时已连接 · 等待首批更新";
|
||||
}
|
||||
const state = earthStatsSummary?.aisstreamConnectionState;
|
||||
if (state === "connected") {
|
||||
const lastSeen = formatRelativeTime(earthStatsSummary?.aisstreamLastSeenAt);
|
||||
return `AISStream 后台已连接${lastSeen ? ` · 最近 ${lastSeen}` : ""}`;
|
||||
}
|
||||
if (state === "reconnecting") return "AISStream 正在重连";
|
||||
if (state === "connecting") return "AISStream 正在连接";
|
||||
return "AISStream 未连接";
|
||||
}
|
||||
|
||||
function updateVesselToggleUi(enabled, vesselCount = getVesselCount()) {
|
||||
const vesselBtn = document.getElementById("toggle-vessels");
|
||||
if (vesselBtn) {
|
||||
@@ -2164,6 +2204,7 @@ function updateVesselToggleUi(enabled, vesselCount = getVesselCount()) {
|
||||
});
|
||||
}
|
||||
setEarthStatValue("vessel-count", `${vesselCount || 0} 艘`);
|
||||
setEarthStatValue("vessel-live-summary", formatVesselLiveSummary());
|
||||
}
|
||||
|
||||
function updateCableToggleUi(enabled) {
|
||||
@@ -2292,6 +2333,12 @@ async function ensureVesselsEnabled() {
|
||||
vesselsEnabled = true;
|
||||
const result = await loadVessels(scene, earth);
|
||||
toggleVessels(true);
|
||||
startVesselRealtime(earth, {
|
||||
onUpdate: ({ totalCount }) => {
|
||||
updateVesselToggleUi(true, totalCount);
|
||||
updateStatsSummary();
|
||||
},
|
||||
});
|
||||
updateVesselToggleUi(true, result.totalCount);
|
||||
setLegendItems("vessels", getVesselLegendItems());
|
||||
refreshLegend();
|
||||
@@ -2300,6 +2347,7 @@ async function ensureVesselsEnabled() {
|
||||
|
||||
function disableVessels() {
|
||||
vesselsEnabled = false;
|
||||
stopVesselRealtime();
|
||||
toggleVessels(false);
|
||||
clearVesselSelection();
|
||||
updateVesselToggleUi(false, 0);
|
||||
@@ -2334,6 +2382,7 @@ function updateStatsSummary() {
|
||||
landingPointCount: `${landingPointCount}个`,
|
||||
satelliteCount: `${satelliteCount} 颗`,
|
||||
vesselCount: `${vesselCount} 艘`,
|
||||
vesselLiveSummary: formatVesselLiveSummary(),
|
||||
computeCenterCount: `${computeCenterCount} 个`,
|
||||
bgpAnomalyCount: `${bgpEventCount} 起`,
|
||||
bgpCollectorCount: `${bgpCollectorCount} 个`,
|
||||
|
||||
@@ -218,6 +218,7 @@ export function updateEarthStats(stats) {
|
||||
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
|
||||
}
|
||||
if (has("vesselCount")) setEarthStatValue("vessel-count", String(stats.vesselCount || 0));
|
||||
if (has("vesselLiveSummary")) setEarthStatValue("vessel-live-summary", stats.vesselLiveSummary || "-");
|
||||
if (has("bgpAnomalyCount")) setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
|
||||
if (has("bgpCollectorCount")) {
|
||||
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
|
||||
|
||||
@@ -6,6 +6,15 @@ import { latLonToVector3 } from "./utils.js";
|
||||
|
||||
let showVessels = false;
|
||||
let activeTrackLine = null;
|
||||
let vesselStreamSocket = null;
|
||||
let vesselStreamReconnectTimer = null;
|
||||
let vesselDataByKey = new Map();
|
||||
let vesselRealtimeStats = {
|
||||
connected: false,
|
||||
updates: 0,
|
||||
lastUpdateAt: null,
|
||||
lastBatchSize: 0,
|
||||
};
|
||||
|
||||
const VESSEL_RENDER_ORDER = 4.4;
|
||||
const VESSEL_POINT_SIZE = 34;
|
||||
@@ -13,6 +22,95 @@ const VESSEL_ATLAS_CELL_SIZE = 128;
|
||||
const VESSEL_COURSE_BINS = 32;
|
||||
const VESSEL_TRACK_ENDPOINT_EPSILON = 0.001;
|
||||
|
||||
function getVesselDedupeKey(feature, markerData) {
|
||||
const props = feature?.properties || {};
|
||||
const mmsi = props.mmsi ?? feature?.id ?? markerData?.mmsi;
|
||||
if (mmsi !== undefined && mmsi !== null && String(mmsi).trim() !== "") {
|
||||
return `mmsi:${String(mmsi).trim()}`;
|
||||
}
|
||||
return [
|
||||
"position",
|
||||
Number(markerData.latitude).toFixed(5),
|
||||
Number(markerData.longitude).toFixed(5),
|
||||
String(props.name || markerData.name || "").trim().toLowerCase(),
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function dedupeVesselFeatures(features) {
|
||||
const seen = new Set();
|
||||
const markerData = [];
|
||||
features.forEach((feature) => {
|
||||
const marker = buildVesselMarkerData(feature);
|
||||
if (!marker) return;
|
||||
const key = getVesselDedupeKey(feature, marker);
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
markerData.push(marker);
|
||||
});
|
||||
return markerData;
|
||||
}
|
||||
|
||||
function markerDataToDedupeKey(item) {
|
||||
const mmsi = item?.mmsi;
|
||||
if (mmsi !== undefined && mmsi !== null && String(mmsi).trim() !== "") {
|
||||
return `mmsi:${String(mmsi).trim()}`;
|
||||
}
|
||||
return [
|
||||
"position",
|
||||
Number(item.latitude).toFixed(5),
|
||||
Number(item.longitude).toFixed(5),
|
||||
String(item.name || "").trim().toLowerCase(),
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function buildVesselFeatureFromDelta(item) {
|
||||
const lat = Number(item?.lat ?? item?.latitude);
|
||||
const lon = Number(item?.lon ?? item?.lng ?? item?.longitude);
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
||||
return {
|
||||
type: "Feature",
|
||||
id: item.mmsi,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [lon, lat],
|
||||
},
|
||||
properties: {
|
||||
...item,
|
||||
mmsi: item.mmsi,
|
||||
mmsi_display: item.mmsi_display || (item.mmsi !== undefined && item.mmsi !== null ? String(item.mmsi) : undefined),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rebuildVesselLayerFromCache(earth) {
|
||||
if (!earth) return;
|
||||
vesselIconLayer.setData(Array.from(vesselDataByKey.values()));
|
||||
vesselIconLayer.attach(earth);
|
||||
vesselIconLayer.setVisible(showVessels);
|
||||
}
|
||||
|
||||
function applyVesselDeltas(earth, vessels = []) {
|
||||
let changed = false;
|
||||
vessels.forEach((item) => {
|
||||
const feature = buildVesselFeatureFromDelta(item);
|
||||
if (!feature) return;
|
||||
const marker = buildVesselMarkerData(feature);
|
||||
if (!marker) return;
|
||||
vesselDataByKey.set(markerDataToDedupeKey(marker), marker);
|
||||
changed = true;
|
||||
});
|
||||
if (changed) {
|
||||
rebuildVesselLayerFromCache(earth);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
function getVesselStreamUrl() {
|
||||
if (typeof window === "undefined") return "ws://localhost:8000/ws";
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
return `${protocol}//${window.location.host}/ws`;
|
||||
}
|
||||
|
||||
function normalizeVesselType(value, code) {
|
||||
const type = String(value || "").trim().toLowerCase();
|
||||
const numericCode = Number(code);
|
||||
@@ -67,9 +165,14 @@ function buildVesselMarkerData(feature) {
|
||||
const navStatus = Number(props.nav_status);
|
||||
const speed = Number(props.sog);
|
||||
const anchored = navStatus === 1 || navStatus === 5 || (Number.isFinite(speed) && speed < 0.5);
|
||||
const mmsiString = props.mmsi !== undefined && props.mmsi !== null && String(props.mmsi).trim() !== ""
|
||||
? String(props.mmsi)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...props,
|
||||
mmsi: mmsiString,
|
||||
mmsi_display: props.mmsi_display ? String(props.mmsi_display) : mmsiString,
|
||||
latitude,
|
||||
longitude,
|
||||
type,
|
||||
@@ -163,6 +266,10 @@ export function getVesselCount() {
|
||||
return vesselIconLayer.getCount();
|
||||
}
|
||||
|
||||
export function getVesselRealtimeStats() {
|
||||
return { ...vesselRealtimeStats };
|
||||
}
|
||||
|
||||
export function getShowVessels() {
|
||||
return showVessels;
|
||||
}
|
||||
@@ -199,6 +306,7 @@ export function getVesselPointerIntersections(options) {
|
||||
|
||||
export function clearVesselData(earth) {
|
||||
clearVesselSelection();
|
||||
vesselDataByKey.clear();
|
||||
vesselIconLayer.clearData(earth);
|
||||
}
|
||||
|
||||
@@ -216,12 +324,11 @@ export async function loadVessels(_scene, earth, options = {}) {
|
||||
const features = Array.isArray(payload?.features) ? payload.features : [];
|
||||
|
||||
clearVesselData(earth);
|
||||
let markerData = features
|
||||
.map((feature) => buildVesselMarkerData(feature))
|
||||
.filter(Boolean);
|
||||
let markerData = dedupeVesselFeatures(features);
|
||||
if (Number.isFinite(requestedLimit) && requestedLimit > 0) {
|
||||
markerData = markerData.slice(0, requestedLimit);
|
||||
}
|
||||
vesselDataByKey = new Map(markerData.map((item) => [markerDataToDedupeKey(item), item]));
|
||||
vesselIconLayer.setData(markerData);
|
||||
|
||||
vesselIconLayer.attach(earth);
|
||||
@@ -233,6 +340,97 @@ export async function loadVessels(_scene, earth, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function startVesselRealtime(earth, { onUpdate } = {}) {
|
||||
if (vesselStreamSocket || typeof WebSocket === "undefined") return;
|
||||
const connect = () => {
|
||||
if (!showVessels || vesselStreamSocket) return;
|
||||
const socket = new WebSocket(getVesselStreamUrl());
|
||||
vesselStreamSocket = socket;
|
||||
socket.onopen = () => {
|
||||
vesselRealtimeStats = {
|
||||
...vesselRealtimeStats,
|
||||
connected: true,
|
||||
};
|
||||
onUpdate?.({ totalCount: getVesselCount(), stream: getVesselRealtimeStats() });
|
||||
socket.send(JSON.stringify({ type: "subscribe", data: { channels: ["vessels"] } }));
|
||||
};
|
||||
socket.onmessage = (event) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (message.type === "heartbeat" && message.data?.action === "ping") {
|
||||
socket.send(JSON.stringify({ type: "heartbeat" }));
|
||||
return;
|
||||
}
|
||||
if (message.type !== "data_frame" || message.channel !== "vessels") return;
|
||||
const payload = message.payload || {};
|
||||
if (payload.action === "reload") {
|
||||
loadVessels(null, earth)
|
||||
.then((result) => {
|
||||
vesselRealtimeStats = {
|
||||
...vesselRealtimeStats,
|
||||
connected: true,
|
||||
updates: vesselRealtimeStats.updates + 1,
|
||||
lastUpdateAt: new Date(),
|
||||
lastBatchSize: 0,
|
||||
};
|
||||
onUpdate?.({ totalCount: result?.totalCount ?? getVesselCount(), payload, stream: getVesselRealtimeStats() });
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (payload.action !== "upsert" || !Array.isArray(payload.vessels)) return;
|
||||
if (applyVesselDeltas(earth, payload.vessels)) {
|
||||
vesselRealtimeStats = {
|
||||
connected: true,
|
||||
updates: vesselRealtimeStats.updates + 1,
|
||||
lastUpdateAt: new Date(),
|
||||
lastBatchSize: payload.vessels.length,
|
||||
};
|
||||
onUpdate?.({ totalCount: getVesselCount(), payload, stream: getVesselRealtimeStats() });
|
||||
}
|
||||
};
|
||||
socket.onclose = () => {
|
||||
if (vesselStreamSocket === socket) {
|
||||
vesselStreamSocket = null;
|
||||
}
|
||||
vesselRealtimeStats = {
|
||||
...vesselRealtimeStats,
|
||||
connected: false,
|
||||
};
|
||||
onUpdate?.({ totalCount: getVesselCount(), stream: getVesselRealtimeStats() });
|
||||
if (showVessels) {
|
||||
vesselStreamReconnectTimer = window.setTimeout(connect, 3000);
|
||||
}
|
||||
};
|
||||
socket.onerror = () => {
|
||||
socket.close();
|
||||
};
|
||||
};
|
||||
connect();
|
||||
}
|
||||
|
||||
export function stopVesselRealtime() {
|
||||
if (vesselStreamReconnectTimer) {
|
||||
window.clearTimeout(vesselStreamReconnectTimer);
|
||||
vesselStreamReconnectTimer = null;
|
||||
}
|
||||
if (vesselStreamSocket) {
|
||||
const socket = vesselStreamSocket;
|
||||
vesselStreamSocket = null;
|
||||
socket.close();
|
||||
}
|
||||
vesselRealtimeStats = {
|
||||
connected: false,
|
||||
updates: 0,
|
||||
lastUpdateAt: null,
|
||||
lastBatchSize: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function showVesselTrack(marker, earth) {
|
||||
clearVesselTrack();
|
||||
if (!marker?.userData?.mmsi || !earth) return null;
|
||||
|
||||
Reference in New Issue
Block a user