geolog/index.html
2025-08-30 23:49:30 +02:00

276 lines
9.5 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Connections Animation Map</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<style>
body { margin: 0; font-family: Arial, sans-serif; }
#controls {
position: absolute;
top: 10px;
left: 48px;
z-index: 1000;
background: rgba(255,255,255,0.95);
padding: 8px;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
max-width: 320px;
font-size: 13px;
pointer-events: auto;
}
#controls label { display: block; margin: 6px 0; }
#map { height: 100vh; width: 100vw; }
.fade-marker { transition: opacity 1s linear; opacity: 1; }
.fade-out { opacity: 0 !important; }
.arc-path { transition: opacity 1s linear; opacity: 1; stroke-width: 2; }
.arc-fade { opacity: 0 !important; }
#timeDisplay { font-weight: 600; margin-left: 6px; }
.small { font-size: 12px; color: #333; }
</style>
</head>
<body>
<div id="controls">
<label>Service: <input type="text" id="service" placeholder="/api/service1"></label>
<label>Start: <input type="datetime-local" id="start"></label>
<label>End: <input type="datetime-local" id="end"></label>
<label>Duration (seconds): <input type="number" id="duration" value="10" min="1"></label>
<label class="small">Time:
<input type="range" id="timeSlider" min="0" max="100" value="0" style="width: 220px;">
<span id="timeDisplay"></span>
</label>
<div style="margin-top:6px;">
<button id="startBtn">Start Simulation</button>
<button id="stopBtn">Stop</button>
</div>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<script>
const OUR_COORDS = [37.7749, -122.4194];
const MAP_CENTER = [20, 0];
const MAP_ZOOM = 2;
var map = L.map('map').setView(MAP_CENTER, MAP_ZOOM);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: "© OpenStreetMap contributors"
}).addTo(map);
const serverMarker = L.circleMarker(OUR_COORDS, {
radius: 7,
color: 'red',
fillColor: 'red',
fillOpacity: 1
}).addTo(map);
serverMarker.bindTooltip("SRV", {
permanent: true,
direction: "right",
offset: [8, 0],
className: "srv-label"
}).openTooltip();
var markersLayer = L.layerGroup().addTo(map);
var arcsLayer = L.layerGroup().addTo(map);
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const timeSlider = document.getElementById('timeSlider');
const timeDisplay = document.getElementById('timeDisplay');
let allData = [];
let animTimers = [];
let isPlaying = false;
let simStartMs = 0, simEndMs = 0;
async function fetchConnections(service, start, end) {
let url = `http://localhost:8000/connections?`;
if (service) url += `service=${encodeURIComponent(service)}&`;
if (start) url += `start=${new Date(start).toISOString()}&`;
if (end) url += `end=${new Date(end).toISOString()}&`;
let response = await fetch(url);
return await response.json();
}
function latLonToVec(lat, lon) {
const rlat = lat * Math.PI / 180;
const rlon = lon * Math.PI / 180;
return [Math.cos(rlat) * Math.cos(rlon), Math.cos(rlat) * Math.sin(rlon), Math.sin(rlat)];
}
function vecToLatLon(v) {
const x = v[0], y = v[1], z = v[2];
const lon = Math.atan2(y, x);
const hyp = Math.sqrt(x*x + y*y);
const lat = Math.atan2(z, hyp);
return [lat * 180/Math.PI, lon * 180/Math.PI];
}
function slerp(a, b, t) {
let dot = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];
dot = Math.min(1, Math.max(-1, dot));
const omega = Math.acos(dot);
if (Math.abs(omega) < 1e-6) return a;
const so = Math.sin(omega);
const c1 = Math.sin((1 - t) * omega) / so;
const c2 = Math.sin(t * omega) / so;
return [c1 * a[0] + c2 * b[0], c1 * a[1] + c2 * b[1], c1 * a[2] + c2 * b[2]];
}
function computeArcPoints(lat1, lon1, lat2, lon2, segments = 60, heightFactor = 0.2) {
const v1 = latLonToVec(lat1, lon1);
const v2 = latLonToVec(lat2, lon2);
const pts = [];
for (let i = 0; i <= segments; i++) {
const t = i / segments;
let vi = slerp(v1, v2, t);
const bulge = Math.sin(Math.PI * t) * heightFactor;
const len = Math.sqrt(vi[0]*vi[0] + vi[1]*vi[1] + vi[2]*vi[2]);
vi = [vi[0]/len * (1 + bulge), vi[1]/len * (1 + bulge), vi[2]/len * (1 + bulge)];
pts.push(vecToLatLon(vi));
}
return pts;
}
function createMarkerAndArc(conn, visibleTimeMs = 2500) {
const lat = conn.lat, lon = conn.lon;
const marker = L.circleMarker([lat, lon], { radius: 5, fillOpacity: 0.9 })
.bindPopup(`<b>IP:</b> ${conn.ip}<br><b>Service:</b> ${conn.path}<br><b>Time:</b> ${conn.timestamp}`)
.addTo(markersLayer);
const arcPoints = computeArcPoints(lat, lon, OUR_COORDS[0], OUR_COORDS[1], 48, 0.18);
const arc = L.polyline(arcPoints, { color: '#0077ff', weight: 2, opacity: 1, className: 'arc-path' })
.addTo(arcsLayer);
const markerElem = marker._path;
if (markerElem) markerElem.classList.add('fade-marker');
const arcElem = arc._path;
if (arcElem) arcElem.classList.add('arc-path');
const t1 = setTimeout(() => {
if (markerElem) markerElem.classList.add('fade-out');
if (arcElem) arcElem.classList.add('arc-fade');
setTimeout(() => {
try { markersLayer.removeLayer(marker); } catch(e){}
try { arcsLayer.removeLayer(arc); } catch(e){}
}, 1000);
}, visibleTimeMs);
return t1;
}
function clearAll() {
animTimers.forEach(t => clearTimeout(t));
animTimers = [];
markersLayer.clearLayers();
arcsLayer.clearLayers();
isPlaying = false;
}
// === NEW: 24H format display ===
function updateTimeDisplay(ms) {
if (!ms || isNaN(ms)) {
timeDisplay.textContent = '—';
return;
}
const d = new Date(ms);
const pad = n => n.toString().padStart(2,'0');
timeDisplay.textContent = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function drawUpTo(ms) {
markersLayer.clearLayers();
arcsLayer.clearLayers();
const subset = allData.filter(ev => new Date(ev.timestamp).getTime() <= ms);
subset.forEach(ev => {
const m = L.circleMarker([ev.lat, ev.lon], { radius: 5, fillOpacity: 0.9 })
.bindPopup(`<b>IP:</b> ${ev.ip}<br><b>Service:</b> ${ev.path}<br><b>Time:</b> ${ev.timestamp}`)
.addTo(markersLayer);
const arcP = computeArcPoints(ev.lat, ev.lon, OUR_COORDS[0], OUR_COORDS[1], 36, 0.12);
L.polyline(arcP, { color: '#0077ff', weight: 2, opacity: 0.6 })
.addTo(arcsLayer);
});
updateTimeDisplay(ms);
}
function formatDatetimeLocal(d) {
const pad = n => n.toString().padStart(2,'0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
async function startSimulation() {
clearAll();
const service = document.getElementById("service").value;
let startVal = document.getElementById("start").value;
let endVal = document.getElementById("end").value;
const durationSec = parseFloat(document.getElementById("duration").value) || 10;
if (!startVal && !endVal) {
const now = new Date();
const twoWeeksAgo = new Date(now.getTime() - 1000*60*60*24*14);
startVal = formatDatetimeLocal(twoWeeksAgo);
endVal = formatDatetimeLocal(now);
document.getElementById("start").value = startVal;
document.getElementById("end").value = endVal;
} else if (!startVal || !endVal) {
alert('Please set both start & end times');
return;
}
const startMs = new Date(startVal).getTime();
const endMs = new Date(endVal).getTime();
if (endMs <= startMs) { alert('End must be after start'); return; }
if ((endMs - startMs) > 1000*60*60*24*90) { alert('Max 3 months'); return; }
const data = await fetchConnections(service, startVal, endVal);
if (!data.length) { alert('No data'); return; }
allData = data.slice().sort((a,b) => new Date(a.timestamp)-new Date(b.timestamp));
simStartMs = startMs; simEndMs = endMs;
timeSlider.min = simStartMs;
timeSlider.max = simEndMs;
timeSlider.value = simStartMs;
updateTimeDisplay(simStartMs);
isPlaying = true;
const totalEvents = allData.length;
const totalMs = durationSec * 1000;
const baseInterval = totalMs / totalEvents;
allData.forEach((ev, idx) => {
const scheduledAt = Math.round(idx * baseInterval);
const visibleTime = Math.max(600, Math.min(4000, totalMs/8));
const tshow = setTimeout(() => {
const timer = createMarkerAndArc(ev, visibleTime);
animTimers.push(timer);
const simTime = simStartMs + (scheduledAt/totalMs)*(simEndMs - simStartMs);
timeSlider.value = simTime;
updateTimeDisplay(simTime);
}, scheduledAt);
animTimers.push(tshow);
});
const finishTimer = setTimeout(() => { isPlaying = false; }, totalMs+2000);
animTimers.push(finishTimer);
}
function stopSimulation() {
clearAll();
if (simStartMs) {
timeSlider.value = simStartMs;
updateTimeDisplay(simStartMs);
}
}
timeSlider.addEventListener('input', e => {
if (isPlaying) clearAll();
drawUpTo(parseInt(e.target.value,10));
});
startBtn.addEventListener('click', startSimulation);
stopBtn.addEventListener('click', stopSimulation);
</script>
</body>
</html>