first setup
This commit is contained in:
commit
b2a5f85e4e
7 changed files with 745 additions and 0 deletions
276
index.html
Normal file
276
index.html
Normal file
|
@ -0,0 +1,276 @@
|
|||
<!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>
|
Loading…
Add table
Add a link
Reference in a new issue