Initial commit — multi-tenant filtering, port constraints, chart bbox
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
'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'));
|
||||
Reference in New Issue
Block a user