Files
cosma-nav-tools/viewer/index.html
Poulpe ad6c197f5c feat(viewer): calendar date-picker + mission name from backend 8766
- date-picker bar with datalist highlighting available dates
- fetch /api/data-dates from 192.168.0.83:8766 on load
- mission name + session count shown on date change
- Aujourd hui button sets today
- slider 24h intact
2026-04-27 13:45:20 +00:00

268 lines
8.4 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>COSMA — USV Track Viewer</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; display: flex; flex-direction: column; height: 100vh; }
#map { flex: 1; }
/* ── top bar: date picker ── */
#datebar {
background: #0d1b2a;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #0f3460;
flex-wrap: wrap;
}
#datebar label { font-size: 11px; color: #a0c4ff; white-space: nowrap; }
#date-picker {
background: #16213e;
color: #e0e0e0;
border: 1px solid #0f3460;
padding: 4px 8px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
}
#date-picker::-webkit-calendar-picker-indicator { filter: invert(1); }
#btn-today {
background: #0f3460;
color: #a0c4ff;
border: 1px solid #1a5276;
padding: 4px 10px;
font-family: monospace;
font-size: 11px;
cursor: pointer;
}
#btn-today:hover { background: #1a5276; }
#mission-label {
font-size: 12px;
color: #e94560;
font-weight: bold;
min-width: 160px;
}
#dates-hint {
font-size: 10px;
color: #556;
white-space: nowrap;
}
/* ── bottom bar: slider ── */
#controls {
background: #16213e;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 12px;
border-top: 1px solid #0f3460;
}
#slider { flex: 1; cursor: pointer; }
#info { font-size: 11px; min-width: 340px; color: #a0c4ff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#title { font-size: 13px; font-weight: bold; color: #e94560; white-space: nowrap; }
</style>
</head>
<body>
<!-- ── Date picker bar ── -->
<div id="datebar">
<label>Date mission:</label>
<input type="date" id="date-picker">
<button id="btn-today">Aujourd'hui</button>
<span id="mission-label"></span>
<span id="dates-hint">Chargement calendrier…</span>
</div>
<div id="map"></div>
<!-- ── Slider bar ── -->
<div id="controls">
<span id="title">USV Track</span>
<input type="range" id="slider" min="0" value="0">
<div id="info">Chargement…</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ── Map init ──────────────────────────────────────────────────────────────
const map = L.map('map', { zoomControl: true });
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19,
});
osm.addTo(map);
const seamarks = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
attribution: '© OpenSeaMap',
maxZoom: 18,
opacity: 0.8,
});
const gebco = L.tileLayer(
'https://tiles.arcgis.com/tiles/C8EMgrsFcRFL6LrL/arcgis/rest/services/GEBCO_basemap_NCEI/MapServer/tile/{z}/{y}/{x}',
{
attribution: 'GEBCO / NCEI',
maxZoom: 13,
opacity: 0.6,
}
);
L.control.layers(
{ 'OpenStreetMap': osm },
{ 'OpenSeaMap (balises)': seamarks, 'GEBCO (bathymétrie)': gebco },
{ collapsed: false }
).addTo(map);
// ── SVG arrow marker factory ──────────────────────────────────────────────
function makeArrowIcon(heading) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="-16 -16 32 32">
<g transform="rotate(${heading || 0})">
<polygon points="0,-12 6,8 0,4 -6,8" fill="#e94560" stroke="#fff" stroke-width="1.5"/>
</g>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [32, 32],
iconAnchor: [16, 16],
});
}
// ── Calendar data ─────────────────────────────────────────────────────────
const BACKEND = 'http://192.168.0.83:8766';
let dataMissions = {}; // date → {missions, session_count}
async function loadCalendar() {
try {
const resp = await fetch(`${BACKEND}/api/data-dates`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const json = await resp.json();
json.dates.forEach(d => {
dataMissions[d.date] = { missions: d.missions, session_count: d.session_count };
});
const availDates = Object.keys(dataMissions).sort();
const picker = document.getElementById('date-picker');
if (availDates.length > 0) {
picker.min = availDates[0];
picker.max = availDates[availDates.length - 1];
}
// Highlight available dates via custom attribute list (CSS trick)
// We inject a datalist for browsers that support it
const dl = document.createElement('datalist');
dl.id = 'available-dates';
availDates.forEach(d => {
const opt = document.createElement('option');
opt.value = d;
dl.appendChild(opt);
});
document.body.appendChild(dl);
picker.setAttribute('list', 'available-dates');
document.getElementById('dates-hint').textContent =
`${availDates.length} dates avec données`;
// Select most recent date by default
if (availDates.length > 0) {
picker.value = availDates[availDates.length - 1];
onDateChange(picker.value);
}
} catch (e) {
document.getElementById('dates-hint').textContent = 'Calendrier indispo: ' + e.message;
}
}
function onDateChange(date) {
const label = document.getElementById('mission-label');
const info = dataMissions[date];
if (info) {
label.textContent = info.missions.join(', ') + ` (${info.session_count} session${info.session_count > 1 ? 's' : ''})`;
label.style.color = '#00e676';
} else {
label.textContent = 'Aucune donnée pour cette date';
label.style.color = '#666';
}
}
document.getElementById('date-picker').addEventListener('change', e => onDateChange(e.target.value));
document.getElementById('btn-today').addEventListener('click', () => {
const today = new Date().toISOString().split('T')[0];
const picker = document.getElementById('date-picker');
picker.value = today;
onDateChange(today);
});
// ── Track data loading ────────────────────────────────────────────────────
let points = [];
let trackLayer = null;
let marker = null;
async function loadData() {
try {
const [trackResp, pointsResp] = await Promise.all([
fetch('track.geojson'),
fetch('points.json'),
]);
if (!trackResp.ok) throw new Error('track.geojson not found');
if (!pointsResp.ok) throw new Error('points.json not found');
const trackGeo = await trackResp.json();
points = await pointsResp.json();
// Draw track
trackLayer = L.geoJSON(trackGeo, {
style: { color: '#00b4d8', weight: 2, opacity: 0.8 },
}).addTo(map);
map.fitBounds(trackLayer.getBounds(), { padding: [40, 40] });
// Init slider
const slider = document.getElementById('slider');
slider.max = points.length - 1;
slider.value = 0;
slider.addEventListener('input', () => updateMarker(+slider.value));
// Init marker
if (points.length > 0) {
const p = points[0];
marker = L.marker([p.lat, p.lon], { icon: makeArrowIcon(p.heading || 0) }).addTo(map);
updateInfo(0);
}
document.getElementById('title').textContent = `USV Track — ${points.length} pts`;
} catch (e) {
document.getElementById('info').textContent = 'Erreur: ' + e.message;
}
}
function updateMarker(idx) {
const p = points[idx];
if (!p || !marker) return;
marker.setLatLng([p.lat, p.lon]);
marker.setIcon(makeArrowIcon(p.heading || 0));
updateInfo(idx);
}
function updateInfo(idx) {
const p = points[idx];
if (!p) return;
const hdg = p.heading !== null ? `${p.heading.toFixed(1)}°` : 'N/A';
document.getElementById('info').textContent =
`[${idx+1}/${points.length}] ${p.t} | Lat: ${p.lat.toFixed(6)} Lon: ${p.lon.toFixed(6)} | Cap: ${hdg}`;
}
// ── Init ──────────────────────────────────────────────────────────────────
loadCalendar();
loadData();
</script>
</body>
</html>