198 lines
8.7 KiB
JavaScript
198 lines
8.7 KiB
JavaScript
'use strict';
|
|
// DVR — AIS Track History and Playback
|
|
|
|
let _dvrTrack = []; // loaded track points
|
|
let _dvrTimer = null; // setInterval handle for playback
|
|
let _dvrIndex = 0; // current playback position
|
|
let _dvrPlaying = false;
|
|
|
|
// ── Open / init modal ─────────────────────────────────────────────────────────
|
|
window.openTrackHistoryModal = function() {
|
|
const today = new Date().toISOString().slice(0, 10);
|
|
const from = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
|
|
document.getElementById('dvr-from').value = from;
|
|
document.getElementById('dvr-to').value = today;
|
|
document.getElementById('dvr-mmsi').value = '';
|
|
document.getElementById('dvr-body').innerHTML =
|
|
'<div style="color:var(--text-muted);font-size:0.75rem;padding:20px 0;text-align:center">Enter an MMSI and date range, then press LOAD TRACK.</div>';
|
|
document.getElementById('dvr-stats').style.display = 'none';
|
|
document.getElementById('dvr-controls').style.display = 'none';
|
|
_dvrStop();
|
|
_dvrTrack = [];
|
|
_showModal('modal-track-history');
|
|
};
|
|
|
|
// ── Load track ────────────────────────────────────────────────────────────────
|
|
document.getElementById('btn-dvr-load')?.addEventListener('click', _dvrLoad);
|
|
|
|
async function _dvrLoad() {
|
|
const mmsi = document.getElementById('dvr-mmsi').value.trim();
|
|
const from = document.getElementById('dvr-from').value;
|
|
const to = document.getElementById('dvr-to').value;
|
|
const source = document.getElementById('dvr-source').value;
|
|
|
|
if (!mmsi) { alert('Enter an MMSI first.'); return; }
|
|
|
|
const body = document.getElementById('dvr-body');
|
|
body.innerHTML = '<div style="color:var(--text-muted);padding:8px">Loading…</div>';
|
|
document.getElementById('dvr-stats').style.display = 'none';
|
|
document.getElementById('dvr-controls').style.display = 'none';
|
|
_dvrStop();
|
|
|
|
try {
|
|
const endpoint = source === 'aton'
|
|
? `${API}/tracks/atons/${encodeURIComponent(mmsi)}`
|
|
: `${API}/tracks/vessels/${encodeURIComponent(mmsi)}`;
|
|
const url = `${endpoint}?from=${from}T00:00:00&to=${to}T23:59:59&limit=50000`;
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: `Bearer ${window.Auth?.token()}` }
|
|
});
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
_dvrTrack = await res.json();
|
|
} catch (e) {
|
|
body.innerHTML = `<div class="modal-error" style="display:block">Error: ${e.message}</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!_dvrTrack.length) {
|
|
body.innerHTML = '<div style="color:var(--text-muted);font-size:0.75rem;padding:20px 0;text-align:center">No track data found for this MMSI and date range.</div>';
|
|
return;
|
|
}
|
|
|
|
// Stats bar
|
|
const statsEl = document.getElementById('dvr-stats');
|
|
document.getElementById('dvr-stats-text').textContent =
|
|
`${_dvrTrack.length} points · from ${_dvrTrack[0].ts.slice(0,19)} to ${_dvrTrack.at(-1).ts.slice(0,19)} UTC`;
|
|
statsEl.style.display = '';
|
|
|
|
// Playback controls
|
|
const ctrl = document.getElementById('dvr-controls');
|
|
ctrl.style.display = '';
|
|
const slider = document.getElementById('dvr-slider');
|
|
slider.max = _dvrTrack.length - 1;
|
|
slider.value = 0;
|
|
_dvrIndex = 0;
|
|
_dvrUpdateDisplay(0);
|
|
|
|
// Table preview (first/last 50)
|
|
const preview = _dvrTrack.length > 100
|
|
? [..._dvrTrack.slice(0, 50), null, ..._dvrTrack.slice(-50)]
|
|
: _dvrTrack;
|
|
|
|
body.innerHTML = `
|
|
<table class="users-table">
|
|
<thead><tr>
|
|
<th>#</th><th>Timestamp (UTC)</th><th>LAT</th><th>LON</th>
|
|
<th>SOG (kn)</th><th>COG (°)</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
${preview.map((p, i) => p ? `
|
|
<tr style="font-size:0.7rem">
|
|
<td class="mono" style="color:var(--text-muted)">${(i < 50 ? i + 1 : _dvrTrack.length - 99 + i)}</td>
|
|
<td class="mono">${p.ts?.slice(0, 19)}</td>
|
|
<td class="mono">${p.lat?.toFixed(5) ?? '--'}</td>
|
|
<td class="mono">${p.lon?.toFixed(5) ?? '--'}</td>
|
|
<td>${p.sog?.toFixed(1) ?? '--'}</td>
|
|
<td>${p.cog?.toFixed(0) ?? '--'}</td>
|
|
</tr>` : '<tr><td colspan="6" style="text-align:center;color:var(--text-muted);font-size:0.7rem">…</td></tr>'
|
|
).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
}
|
|
|
|
// ── Playback controls ─────────────────────────────────────────────────────────
|
|
document.getElementById('btn-dvr-play')?.addEventListener('click', _dvrPlay);
|
|
document.getElementById('btn-dvr-pause')?.addEventListener('click', _dvrPause);
|
|
document.getElementById('btn-dvr-stop')?.addEventListener('click', _dvrStop);
|
|
|
|
document.getElementById('dvr-slider')?.addEventListener('input', (e) => {
|
|
_dvrIndex = parseInt(e.target.value);
|
|
_dvrUpdateDisplay(_dvrIndex);
|
|
});
|
|
|
|
function _dvrPlay() {
|
|
if (!_dvrTrack.length) return;
|
|
if (_dvrIndex >= _dvrTrack.length - 1) _dvrIndex = 0;
|
|
_dvrPlaying = true;
|
|
const speed = parseInt(document.getElementById('dvr-speed').value) || 20;
|
|
// Each step advances by `speed` seconds of track data; timer fires at 100ms
|
|
if (_dvrTimer) clearInterval(_dvrTimer);
|
|
_dvrTimer = setInterval(() => {
|
|
if (!_dvrPlaying || _dvrIndex >= _dvrTrack.length - 1) {
|
|
_dvrPause();
|
|
return;
|
|
}
|
|
// Find next point that is at least `speed` seconds ahead
|
|
const curTs = new Date(_dvrTrack[_dvrIndex].ts).getTime();
|
|
let next = _dvrIndex + 1;
|
|
while (next < _dvrTrack.length - 1) {
|
|
const diff = (new Date(_dvrTrack[next].ts).getTime() - curTs) / 1000;
|
|
if (diff >= speed) break;
|
|
next++;
|
|
}
|
|
_dvrIndex = next;
|
|
document.getElementById('dvr-slider').value = _dvrIndex;
|
|
_dvrUpdateDisplay(_dvrIndex);
|
|
}, 100);
|
|
}
|
|
|
|
function _dvrPause() {
|
|
_dvrPlaying = false;
|
|
if (_dvrTimer) { clearInterval(_dvrTimer); _dvrTimer = null; }
|
|
}
|
|
|
|
function _dvrStop() {
|
|
_dvrPause();
|
|
_dvrIndex = 0;
|
|
const slider = document.getElementById('dvr-slider');
|
|
if (slider) slider.value = 0;
|
|
if (_dvrTrack.length) _dvrUpdateDisplay(0);
|
|
// Remove DVR marker from map if present
|
|
if (window._dvrMapClear) window._dvrMapClear();
|
|
}
|
|
|
|
function _dvrUpdateDisplay(idx) {
|
|
if (!_dvrTrack.length || idx >= _dvrTrack.length) return;
|
|
const p = _dvrTrack[idx];
|
|
document.getElementById('dvr-timestamp').textContent =
|
|
p.ts ? p.ts.slice(0, 19) + ' UTC' : '--';
|
|
document.getElementById('dvr-lat').textContent = p.lat ? `LAT ${p.lat.toFixed(5)}` : 'LAT --';
|
|
document.getElementById('dvr-lon').textContent = p.lon ? `LON ${p.lon.toFixed(5)}` : 'LON --';
|
|
document.getElementById('dvr-sog').textContent = p.sog != null ? `SOG ${p.sog.toFixed(1)}` : 'SOG --';
|
|
document.getElementById('dvr-cog').textContent = p.cog != null ? `COG ${p.cog.toFixed(0)}` : 'COG --';
|
|
|
|
// Move DVR marker on map (map.js must expose window._dvrMapMove)
|
|
if (window._dvrMapMove && p.lat != null && p.lon != null) {
|
|
window._dvrMapMove(p.lon, p.lat, p.cog ?? 0);
|
|
}
|
|
}
|
|
|
|
// ── Show on map ───────────────────────────────────────────────────────────────
|
|
document.getElementById('btn-dvr-show-map')?.addEventListener('click', () => {
|
|
if (!_dvrTrack.length) return;
|
|
if (window._dvrMapShowTrack) {
|
|
window._dvrMapShowTrack(_dvrTrack);
|
|
_hideModal('modal-track-history');
|
|
}
|
|
});
|
|
|
|
// ── Export CSV ────────────────────────────────────────────────────────────────
|
|
document.getElementById('btn-dvr-csv')?.addEventListener('click', () => {
|
|
if (!_dvrTrack.length) return;
|
|
const mmsi = document.getElementById('dvr-mmsi').value.trim();
|
|
const rows = ['timestamp,lat,lon,sog,cog,heading',
|
|
..._dvrTrack.map(p =>
|
|
`${p.ts},${p.lat},${p.lon},${p.sog ?? ''},${p.cog ?? ''},${p.heading ?? ''}`)
|
|
];
|
|
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = `track_${mmsi}_${document.getElementById('dvr-from').value}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
});
|
|
|
|
// ── Close buttons ─────────────────────────────────────────────────────────────
|
|
document.getElementById('btn-close-track-history')?.addEventListener('click', () => _hideModal('modal-track-history'));
|
|
document.getElementById('btn-close-track-history2')?.addEventListener('click', () => _hideModal('modal-track-history'));
|