import { forActiveRig, isActiveRigDecode } from "./chunk-S57W63QN.js"; import { hostCore, hostState } from "./chunk-KL66PICH.js"; // src/plugins/ais.ts var aisWindow = window; var escapeAisHtml = (input) => hostCore.escapeMapHtml(input); var aisStatus = document.getElementById("ais-status"); var aisMessagesEl = document.getElementById("ais-messages"); var aisFilterInput = document.getElementById("ais-filter"); var aisBarOverlay = document.getElementById("ais-bar-overlay"); var aisChannelSummaryEl = document.getElementById("ais-channel-summary"); var aisVesselCountEl = document.getElementById("ais-vessel-count"); var aisLatestSeenEl = document.getElementById("ais-latest-seen"); var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3; var AIS_DEFAULT_A_HZ = 161975e3; var AIS_CHANNEL_SPACING_HZ = 5e4; var aisFilterText = ""; var aisMessageHistory = []; function currentAisHistoryRetentionMs() { return typeof aisWindow.getDecodeHistoryRetentionMs === "function" ? aisWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; } function pruneAisMessageHistory() { const cutoffMs = Date.now() - currentAisHistoryRetentionMs(); aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs); } function scheduleAisUi(key, job) { if (typeof aisWindow.trxScheduleUiFrameJob === "function") { aisWindow.trxScheduleUiFrameJob(key, job); return; } job(); } function scheduleAisHistoryRender() { scheduleAisUi("ais-history", () => { renderAisHistory(); }); } function scheduleAisBarUpdate() { scheduleAisUi("ais-bar", () => { updateAisBar(); }); } function formatAisMhz(freqHz) { return `${(freqHz / 1e6).toFixed(3)} MHz`; } function currentAisChannelPlan() { const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, ""); const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ; const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ; return { aHz: safeAHz, bHz: safeAHz + AIS_CHANNEL_SPACING_HZ }; } function aisChannelInfo(channel) { const plan = currentAisChannelPlan(); const ch = (channel ?? "").trim().toUpperCase(); if (ch === "B") { return { label: "AIS-B", badgeClass: "ais-badge ais-badge-channel-b", freqText: formatAisMhz(plan.bHz) }; } return { label: "AIS-A", badgeClass: "ais-badge ais-badge-channel-a", freqText: formatAisMhz(plan.aHz) }; } function aisDisplayName(msg) { return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`; } function aisDisplayNameHtml(msg) { const label = escapeAisHtml(aisDisplayName(msg)); const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null; if (!url) return label; return `${label}`; } function aisTypeLabel(type) { switch (Number(type)) { case 1: case 2: case 3: return "Class A Position"; case 4: return "Base Station"; case 5: return "Static/Voyage"; case 18: return "Class B Position"; case 19: return "Class B Extended"; case 21: return "Aid to Nav"; case 24: return "Class B Static"; default: return `Type ${type ?? "--"}`; } } function aisAgeText(tsMs) { if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now"; const deltaMs = Math.max(0, Date.now() - tsMs); const seconds = Math.round(deltaMs / 1e3); if (seconds < 5) return "just now"; if (seconds < 60) return `${seconds}s ago`; const minutes = Math.round(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.round(minutes / 60); return `${hours}h ago`; } function aisMotionText(msg) { const parts = [ msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null, msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null, msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null ].filter(Boolean); return parts.join(" · "); } function aisRouteText(msg) { return [msg.callsign, msg.destination].filter(Boolean).join(" -> "); } function aisDistanceText(msg) { if (hostState.serverLat == null || hostState.serverLon == null || msg.lat == null || msg.lon == null) { return ""; } const distKm = hostCore.haversineKm(hostState.serverLat, hostState.serverLon, msg.lat, msg.lon); if (!Number.isFinite(distKm)) return ""; if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`; return `${distKm.toFixed(1)} km from TRX`; } function aisLatestByVessel(messages) { const byMmsi = /* @__PURE__ */ new Map(); for (const msg of messages) { const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`; if (!byMmsi.has(key)) byMmsi.set(key, msg); } return Array.from(byMmsi.values()); } function aisRigMessages() { return forActiveRig(aisMessageHistory); } function updateAisSummary() { const plan = currentAisChannelPlan(); if (aisChannelSummaryEl) { aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`; } const rigMessages = aisRigMessages(); const vessels = aisLatestByVessel(rigMessages); if (aisVesselCountEl) { const count = vessels.length; aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`; } if (aisLatestSeenEl) { const latest = rigMessages[0]; if (!latest) { aisLatestSeenEl.textContent = "No traffic yet"; } else { const channel = aisChannelInfo(latest.channel); aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`; } } } function aisSummaryText(msg) { const parts = []; if (msg.lat != null && msg.lon != null) parts.push(`${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}`); const motion = aisMotionText(msg); if (motion) parts.push(motion); const route = aisRouteText(msg); if (route && parts.length < 2) parts.push(route); if (!parts.length) return route || "no position reported"; return parts.join(" · "); } function renderAisRow(msg) { const row = document.createElement("details"); row.className = "ais-message"; const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); const name = aisDisplayName(msg); const nameHtml = aisDisplayNameHtml(msg); const channel = aisChannelInfo(msg.channel); const motion = aisMotionText(msg); const route = aisRouteText(msg); const distance = aisDistanceText(msg); const pos = msg.lat != null && msg.lon != null ? `${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)}` : ""; const vesselUrl = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null; row.dataset.filterText = [ name, msg.mmsi, msg.channel, channel.label, msg.vessel_name, msg.callsign, msg.destination, aisTypeLabel(msg.message_type) ].filter(Boolean).join(" ").toUpperCase(); row.innerHTML = `${escapeAisHtml(ts)}${nameHtml}${escapeAisHtml(aisTypeLabel(msg.message_type))}${escapeAisHtml(aisSummaryText(msg))}${escapeAisHtml(channel.label)}` + (distance ? `${escapeAisHtml(distance)}` : "") + `
MMSI ${escapeAisHtml(String(msg.mmsi))}${escapeAisHtml(channel.freqText)}` + (route ? `${escapeAisHtml(route)}` : "") + (motion ? `${escapeAisHtml(motion)}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}` + (pos ? `${pos}` : "") + `
` + (msg.lat != null && msg.lon != null ? `` : "") + (vesselUrl ? `Vessel` : "") + `
`; row.querySelectorAll("[data-ais-map]").forEach((element) => { element.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); const [lat, lon] = (element.dataset.aisMap ?? "").split(",").map(Number); if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) return; aisWindow.navigateToAprsMap?.(lat, lon); }); }); applyAisFilterToRow(row); return row; } function applyAisFilterToRow(row) { if (!aisFilterText) { row.style.display = ""; return; } const message = row.dataset.filterText || ""; row.style.display = message.includes(aisFilterText) ? "" : "none"; } function updateAisBar() { if (!aisBarOverlay) return; updateAisSummary(); const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS"; const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS; const recent = aisMessageHistory.filter( (msg) => (msg._tsMs ?? 0) >= cutoffMs && isActiveRigDecode(msg.rig_id) ); const messages = aisLatestByVessel(recent).slice(0, 8); if (!isAis || messages.length === 0) { aisBarOverlay.style.display = "none"; aisBarOverlay.innerHTML = ""; return; } let html = `
AISLiveClearLast 15 minutes
`; for (const msg of messages) { const ts = msg._ts ? `${msg._ts}` : ""; const pin = msg.lat != null && msg.lon != null ? `` : ""; const name = `${aisDisplayNameHtml(msg)}`; const channel = aisChannelInfo(msg.channel); const distance = aisDistanceText(msg); const details = [ `MMSI ${escapeAisHtml(String(msg.mmsi))}`, escapeAisHtml(channel.label), msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null, msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}°` : null, distance ? escapeAisHtml(distance) : null, escapeAisHtml(aisAgeText(msg._tsMs)) ].filter(Boolean).join(" · "); html += `
${ts}${pin}${name}: ${details}
`; } aisBarOverlay.innerHTML = html; aisBarOverlay.style.display = "flex"; } aisWindow.updateAisBar = updateAisBar; aisWindow.clearAisBar = function() { resetAisHistoryView(); }; function resetAisHistoryView() { if (aisMessagesEl) aisMessagesEl.innerHTML = ""; aisMessageHistory = []; updateAisBar(); renderAisHistory(); aisWindow.clearMapMarkersByType?.("ais"); } function renderAisHistory() { pruneAisMessageHistory(); if (!aisMessagesEl) { updateAisSummary(); return; } const fragment = document.createDocumentFragment(); for (const message of aisRigMessages()) { fragment.appendChild(renderAisRow(message)); } aisMessagesEl.replaceChildren(fragment); updateAisSummary(); } function addAisMessage(msg) { const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); msg._tsMs = tsMs; msg._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); aisMessageHistory.unshift(msg); pruneAisMessageHistory(); scheduleAisBarUpdate(); scheduleAisHistoryRender(); plotAisMessage(msg); } function plotAisMessage(msg) { if (msg.lat == null || msg.lon == null || !aisWindow.aisMapAddVessel) return; aisWindow.aisMapAddVessel(msg); } function normalizeServerAisMessage(msg) { return { ...msg, rig_id: msg.rig_id || null }; } function onServerAisBatch(messages) { if (!Array.isArray(messages) || messages.length === 0) return; const normalized = []; for (const msg of messages) { const next = normalizeServerAisMessage(msg); if (aisStatus && isActiveRigDecode(next.rig_id)) aisStatus.textContent = "Receiving"; const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now(); next._tsMs = tsMs; next._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); plotAisMessage(next); normalized.push(next); } normalized.reverse(); aisMessageHistory = normalized.concat(aisMessageHistory); pruneAisMessageHistory(); scheduleAisBarUpdate(); scheduleAisHistoryRender(); } function pruneAisHistoryView() { pruneAisMessageHistory(); updateAisBar(); renderAisHistory(); } document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => { void (async () => { if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return; try { await hostCore.postPath("/clear_ais_decode"); resetAisHistoryView(); } catch (e) { console.error("AIS history clear failed", e); } })(); }); if (aisFilterInput) { aisFilterInput.addEventListener("input", () => { aisFilterText = aisFilterInput.value.trim().toUpperCase(); renderAisHistory(); }); } function onServerAis(msg) { const message = normalizeServerAisMessage(msg); if (aisStatus && isActiveRigDecode(message.rig_id)) aisStatus.textContent = "Receiving"; addAisMessage(message); } updateAisSummary(); window.trxPluginRuntime.registerDecoder({ id: "ais", onMessage: onServerAis, onBatch: onServerAisBatch, restore: onServerAisBatch, reset: resetAisHistoryView, prune: pruneAisHistoryView, rerender: () => { updateAisBar(); renderAisHistory(); }, // Oldest first, so vessel tracks are rebuilt in the order they happened. syncMap: () => { for (const entry of [...aisMessageHistory].reverse()) plotAisMessage(entry); } });