Files
overwatch/defense_sim.html
2026-09-30 23:49:38 +00:00

1369 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Defense Simulation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: #0a0f14;
color: #d4d4d4;
padding: 12px;
}
.container { width: 100%; }
.main-layout {
display: grid;
grid-template-columns: 340px 1fr;
gap: 12px;
align-items: start;
}
.controls {
background: #1a2332;
padding: 15px;
border: 1px solid #3a4f6a;
border-radius: 3px;
position: sticky;
top: 12px;
align-self: start;
max-height: calc(100vh - 24px);
overflow-y: auto;
}
.section-title {
font-size: 11px;
color: #8ab4f8;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
border-bottom: 1px solid #3a4f6a;
padding-bottom: 5px;
}
.param-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
}
.param-label { color: #a0b4c8; }
.param-input {
width: 60px;
padding: 3px 5px;
background: #0f1923;
border: 1px solid #3a4f6a;
color: #d4d4d4;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.btn {
width: 100%;
padding: 8px;
background: #2a5f8a;
border: 1px solid #3a7fba;
color: #fff;
font-family: 'Courier New', monospace;
font-size: 12px;
cursor: pointer;
margin-top: 10px;
}
.btn:hover { background: #3a7fba; }
.map-container {
background: #1a2332;
border: 1px solid #3a4f6a;
border-radius: 3px;
padding: 10px;
position: relative;
}
#map {
width: 100%;
height: calc(100vh - 200px);
min-height: 380px;
background: #0f1923;
position: relative;
overflow: hidden;
}
.alert-box {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 15px;
background: #8b0000;
border: 2px solid #ff0000;
color: #fff;
font-size: 14px;
font-weight: bold;
display: none;
}
.alert-box.active { display: block; }
.alert-box.success { background: #006400; border-color: #00ff00; }
.radar {
position: absolute;
width: 16px;
height: 16px;
background: #4caf50;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.radar.active { background: #00e676; box-shadow: 0 0 8px #00e676; }
.radar.inactive { background: #444; }
.radar-range {
position: absolute;
border: 1px dashed rgba(76, 175, 80, 0.2);
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
.radar-range.active { border-color: rgba(0, 230, 118, 0.4); }
.drone {
position: absolute;
width: 8px;
height: 8px;
background: #2196f3;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.drone.flying { background: #03a9f4; box-shadow: 0 0 5px #03a9f4; }
.drone.ir-tracking { background: #00bcd4; box-shadow: 0 0 6px #00bcd4; }
.target {
position: absolute;
width: 20px;
height: 20px;
background: #f44336;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.altitude-marker {
position: absolute;
font-size: 9px;
color: #8ab4f8;
background: rgba(15, 25, 35, 0.9);
padding: 1px 4px;
border-radius: 2px;
white-space: nowrap;
z-index: 20;
}
.city {
position: absolute;
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid #666;
border-radius: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #888;
z-index: 5;
}
.trajectory {
position: absolute;
height: 2px;
background: rgba(244, 67, 54, 0.6);
transform-origin: left center;
z-index: 5;
}
.error-band {
position: absolute;
background: rgba(244, 67, 54, 0.08);
border-top: 1px dashed rgba(244, 67, 54, 0.3);
border-bottom: 1px dashed rgba(244, 67, 54, 0.3);
transform-origin: left center;
z-index: 4;
}
.grid-line {
position: absolute;
background: rgba(58, 79, 106, 0.15);
}
.data-panel {
margin-top: 10px;
padding: 12px;
background: #1a2332;
border: 1px solid #3a4f6a;
border-radius: 3px;
font-size: 11px;
}
.data-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
}
.data-cell {
background: #0f1923;
padding: 6px;
border: 1px solid #3a4f6a;
border-radius: 2px;
}
.data-cell .lbl { color: #8ab4f8; font-size: 9px; text-transform: uppercase; }
.data-cell .val { color: #d4d4d4; font-size: 13px; font-weight: bold; }
.stats-table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
margin-top: 6px;
}
.stats-table th, .stats-table td {
border: 1px solid #3a4f6a;
padding: 4px 6px;
text-align: center;
background: #0f1923;
}
.stats-table th {
color: #8ab4f8;
text-transform: uppercase;
font-size: 9px;
}
.stats-table td.preset-name {
text-align: left;
color: #d4d4d4;
font-weight: bold;
}
.stats-table tr.highlight td {
background: #1a3a5a;
border-color: #4fc3f7;
}
.closeup {
width: 200px;
height: 150px;
background: #0f1923;
border: 1px solid #3a4f6a;
border-radius: 2px;
position: relative;
overflow: hidden;
}
.closeup-title {
position: absolute;
top: 3px;
left: 5px;
font-size: 9px;
color: #8ab4f8;
text-transform: uppercase;
z-index: 2;
}
.closeup-dot {
position: absolute;
width: 8px;
height: 8px;
background: #f44336;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.closeup-drone {
position: absolute;
width: 6px;
height: 6px;
background: #03a9f4;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
.closeup-band {
position: absolute;
background: rgba(244, 67, 54, 0.15);
border-top: 1px dashed #f44336;
border-bottom: 1px dashed #f44336;
z-index: 1;
}
.closeup-traj {
position: absolute;
height: 2px;
background: rgba(244, 67, 54, 0.6);
transform-origin: left center;
z-index: 1;
}
.overlay-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 6;
pointer-events: none;
}
.legend {
position: absolute;
top: 10px;
left: 10px;
background: rgba(15, 25, 35, 0.9);
border: 1px solid #3a4f6a;
padding: 6px 10px;
font-size: 10px;
z-index: 20;
}
.legend .item { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
.legend .swatch { width: 12px; height: 12px; border-radius: 2px; flex-shrink: 0; }
.stats-panel {
background: #1a2332;
border: 1px solid #3a4f6a;
border-radius: 3px;
padding: 12px;
margin-top: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="main-layout">
<div class="controls">
<div class="section-title">Attack Parameters</div>
<div class="param-row">
<span class="param-label">Projectile Preset</span>
<select id="presetSelect" class="param-input" style="width: 130px;">
<option value="custom">Custom</option>
<option value="shahed-136">Shahed-136 (Geran-2)</option>
<option value="shahed-238" selected>Shahed-238 (Geran-3)</option>
<option value="cruise-missile">Cruise Missile</option>
<option value="ballistic-reentry">Ballistic (Re-entry)</option>
<option value="prop-drone">Small UAS / Prop Drone</option>
</select>
</div>
<div class="param-row">
<span class="param-label">Start Dist (km)</span>
<input type="number" id="attackStartDist" value="35" min="5" max="200" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Speed (m/s)</span>
<input type="number" id="attackSpeed" value="150" min="10" max="2000" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Min Cruise Alt (m)</span>
<input type="number" id="cruiseAltMin" value="1000" min="0" max="80000" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Max Cruise Alt (m)</span>
<input type="number" id="cruiseAltMax" value="2000" min="0" max="80000" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Descent Start (km)</span>
<input type="number" id="descentStart" value="8" min="0" max="50" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Terminal Alt (m)</span>
<input type="number" id="terminalAlt" value="200" min="0" max="80000" class="param-input">
</div>
<div class="section-title">Defense Parameters</div>
<div class="param-row">
<span class="param-label">Radars</span>
<input type="number" id="numRadars" value="8" min="4" max="16" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Drones</span>
<input type="number" id="numDrones" value="24" min="8" max="48" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Radar Range (km)</span>
<input type="number" id="radarRange" value="20" min="5" max="50" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Drone Speed (m/s)</span>
<input type="number" id="droneSpeed" value="100" min="50" max="200" class="param-input">
</div>
<div class="param-row">
<span class="param-label">IR Range (m)</span>
<input type="number" id="irRange" value="5000" min="1000" max="10000" class="param-input">
</div>
<div class="param-row">
<span class="param-label">City Radius (km)</span>
<input type="number" id="cityRadius" value="10" min="5" max="20" class="param-input">
</div>
<div class="param-row">
<span class="param-label">Drone Line (km)</span>
<input type="number" id="droneDefenseLine" value="8" min="3" max="15" class="param-input">
</div>
<button class="btn" onclick="runSimulation()">RUN SIMULATION</button>
<button class="btn" onclick="resetSimulation()">RESET</button>
</div>
<div class="map-container">
<div id="map">
<div class="alert-box" id="alertBox">ALERT!</div>
</div>
<div class="data-panel">
<div class="data-grid">
<div class="data-cell"><span class="lbl">Status</span><span class="val" id="dataStatus">STANDBY</span></div>
<div class="data-cell"><span class="lbl">Radars Active</span><span class="val" id="dataRadars">0</span></div>
<div class="data-cell"><span class="lbl">Data Points</span><span class="val" id="dataPoints">0</span></div>
<div class="data-cell"><span class="lbl">R² Fit</span><span class="val" id="dataR2">--</span></div>
<div class="data-cell"><span class="lbl">Speed Est</span><span class="val" id="dataSpeed">-- m/s</span></div>
<div class="data-cell"><span class="lbl">Altitude</span><span class="val" id="dataAltitude">-- m</span></div>
<div class="data-cell"><span class="lbl">Pos Error σ</span><span class="val" id="dataErrorSigma">-- m</span></div>
<div class="data-cell"><span class="lbl">95% Conf</span><span class="val" id="dataError95">-- m</span></div>
<div class="data-cell"><span class="lbl">Time to Impact</span><span class="val" id="dataTTI">-- s</span></div>
<div class="data-cell"><span class="lbl">Heading</span><span class="val" id="dataHeading">--°</span></div>
<div class="data-cell"><span class="lbl">Drone Alt</span><span class="val" id="dataDroneAlt">-- m</span></div>
<div class="data-cell"><span class="lbl">IR Drones</span><span class="val" id="dataIRDrones">0</span></div>
</div>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<div class="closeup" id="closeupMap">
<span class="closeup-title">CLOSEUP (500m)</span>
</div>
<div style="flex: 1;">
<div class="data-grid" style="grid-template-columns: repeat(3, 1fr); gap: 6px;">
<div class="data-cell"><span class="lbl">IR Drone 1 Dist</span><span class="val" id="irDist1">-- m</span></div>
<div class="data-cell"><span class="lbl">IR Drone 2 Dist</span><span class="val" id="irDist2">-- m</span></div>
<div class="data-cell"><span class="lbl">IR Drone 3 Dist</span><span class="val" id="irDist3">-- m</span></div>
</div>
<div class="data-grid" style="grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 6px;">
<div class="data-cell"><span class="lbl">IR Error σ</span><span class="val" id="irErrorSigma">-- m</span></div>
<div class="data-cell"><span class="lbl">IR 95% Conf</span><span class="val" id="irError95">-- m</span></div>
<div class="data-cell"><span class="lbl">Combined Error</span><span class="val" id="combinedError">-- m</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="stats-panel">
<div class="section-title">Interception Stats by Projectile (100 runs each)</div>
<table class="stats-table" id="statsTable">
<thead>
<tr>
<th>Projectile</th>
<th>Intercept</th>
<th>Detect</th>
<th>Collision</th>
<th>Impact</th>
</tr>
</thead>
<tbody id="statsBody"></tbody>
</table>
</div>
</div>
</div>
<script>
// =====================================================================
// Constants (matching Python simulation)
// =====================================================================
const SCALE = 10; // 1 km = 10 px on main map
const CLOSEUP_SCALE = 360; // 1 km = 360 px on closeup (500m view)
const MEASUREMENT_ERROR = 20.0; // meters (1 sigma)
const IR_ERROR = 3.0; // meters (1 sigma)
// Projectile presets (prefill attack parameters)
const PRESETS = {
custom: { startDist: 35, speed: 150, cruiseMin: 1000, cruiseMax: 2000, descentStart: 8, terminalAlt: 200 },
'shahed-136': { startDist: 30, speed: 50, cruiseMin: 100, cruiseMax: 300, descentStart: 5, terminalAlt: 0 },
'shahed-238': { startDist: 35, speed: 150, cruiseMin: 1000, cruiseMax: 2000, descentStart: 8, terminalAlt: 200 },
'cruise-missile':{ startDist: 50, speed: 300, cruiseMin: 50, cruiseMax: 150, descentStart: 5, terminalAlt: 0 },
'ballistic-reentry':{ startDist: 100, speed: 800, cruiseMin: 60000, cruiseMax: 80000, descentStart: 40, terminalAlt: 0 },
'prop-drone': { startDist: 15, speed: 30, cruiseMin: 100, cruiseMax: 300, descentStart: 3, terminalAlt: 0 }
};
function applyPreset(name) {
const p = PRESETS[name];
if (!p) return;
document.getElementById('attackStartDist').value = p.startDist;
document.getElementById('attackSpeed').value = p.speed;
document.getElementById('cruiseAltMin').value = p.cruiseMin;
document.getElementById('cruiseAltMax').value = p.cruiseMax;
document.getElementById('descentStart').value = p.descentStart;
document.getElementById('terminalAlt').value = p.terminalAlt;
}
let animationId = null;
// =====================================================================
// Utility functions
// =====================================================================
function getMapCenter() {
const map = document.getElementById('map');
return { x: map.offsetWidth / 2, y: map.offsetHeight / 2 };
}
function kmToPixels(km) { return km * SCALE; }
function kmToCloseup(km) { return km * CLOSEUP_SCALE; }
function clearMap() {
const map = document.getElementById('map');
const alert = document.getElementById('alertBox');
map.innerHTML = '';
map.appendChild(alert);
// SVG overlay for detected data points + projected best-fit trajectory
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = 'overlay';
svg.setAttribute('class', 'overlay-svg');
map.appendChild(svg);
// Legend (top-left, opposite the alert box)
const legend = document.createElement('div');
legend.className = 'legend';
legend.innerHTML = `
<div class="item"><span class="swatch" style="background:#f44336;"></span>Target</div>
<div class="item"><span class="swatch" style="background:#00e5ff;"></span>Detected points</div>
<div class="item"><span class="swatch" style="background:#ffeb3b;"></span>Best-fit projection</div>
<div class="item"><span class="swatch" style="background:#03a9f4;"></span>Interceptor drones</div>
`;
map.appendChild(legend);
}
// =====================================================================
// Target trajectory generation (matches Python)
// =====================================================================
function targetTrajectory(startPos, targetPos, cruiseAlt, speed, descentStart, terminalAlt) {
// start, target: [x, y] in km; cruiseAlt: m; speed: m/s; descentStart: km; terminalAlt: m
const dx = targetPos[0] - startPos[0];
const dy = targetPos[1] - startPos[1];
const horizDist = Math.sqrt(dx*dx + dy*dy); // km
if (horizDist < 1e-6) return [];
const dirX = dx / horizDist;
const dirY = dy / horizDist;
const descentStartDist = Math.max(0, horizDist - descentStart); // km from start
const descentDist = descentStart; // km
const descentDuration = (descentDist * 1000) / speed; // s
const totalDuration = (horizDist * 1000) / speed; // s
const traj = [];
let t = 0;
const dt = 0.5; // s (matching Python)
while (t <= totalDuration) {
const distTraveled = (speed * t) / 1000; // km
const distToTarget = horizDist - distTraveled;
let z; // meters
if (descentDist <= 0 || distTraveled < descentStartDist) {
z = cruiseAlt;
} else {
const descentProgress = Math.min(1.0, (distTraveled - descentStartDist) / descentDist);
z = cruiseAlt + (terminalAlt - cruiseAlt) * descentProgress;
}
const x = startPos[0] + dirX * distTraveled;
const y = startPos[1] + dirY * distTraveled;
traj.push({ x, y, z, t });
t += dt;
}
return traj;
}
// =====================================================================
// Radar detection and position estimation (matches Python)
// =====================================================================
function detectFromRadars(pos, radarPosList, radarRange, noiseFunc) {
const detections = [];
for (let i = 0; i < radarPosList.length; i++) {
const rpos = radarPosList[i];
const dist = Math.sqrt((pos[0]-rpos[0])**2 + (pos[1]-rpos[1])**2 + (pos[2]-rpos[2])**2);
if (dist < radarRange) {
const rangeMeas = dist + noiseFunc(MEASUREMENT_ERROR);
const azTrue = Math.atan2(pos[1]-rpos[1], pos[0]-rpos[0]);
const elTrue = Math.atan2(pos[2]-rpos[2], Math.sqrt((pos[0]-rpos[0])**2 + (pos[1]-rpos[1])**2));
const azMeas = azTrue + noiseFunc(MEASUREMENT_ERROR / Math.max(100, dist));
const elMeas = elTrue + noiseFunc(MEASUREMENT_ERROR / Math.max(100, dist));
detections.push({ range: rangeMeas, azimuth: azMeas, elevation: elMeas, radarIndex: i });
}
}
return detections;
}
function estimatePosition3D(detections, radarPosList) {
if (detections.length < 2) return null;
const estimates = [];
for (const det of detections) {
const ri = radarPosList[det.radarIndex];
const az = det.azimuth;
const el = det.elevation;
const r = det.range;
const dx = r * Math.cos(el) * Math.cos(az);
const dy = r * Math.cos(el) * Math.sin(az);
const dz = r * Math.sin(el);
estimates.push([ri[0]+dx, ri[1]+dy, ri[2]+dz]);
}
// Mean of estimates
const p = [
estimates.reduce((s, e) => s + e[0], 0) / estimates.length,
estimates.reduce((s, e) => s + e[1], 0) / estimates.length,
estimates.reduce((s, e) => s + e[2], 0) / estimates.length
];
return p;
}
// =====================================================================
// Trajectory estimation (linear regression, matches Python)
// =====================================================================
function estimateTrajectory(observations) {
// observations: array of {x, y, z, t}
if (observations.length < 2) return null;
const times = observations.map(o => o.t);
const xs = observations.map(o => o.x);
const ys = observations.map(o => o.y);
const zs = observations.map(o => o.z);
const n = observations.length;
const vel = [0, 0, 0];
for (let axis = 0; axis < 3; axis++) {
const pos = axis === 0 ? xs : (axis === 1 ? ys : zs);
// Least squares: pos = pos0 + vel*t
let sumT = 0, sumP = 0, sumT2 = 0, sumTP = 0;
for (let i = 0; i < n; i++) {
sumT += times[i];
sumP += pos[i];
sumT2 += times[i]*times[i];
sumTP += times[i]*pos[i];
}
const denom = n * sumT2 - sumT * sumT;
if (Math.abs(denom) < 1e-10) continue;
vel[axis] = (n * sumTP - sumT * sumP) / denom;
}
// R² calculation (use x-axis as representative)
const meanX = xs.reduce((s, v) => s + v, 0) / n;
const pos0 = xs[0] - vel[0] * times[0];
let ssTot = 0, ssRes = 0;
for (let i = 0; i < n; i++) {
ssTot += (xs[i] - meanX) ** 2;
const pred = pos0 + vel[0] * times[i];
ssRes += (xs[i] - pred) ** 2;
}
const r2 = ssTot > 0 ? 1 - ssRes / ssTot : 1;
return { vel, r2 };
}
// =====================================================================
// Intercept calculation (matches Python quadratic solver)
// =====================================================================
function computeInterceptPoint(dronePos, targetPos, targetVel, droneSpeed) {
const relPos = [targetPos[0]-dronePos[0], targetPos[1]-dronePos[1], targetPos[2]-dronePos[2]];
const a = targetVel[0]**2 + targetVel[1]**2 + targetVel[2]**2 - droneSpeed**2;
const b = 2 * (relPos[0]*targetVel[0] + relPos[1]*targetVel[1] + relPos[2]*targetVel[2]);
const c = relPos[0]**2 + relPos[1]**2 + relPos[2]**2;
let t;
if (Math.abs(a) < 1e-10) {
// Linear
if (Math.abs(b) < 1e-10) {
t = Math.sqrt(c) / droneSpeed;
} else {
t = -c / b;
}
} else {
const disc = b*b - 4*a*c;
if (disc < 0) {
t = Math.sqrt(c) / droneSpeed;
} else {
const sqrtDisc = Math.sqrt(disc);
const t1 = (-b + sqrtDisc) / (2*a);
const t2 = (-b - sqrtDisc) / (2*a);
const pos = [t1, t2].filter(x => x > 0);
t = pos.length > 0 ? Math.min(...pos) : Math.sqrt(c) / droneSpeed;
}
}
if (t <= 0) t = Math.sqrt(c) / droneSpeed;
const targetFuture = [
targetPos[0] + targetVel[0]*t,
targetPos[1] + targetVel[1]*t,
targetPos[2] + targetVel[2]*t
];
return { intercept: targetFuture, t };
}
// =====================================================================
// Geometry + overlay helpers
// =====================================================================
// Perpendicular distance (km) from point D to the line through P with direction V
// (P in km, V in km/s, D in km)
function perpDistToLine(P, V, D) {
const rel = [D[0]-P[0], D[1]-P[1], D[2]-P[2]];
const cross = [
rel[1]*V[2] - rel[2]*V[1],
rel[2]*V[0] - rel[0]*V[2],
rel[0]*V[1] - rel[1]*V[0]
];
const c = Math.sqrt(cross[0]*cross[0] + cross[1]*cross[1] + cross[2]*cross[2]);
const v = Math.sqrt(V[0]*V[0] + V[1]*V[1] + V[2]*V[2]);
return v > 1e-9 ? c / v : Math.sqrt(c);
}
// Draw detected data points + best-fit projected trajectory on the SVG overlay
function drawOverlay(observations, pos, velMs, tti, center) {
const svg = document.getElementById('overlay');
if (!svg) return;
while (svg.firstChild) svg.removeChild(svg.firstChild);
const toPx = (x, y) => [center.x + kmToPixels(x), center.y - kmToPixels(y)];
// Detected data points (estimated positions)
for (const o of observations) {
const [px, py] = toPx(o.x, o.y);
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', px);
c.setAttribute('cy', py);
c.setAttribute('r', 3);
c.setAttribute('fill', '#00e5ff');
c.setAttribute('fill-opacity', '0.85');
svg.appendChild(c);
}
// Best-fit projected trajectory: from current position forward to projected impact
if (velMs && tti > 0) {
const velKs = [velMs[0]/1000, velMs[1]/1000];
const end = [pos[0] + velKs[0]*tti, pos[1] + velKs[1]*tti];
const [x1, y1] = toPx(pos[0], pos[1]);
const [x2, y2] = toPx(end[0], end[1]);
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
line.setAttribute('stroke', '#ffeb3b');
line.setAttribute('stroke-width', '2');
line.setAttribute('stroke-dasharray', '6 4');
svg.appendChild(line);
}
}
// =====================================================================
// Map drawing
// =====================================================================
function drawCity(center, cityRadius) {
const city = document.createElement('div');
city.className = 'city';
city.style.left = center.x + 'px';
city.style.top = center.y + 'px';
city.style.width = kmToPixels(cityRadius * 2) + 'px';
city.style.height = kmToPixels(cityRadius * 2) + 'px';
city.textContent = 'CITY';
document.getElementById('map').appendChild(city);
}
function drawGrid(center, width, height) {
const map = document.getElementById('map');
for (let km = -20; km <= 20; km += 5) {
const x = center.x + kmToPixels(km);
if (x >= 0 && x <= width) {
const line = document.createElement('div');
line.className = 'grid-line';
line.style.left = x + 'px';
line.style.top = '0';
line.style.width = '1px';
line.style.height = height + 'px';
map.appendChild(line);
}
const y = center.y - kmToPixels(km);
if (y >= 0 && y <= height) {
const line = document.createElement('div');
line.className = 'grid-line';
line.style.top = y + 'px';
line.style.left = '0';
line.style.width = width + 'px';
line.style.height = '1px';
map.appendChild(line);
}
}
}
function setupDefense() {
const map = document.getElementById('map');
const center = getMapCenter();
const numRadars = parseInt(document.getElementById('numRadars').value);
const numDrones = parseInt(document.getElementById('numDrones').value);
const radarRange = parseFloat(document.getElementById('radarRange').value);
const cityRadius = parseFloat(document.getElementById('cityRadius').value);
const droneDefenseLine = parseFloat(document.getElementById('droneDefenseLine').value);
drawGrid(center, map.offsetWidth, map.offsetHeight);
drawCity(center, cityRadius);
const radarPositions = [];
for (let i = 0; i < numRadars; i++) {
const angle = 2 * Math.PI * i / numRadars;
const dist = cityRadius + 2;
const pos = [dist * Math.cos(angle), dist * Math.sin(angle), 0];
radarPositions.push(pos);
const rangeCircle = document.createElement('div');
rangeCircle.className = 'radar-range';
rangeCircle.id = `range-${i}`;
rangeCircle.style.left = (center.x + kmToPixels(pos[0])) + 'px';
rangeCircle.style.top = (center.y - kmToPixels(pos[1])) + 'px';
rangeCircle.style.width = kmToPixels(radarRange * 2) + 'px';
rangeCircle.style.height = kmToPixels(radarRange * 2) + 'px';
map.appendChild(rangeCircle);
const radar = document.createElement('div');
radar.className = 'radar inactive';
radar.id = `radar-${i}`;
radar.style.left = (center.x + kmToPixels(pos[0])) + 'px';
radar.style.top = (center.y - kmToPixels(pos[1])) + 'px';
map.appendChild(radar);
}
const dronePositions = [];
for (let i = 0; i < numDrones; i++) {
const angle = 2 * Math.PI * i / numDrones;
const dist = cityRadius + droneDefenseLine;
const pos = {
x: dist * Math.cos(angle),
y: dist * Math.sin(angle),
z: 0,
index: i,
state: 'waiting',
intercept: null
};
dronePositions.push(pos);
const drone = document.createElement('div');
drone.className = 'drone';
drone.id = `drone-${i}`;
drone.style.left = (center.x + kmToPixels(pos.x)) + 'px';
drone.style.top = (center.y - kmToPixels(pos.y)) + 'px';
map.appendChild(drone);
}
return { radarPositions, dronePositions };
}
// =====================================================================
// Closeup view (rotated to face trajectory direction)
// =====================================================================
function drawCloseup(targetPos, trajDir, errorBandMeters, dronePositions, launchedDrones) {
const closeup = document.getElementById('closeupMap');
closeup.innerHTML = '<span class="closeup-title">CLOSEUP (500m)</span>';
const cx = 100; // center of 200px box
const cy = 75; // center of 150px box
// Rotation angle to face trajectory direction
const angle = Math.atan2(trajDir[1], trajDir[0]);
// Error band (perpendicular to trajectory)
const bandHalf = kmToCloseup(errorBandMeters / 1000);
const bandHeight = Math.max(4, bandHalf * 2);
const band = document.createElement('div');
band.className = 'closeup-band';
band.style.left = (cx - 60) + 'px';
band.style.top = (cy - bandHeight / 2) + 'px';
band.style.width = 120 + 'px';
band.style.height = bandHeight + 'px';
band.style.transform = `rotate(${-angle}rad)`;
band.style.transformOrigin = 'center';
closeup.appendChild(band);
// Trajectory line
const traj = document.createElement('div');
traj.className = 'closeup-traj';
traj.style.left = (cx - 60) + 'px';
traj.style.top = cy + 'px';
traj.style.width = 120 + 'px';
traj.style.transform = `rotate(${-angle}rad)`;
traj.style.transformOrigin = 'left center';
closeup.appendChild(traj);
// Target dot
const dot = document.createElement('div');
dot.className = 'closeup-dot';
dot.style.left = cx + 'px';
dot.style.top = cy + 'px';
closeup.appendChild(dot);
// Drones within 250m
launchedDrones.forEach(i => {
const drone = dronePositions[i];
const dx = drone.x - targetPos[0]; // km
const dy = drone.y - targetPos[1]; // km
if (Math.abs(dx * 1000) <= 250 && Math.abs(dy * 1000) <= 250) {
const droneDot = document.createElement('div');
droneDot.className = 'closeup-drone';
droneDot.style.background = drone.state === 'ir-tracking' ? '#00bcd4' : '#03a9f4';
// Rotate coordinates to align with trajectory
const rx = dx * Math.cos(angle) + dy * Math.sin(angle);
const ry = -dx * Math.sin(angle) + dy * Math.cos(angle);
droneDot.style.left = (cx + kmToCloseup(rx)) + 'px';
droneDot.style.top = (cy - kmToCloseup(ry)) + 'px';
closeup.appendChild(droneDot);
}
});
}
// =====================================================================
// Data display
// =====================================================================
function updateData(status, radars, points, r2, speed, alt, errSigma, err95, tti, heading, droneAlt, irDrones) {
document.getElementById('dataStatus').textContent = status;
document.getElementById('dataRadars').textContent = radars;
document.getElementById('dataPoints').textContent = points;
document.getElementById('dataR2').textContent = r2 !== null ? r2.toFixed(4) : '--';
document.getElementById('dataSpeed').textContent = speed !== null ? `${speed.toFixed(0)} m/s` : '-- m/s';
document.getElementById('dataAltitude').textContent = alt !== null ? `${alt.toFixed(0)} m` : '-- m';
document.getElementById('dataErrorSigma').textContent = errSigma !== null ? `${errSigma.toFixed(1)} m` : '-- m';
document.getElementById('dataError95').textContent = err95 !== null ? `${err95.toFixed(1)} m` : '-- m';
document.getElementById('dataTTI').textContent = tti !== null ? `${tti.toFixed(1)} s` : '-- s';
document.getElementById('dataHeading').textContent = heading !== null ? `${heading.toFixed(1)}°` : '--°';
document.getElementById('dataDroneAlt').textContent = droneAlt !== null ? `${droneAlt.toFixed(0)} m` : '-- m';
document.getElementById('dataIRDrones').textContent = irDrones;
}
function updateIRData(dists, irSigma, ir95, combined) {
document.getElementById('irDist1').textContent = dists[0] !== null ? `${dists[0].toFixed(0)} m` : '-- m';
document.getElementById('irDist2').textContent = dists[1] !== null ? `${dists[1].toFixed(0)} m` : '-- m';
document.getElementById('irDist3').textContent = dists[2] !== null ? `${dists[2].toFixed(0)} m` : '-- m';
document.getElementById('irErrorSigma').textContent = irSigma !== null ? `${irSigma.toFixed(1)} m` : '-- m';
document.getElementById('irError95').textContent = ir95 !== null ? `${ir95.toFixed(1)} m` : '-- m';
document.getElementById('combinedError').textContent = combined !== null ? `${combined.toFixed(1)} m` : '-- m';
}
// =====================================================================
// Noise function (Gaussian)
// =====================================================================
function gaussianNoise(sigma) {
// Box-Muller transform
const u1 = Math.random();
const u2 = Math.random();
return sigma * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
// =====================================================================
// Simulation
// =====================================================================
function runSimulation() {
if (animationId) cancelAnimationFrame(animationId);
const { radarPositions, dronePositions } = resetSimulation();
const center = getMapCenter();
const attackStartDist = parseFloat(document.getElementById('attackStartDist').value);
const targetSpeed = parseFloat(document.getElementById('attackSpeed').value);
const cruiseAltMin = parseFloat(document.getElementById('cruiseAltMin').value);
const cruiseAltMax = parseFloat(document.getElementById('cruiseAltMax').value);
const descentStart = parseFloat(document.getElementById('descentStart').value);
const terminalAlt = parseFloat(document.getElementById('terminalAlt').value);
const radarRange = parseFloat(document.getElementById('radarRange').value);
const droneSpeed = parseFloat(document.getElementById('droneSpeed').value);
const irRange = parseFloat(document.getElementById('irRange').value) / 1000;
const cityRadius = parseFloat(document.getElementById('cityRadius').value);
// Meter-based radar positions for the detection/estimation pipeline
// (the radar math must run in meters, matching the Python reference)
const radarPositionsM = radarPositions.map(r => [r[0]*1000, r[1]*1000, r[2]*1000]);
// Random start
const startAngle = Math.random() * 2 * Math.PI;
const startPos = [
attackStartDist * Math.cos(startAngle),
attackStartDist * Math.sin(startAngle)
];
// Random target
const targetAngle = Math.random() * 2 * Math.PI;
const targetDist = Math.random() * cityRadius * 0.8;
const targetPos = [
targetDist * Math.cos(targetAngle),
targetDist * Math.sin(targetAngle)
];
// Random cruise altitude
const cruiseAlt = cruiseAltMin + Math.random() * (cruiseAltMax - cruiseAltMin);
// Generate trajectory
const trajectory = targetTrajectory(startPos, targetPos, cruiseAlt, targetSpeed, descentStart, terminalAlt);
if (trajectory.length === 0) return;
// Create target element
const targetEl = document.createElement('div');
targetEl.className = 'target';
document.getElementById('map').appendChild(targetEl);
// Altitude marker
const altMarker = document.createElement('div');
altMarker.className = 'altitude-marker';
document.getElementById('map').appendChild(altMarker);
// State
let time = 0;
let detectionTime = null;
let launchedDrones = [];
let collision = false;
let observations = [];
let latestVelMs = null;
let latestTti = null;
const dt = 0.5; // s
// Precompute true position lookup
function getTruePos(t) {
// Find closest trajectory point
let best = trajectory[0];
let bestDist = Infinity;
for (const p of trajectory) {
const d = Math.abs(p.t - t);
if (d < bestDist) {
bestDist = d;
best = p;
}
}
return best;
}
function update() {
time += dt;
const truePos = getTruePos(time);
const pos = [truePos.x, truePos.y, truePos.z / 1000]; // z in km for map
const posM = [pos[0] * 1000, pos[1] * 1000, pos[2] * 1000]; // meters, for radar math
targetEl.style.left = (center.x + kmToPixels(pos[0])) + 'px';
targetEl.style.top = (center.y - kmToPixels(pos[1])) + 'px';
// Altitude marker
altMarker.textContent = `${truePos.z.toFixed(0)}m`;
altMarker.style.left = (center.x + kmToPixels(pos[0]) + 15) + 'px';
altMarker.style.top = (center.y - kmToPixels(pos[1]) - 10) + 'px';
// Radar detection (in meters)
const detections = detectFromRadars(posM, radarPositionsM, radarRange * 1000, gaussianNoise);
const activeRadars = detections.map(d => d.radarIndex);
activeRadars.forEach(i => {
document.getElementById(`radar-${i}`).className = 'radar active';
document.getElementById(`range-${i}`).className = 'radar-range active';
});
if (detections.length > 0) {
observations.push({ x: pos[0], y: pos[1], z: pos[2] * 1000, t: time });
}
// First detection
if (detectionTime === null && observations.length >= 3) {
detectionTime = time;
// Show alert
const alertBox = document.getElementById('alertBox');
alertBox.className = 'alert-box active';
// Estimate position (meters)
const posEst = estimatePosition3D(detections, radarPositionsM);
if (posEst) {
observations[observations.length - 1] = {
x: posEst[0] / 1000, y: posEst[1] / 1000, z: posEst[2], t: time
};
}
// Estimate trajectory
const traj = estimateTrajectory(observations);
if (traj) {
// Velocity in m/s (observations are in km for x,y)
const velMs = [traj.vel[0] * 1000, traj.vel[1] * 1000, traj.vel[2]];
const speedEst = Math.sqrt(velMs[0]**2 + velMs[1]**2 + velMs[2]**2);
// Time to impact
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
const tti = horizDist / (speedEst / 1000);
latestVelMs = velMs;
latestTti = tti;
// Select drones closest to the projected trajectory line,
// preferring those with a viable intercept (t < tti).
// Intercepts are computed in meters (consistent units) and stored back in km.
const velKs = [velMs[0]/1000, velMs[1]/1000, velMs[2]/1000];
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
const candidates = [];
for (let i = 0; i < dronePositions.length; i++) {
const dronePos = dronePositions[i];
const dronePosM = [dronePos.x*1000, dronePos.y*1000, dronePos.z*1000];
const result = computeInterceptPoint(dronePosM, posM, velMs, droneSpeed);
const dist = perpDistToLine(pos, velKs, [dronePos.x, dronePos.y, dronePos.z]);
const viable = (result.t > 0 && result.t < tti);
candidates.push({
i,
t: result.t,
intercept: [result.intercept[0]/1000, result.intercept[1]/1000, result.intercept[2]/1000],
dist,
viable
});
}
const viable = candidates.filter(c => c.viable).sort((a, b) => a.dist - b.dist);
const nonViable = candidates.filter(c => !c.viable).sort((a, b) => a.dist - b.dist);
const chosen = viable.slice(0, 3);
for (const nv of nonViable) {
if (chosen.length >= 3) break;
chosen.push(nv);
}
launchedDrones = chosen.map(c => c.i);
for (const c of chosen) {
dronePositions[c.i].intercept = c.intercept;
dronePositions[c.i].state = 'flying';
document.getElementById(`drone-${c.i}`).className = 'drone flying';
}
// Update data display
const n = observations.length;
const errSigma = MEASUREMENT_ERROR / Math.sqrt(n);
const err95 = 1.96 * errSigma;
const heading = (Math.atan2(velMs[1], velMs[0]) * 180 / Math.PI + 360) % 360;
updateData('TRACKING', activeRadars.length, n, traj.r2, speedEst, pos[2]*1000, errSigma, err95, tti, heading, 0, 0);
}
}
// Continuous updates
if (detectionTime !== null && !collision && observations.length >= 2) {
const traj = estimateTrajectory(observations);
if (traj) {
const velMs = [traj.vel[0] * 1000, traj.vel[1] * 1000, traj.vel[2]];
const speedEst = Math.sqrt(velMs[0]**2 + velMs[1]**2 + velMs[2]**2);
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
const tti = horizDist / (speedEst / 1000);
latestVelMs = velMs;
latestTti = tti;
const n = observations.length;
const errSigma = MEASUREMENT_ERROR / Math.sqrt(n);
const err95 = 1.96 * errSigma;
const heading = (Math.atan2(velMs[1], velMs[0]) * 180 / Math.PI + 360) % 360;
// Re-target drones
if ((time - detectionTime) % 5 < dt) {
const posM = [pos[0]*1000, pos[1]*1000, pos[2]*1000];
for (const i of launchedDrones) {
const drone = dronePositions[i];
if (drone.state === 'flying') {
const dronePosM = [drone.x*1000, drone.y*1000, drone.z*1000];
const result = computeInterceptPoint(dronePosM, posM, velMs, droneSpeed);
drone.intercept = [result.intercept[0]/1000, result.intercept[1]/1000, result.intercept[2]/1000];
}
}
}
updateData('TRACKING', activeRadars.length, n, traj.r2, speedEst, pos[2]*1000, errSigma, err95, tti, heading, 0, 0);
}
}
// Update drones
if (launchedDrones.length > 0 && !collision) {
let irDists = [null, null, null];
let irCount = 0;
let maxDroneAlt = 0;
launchedDrones.forEach((i, idx) => {
const drone = dronePositions[i];
if (drone.state === 'flying' || drone.state === 'ir-tracking') {
const intercept = drone.intercept;
if (intercept) {
const delta = [intercept[0]-drone.x, intercept[1]-drone.y, intercept[2]-drone.z];
const dist = Math.sqrt(delta[0]**2 + delta[1]**2 + delta[2]**2);
if (dist > 0.001) {
const step = droneSpeed * dt / 1000; // km
drone.x += (delta[0] / dist) * step;
drone.y += (delta[1] / dist) * step;
drone.z += (delta[2] / dist) * step;
}
}
// 3D distance to target
const dx = pos[0] - drone.x;
const dy = pos[1] - drone.y;
const dz = pos[2] - drone.z;
const dist3D = Math.sqrt(dx*dx + dy*dy + dz*dz); // km
if (drone.state === 'flying' && dist3D < irRange) {
drone.state = 'ir-tracking';
document.getElementById(`drone-${i}`).className = 'drone ir-tracking';
}
if (drone.state === 'ir-tracking') {
irDists[idx] = dist3D * 1000; // meters
irCount++;
}
maxDroneAlt = Math.max(maxDroneAlt, drone.z * 1000);
// Update drone position on map
const droneEl = document.getElementById(`drone-${i}`);
droneEl.style.left = (center.x + kmToPixels(drone.x)) + 'px';
droneEl.style.top = (center.y - kmToPixels(drone.y)) + 'px';
// Collision check
if (dist3D * 1000 < 20) { // 20 m
collision = true;
const alertBox = document.getElementById('alertBox');
alertBox.textContent = 'TARGET DESTROYED';
alertBox.className = 'alert-box active success';
updateData('DESTROYED', activeRadars.length, observations.length, null, null, null, null, null, null, null, maxDroneAlt, irCount);
}
}
});
if (irCount > 0) {
const irSigma = IR_ERROR / Math.sqrt(irCount);
const ir95 = 1.96 * irSigma;
const radarSigma = MEASUREMENT_ERROR / Math.sqrt(observations.length);
const combined = Math.sqrt(radarSigma**2 + irSigma**2);
updateIRData(irDists, irSigma, ir95, combined);
}
// Update closeup
if (detectionTime !== null) {
const traj = estimateTrajectory(observations);
if (traj) {
const velMs = [traj.vel[0] * 1000, traj.vel[1] * 1000, traj.vel[2]];
const n = observations.length;
const err95 = 1.96 * MEASUREMENT_ERROR / Math.sqrt(n);
const trajDir = [velMs[0], velMs[1]];
drawCloseup(pos, trajDir, err95, dronePositions, launchedDrones);
}
}
}
// Draw detected data points + best-fit projected trajectory
if (observations.length > 0 && latestVelMs) {
drawOverlay(observations, pos, latestVelMs, latestTti, center);
}
// Target reached city
const horizDist = Math.sqrt(pos[0]**2 + pos[1]**2);
if (horizDist < cityRadius && !collision) {
const alertBox = document.getElementById('alertBox');
alertBox.textContent = 'IMPACT';
alertBox.className = 'alert-box active';
updateData('IMPACT', activeRadars.length, observations.length, null, null, null, null, null, null, null, 0, 0);
}
if (!collision && horizDist > cityRadius) {
animationId = requestAnimationFrame(update);
}
}
document.getElementById('alertBox').className = 'alert-box';
animationId = requestAnimationFrame(update);
}
function resetSimulation() {
if (animationId) cancelAnimationFrame(animationId);
clearMap();
const center = getMapCenter();
const defense = setupDefense();
const alertBox = document.getElementById('alertBox');
alertBox.className = 'alert-box';
alertBox.textContent = 'ALERT!';
// Clear closeup
const closeup = document.getElementById('closeupMap');
closeup.innerHTML = '<span class="closeup-title">CLOSEUP (500m)</span>';
updateData('STANDBY', 0, 0, null, null, null, null, null, null, null, null, 0);
updateIRData([null, null, null], null, null, null);
return defense;
}
// Load Monte Carlo stats (per preset) and populate the table
let statsData = {};
function presetDisplayName(key) {
const names = {
'shahed-136': 'Shahed-136',
'shahed-238': 'Shahed-238',
'cruise-missile': 'Cruise Missile',
'ballistic-reentry': 'Ballistic (Re-entry)',
'prop-drone': 'Prop Drone'
};
return names[key] || key;
}
function fmtTime(v) { return v ? `${v.toFixed(1)} s` : '--'; }
function renderStatsTable(activePreset) {
const tbody = document.getElementById('statsBody');
tbody.innerHTML = '';
const presetOrder = ['shahed-136', 'shahed-238', 'cruise-missile', 'ballistic-reentry', 'prop-drone'];
for (const key of presetOrder) {
const s = statsData[key];
if (!s) continue;
const tr = document.createElement('tr');
if (key === activePreset) tr.className = 'highlight';
const rate = s.interception_rate;
const rateColor = rate >= 80 ? '#4caf50' : (rate >= 40 ? '#ff9800' : '#f44336');
tr.innerHTML = `
<td class="preset-name">${presetDisplayName(key)}</td>
<td style="color:${rateColor};font-weight:bold;">${rate.toFixed(0)}%</td>
<td>${fmtTime(s.avg_detection_time)}</td>
<td>${fmtTime(s.avg_collision_time)}</td>
<td>${fmtTime(s.avg_impact_time)}</td>
`;
tbody.appendChild(tr);
}
}
fetch('montecarlo_stats.json')
.then(r => r.json())
.then(stats => {
statsData = stats;
renderStatsTable(document.getElementById('presetSelect').value);
})
.catch(() => {});
// Preset dropdown handler: prefill attack parameters on change
document.getElementById('presetSelect').addEventListener('change', (e) => {
applyPreset(e.target.value);
resetSimulation();
renderStatsTable(e.target.value);
});
// Apply the default preset on load
applyPreset(document.getElementById('presetSelect').value);
resetSimulation();
</script>
</body>
</html>