feat(cosma-nav-tools): v1 parser + Leaflet viewer USV La Ciotat 2026-04-08
This commit is contained in:
142
viewer/index.html
Normal file
142
viewer/index.html
Normal file
@@ -0,0 +1,142 @@
|
||||
<!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; }
|
||||
#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>
|
||||
<div id="map"></div>
|
||||
<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,
|
||||
});
|
||||
|
||||
// GEBCO bathymetry (tile-based, simpler than WMS)
|
||||
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],
|
||||
});
|
||||
}
|
||||
|
||||
// ── 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}`;
|
||||
}
|
||||
|
||||
loadData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user