Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ADS-B RADAR — Live Demo (Simulation)</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> | |
| <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> | |
| <style> | |
| :root { | |
| --radar-green: #00ff88; | |
| --radar-dim: #00aa55; | |
| --radar-glow: rgba(0,255,136,0.15); | |
| --bg-deep: #030d06; | |
| --bg-panel: #060f08; | |
| --bg-card: #0a1a0d; | |
| --border: rgba(0,255,136,0.2); | |
| --border-bright: rgba(0,255,136,0.5); | |
| --alert-red: #ff3355; | |
| --alert-yellow: #ffcc00; | |
| --text-dim: rgba(0,255,136,0.5); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: var(--bg-deep); | |
| color: var(--radar-green); | |
| font-family: 'Share Tech Mono', monospace; | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* ── SCANLINE OVERLAY ── */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: repeating-linear-gradient( | |
| 0deg, | |
| transparent, | |
| transparent 2px, | |
| rgba(0,0,0,0.08) 2px, | |
| rgba(0,0,0,0.08) 4px | |
| ); | |
| pointer-events: none; | |
| z-index: 9999; | |
| } | |
| /* ── HEADER ── */ | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 8px 20px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| flex-shrink: 0; | |
| } | |
| .header-left { display: flex; align-items: center; gap: 16px; } | |
| .logo { | |
| font-family: 'Rajdhani', sans-serif; | |
| font-size: 22px; | |
| font-weight: 700; | |
| letter-spacing: 4px; | |
| text-shadow: 0 0 20px var(--radar-green); | |
| } | |
| .logo span { color: var(--text-dim); font-weight: 300; } | |
| .status-pill { | |
| display: flex; align-items: center; gap: 6px; | |
| font-size: 11px; letter-spacing: 2px; | |
| padding: 3px 10px; | |
| border: 1px solid var(--border); | |
| background: var(--radar-glow); | |
| } | |
| .dot-live { | |
| width: 7px; height: 7px; border-radius: 50%; | |
| background: var(--radar-green); | |
| box-shadow: 0 0 8px var(--radar-green); | |
| animation: pulse-dot 1.2s ease-in-out infinite; | |
| } | |
| @keyframes pulse-dot { | |
| 0%,100% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| } | |
| .header-stats { | |
| display: flex; gap: 24px; | |
| font-size: 11px; letter-spacing: 1px; | |
| } | |
| .hstat { display: flex; flex-direction: column; align-items: flex-end; } | |
| .hstat-val { font-size: 20px; font-weight: 700; line-height: 1; text-shadow: 0 0 12px var(--radar-green); } | |
| .hstat-label { color: var(--text-dim); font-size: 10px; letter-spacing: 2px; } | |
| .utc-clock { | |
| font-size: 24px; | |
| letter-spacing: 3px; | |
| text-shadow: 0 0 15px var(--radar-green); | |
| border-left: 1px solid var(--border); | |
| padding-left: 20px; | |
| } | |
| /* ── MAIN LAYOUT ── */ | |
| .main { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| /* ── LEFT PANEL ── */ | |
| .left-panel { | |
| width: 280px; | |
| flex-shrink: 0; | |
| background: var(--bg-panel); | |
| border-right: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .panel-title { | |
| padding: 10px 16px; | |
| font-size: 10px; | |
| letter-spacing: 3px; | |
| color: var(--text-dim); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .panel-title span { color: var(--radar-green); } | |
| /* flight list */ | |
| .flight-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 4px 0; | |
| } | |
| .flight-list::-webkit-scrollbar { width: 3px; } | |
| .flight-list::-webkit-scrollbar-track { background: transparent; } | |
| .flight-list::-webkit-scrollbar-thumb { background: var(--border); } | |
| .flight-item { | |
| padding: 8px 16px; | |
| border-bottom: 1px solid rgba(0,255,136,0.05); | |
| cursor: pointer; | |
| transition: background 0.15s; | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| gap: 4px; | |
| align-items: center; | |
| } | |
| .flight-item:hover { background: var(--radar-glow); } | |
| .flight-item.selected { background: rgba(0,255,136,0.1); border-left: 2px solid var(--radar-green); } | |
| .fi-callsign { | |
| font-size: 14px; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| text-shadow: 0 0 8px var(--radar-green); | |
| } | |
| .fi-hex { font-size: 9px; color: var(--text-dim); letter-spacing: 1px; } | |
| .fi-alt { | |
| font-size: 12px; | |
| text-align: right; | |
| line-height: 1.2; | |
| } | |
| .fi-alt-val { color: var(--radar-green); } | |
| .fi-speed { font-size: 10px; color: var(--text-dim); } | |
| .fi-track { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| gap: 12px; | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| margin-top: 2px; | |
| } | |
| /* ── MAP AREA ── */ | |
| .map-wrapper { | |
| flex: 1; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #map { | |
| width: 100%; | |
| height: 100%; | |
| background: #020a04 ; | |
| } | |
| /* Custom Leaflet dark tiles override */ | |
| .leaflet-container { background: #020a04; } | |
| /* MAP OVERLAY ELEMENTS */ | |
| .map-overlay-tl, .map-overlay-tr, .map-overlay-bl, .map-overlay-br { | |
| position: absolute; | |
| z-index: 1000; | |
| pointer-events: none; | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| color: var(--text-dim); | |
| } | |
| .map-overlay-tl { top: 12px; left: 12px; } | |
| .map-overlay-tr { top: 12px; right: 12px; text-align: right; } | |
| .map-overlay-bl { bottom: 30px; left: 12px; } | |
| .map-overlay-br { bottom: 30px; right: 12px; text-align: right; } | |
| /* SWEEP ANIMATION */ | |
| .radar-sweep { | |
| position: absolute; | |
| top: 50%; left: 50%; | |
| width: 1px; height: 1px; | |
| z-index: 999; | |
| pointer-events: none; | |
| display: none; /* hidden for map mode */ | |
| } | |
| /* ── RIGHT PANEL ── */ | |
| .right-panel { | |
| width: 260px; | |
| flex-shrink: 0; | |
| background: var(--bg-panel); | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* SELECTED AIRCRAFT DETAIL */ | |
| .ac-detail { | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .ac-detail-callsign { | |
| font-family: 'Rajdhani', sans-serif; | |
| font-size: 28px; | |
| font-weight: 700; | |
| letter-spacing: 3px; | |
| text-shadow: 0 0 20px var(--radar-green); | |
| line-height: 1; | |
| margin-bottom: 4px; | |
| } | |
| .ac-detail-hex { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| letter-spacing: 2px; | |
| margin-bottom: 12px; | |
| } | |
| .data-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .data-cell { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| padding: 8px 10px; | |
| } | |
| .dc-label { | |
| font-size: 8px; | |
| color: var(--text-dim); | |
| letter-spacing: 2px; | |
| margin-bottom: 3px; | |
| } | |
| .dc-value { | |
| font-size: 16px; | |
| font-weight: 700; | |
| text-shadow: 0 0 8px var(--radar-green); | |
| line-height: 1; | |
| } | |
| .dc-unit { font-size: 10px; color: var(--text-dim); } | |
| /* HEADING COMPASS */ | |
| .compass-wrap { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 14px 16px; | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| } | |
| .compass-label { | |
| font-size: 9px; | |
| letter-spacing: 3px; | |
| color: var(--text-dim); | |
| margin-bottom: 8px; | |
| } | |
| .compass { | |
| width: 90px; height: 90px; | |
| position: relative; | |
| } | |
| .compass svg { width: 100%; height: 100%; } | |
| /* LOG */ | |
| .log-section { flex: 1; display: flex; flex-direction: column; overflow: hidden; } | |
| .log-entries { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 6px 0; | |
| } | |
| .log-entries::-webkit-scrollbar { width: 3px; } | |
| .log-entries::-webkit-scrollbar-thumb { background: var(--border); } | |
| .log-entry { | |
| padding: 5px 16px; | |
| font-size: 10px; | |
| border-bottom: 1px solid rgba(0,255,136,0.04); | |
| display: flex; | |
| gap: 8px; | |
| color: var(--text-dim); | |
| } | |
| .log-time { color: var(--radar-green); flex-shrink: 0; } | |
| .log-msg { flex: 1; } | |
| .log-entry.alert { color: var(--alert-yellow); } | |
| .log-entry.alert .log-time { color: var(--alert-yellow); } | |
| /* FOOTER */ | |
| footer { | |
| padding: 5px 20px; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg-panel); | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| letter-spacing: 2px; | |
| flex-shrink: 0; | |
| } | |
| /* LEAFLET CUSTOM STYLES */ | |
| .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.65) saturate(0.6) contrast(0.9); } | |
| .leaflet-popup-content-wrapper { | |
| background: var(--bg-panel); | |
| border: 1px solid var(--border-bright); | |
| border-radius: 0; | |
| color: var(--radar-green); | |
| font-family: 'Share Tech Mono', monospace; | |
| font-size: 11px; | |
| box-shadow: 0 0 20px var(--radar-glow); | |
| } | |
| .leaflet-popup-tip { background: var(--border-bright); } | |
| .leaflet-popup-close-button { color: var(--radar-green) ; } | |
| /* No-selection state */ | |
| .no-selection { | |
| padding: 20px 16px; | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| letter-spacing: 1px; | |
| line-height: 1.8; | |
| text-align: center; | |
| } | |
| .no-selection::before { | |
| content: '[ SELECT AIRCRAFT ]'; | |
| display: block; | |
| font-size: 9px; | |
| letter-spacing: 3px; | |
| margin-bottom: 12px; | |
| color: rgba(0,255,136,0.3); | |
| } | |
| /* data source toggle */ | |
| .source-toggle { | |
| display: flex; | |
| gap: 0; | |
| border: 1px solid var(--border); | |
| overflow: hidden; | |
| pointer-events: all; | |
| } | |
| .src-btn { | |
| padding: 4px 12px; | |
| font-size: 9px; | |
| letter-spacing: 2px; | |
| background: transparent; | |
| color: var(--text-dim); | |
| border: none; | |
| cursor: pointer; | |
| font-family: 'Share Tech Mono', monospace; | |
| transition: all 0.15s; | |
| } | |
| .src-btn.active { | |
| background: var(--radar-glow); | |
| color: var(--radar-green); | |
| } | |
| .src-btn:hover { color: var(--radar-green); } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- HEADER --> | |
| <header> | |
| <div class="header-left"> | |
| <div class="logo">ADS-B <span>RADAR</span></div> | |
| <div class="status-pill"> | |
| <div class="dot-live"></div> | |
| <span id="src-label">SIMULATION (static demo)</span> | |
| </div> | |
| <div class="source-toggle"> | |
| <button class="src-btn active" data-src="sim" onclick="setSource('sim')">SIM</button> | |
| <button class="src-btn" data-src="live" onclick="setSource('live')">LIVE</button> | |
| </div> | |
| </div> | |
| <div class="header-stats"> | |
| <div class="hstat"> | |
| <div class="hstat-val" id="stat-count">0</div> | |
| <div class="hstat-label">AIRCRAFT</div> | |
| </div> | |
| <div class="hstat"> | |
| <div class="hstat-val" id="stat-msgs">0</div> | |
| <div class="hstat-label">UPDATES/S</div> | |
| </div> | |
| <div class="hstat"> | |
| <div class="hstat-val" id="stat-range">—</div> | |
| <div class="hstat-label">MAX ALT (ft)</div> | |
| </div> | |
| </div> | |
| <div class="utc-clock" id="utc-clock">00:00:00Z</div> | |
| </header> | |
| <!-- MAIN --> | |
| <div class="main"> | |
| <!-- LEFT: FLIGHT LIST --> | |
| <div class="left-panel"> | |
| <div class="panel-title"> | |
| TRACKED FLIGHTS | |
| <span id="list-count">0</span> | |
| </div> | |
| <div class="flight-list" id="flight-list"> | |
| <!-- populated by JS --> | |
| </div> | |
| </div> | |
| <!-- MAP --> | |
| <div class="map-wrapper"> | |
| <div id="map"></div> | |
| <div class="map-overlay-tl"> | |
| KUWAIT ACC<br> | |
| FIR: OKAC<br> | |
| FREQ: 1090 MHz | |
| </div> | |
| <div class="map-overlay-tr" id="overlay-tr"> | |
| MODE: <span style="color:var(--radar-green)">SURVEILLANCE</span><br> | |
| RANGE: 350 NM<br> | |
| FILTER: ALL | |
| </div> | |
| <div class="map-overlay-bl" id="overlay-bl"> | |
| LAT: 29.37°N LON: 47.98°E | |
| </div> | |
| <div class="map-overlay-br"> | |
| © ADS-B TRACKER — KCST FYP<br> | |
| POWERED BY RTL-SDR + DUMP1090 | |
| </div> | |
| </div> | |
| <!-- RIGHT: DETAIL PANEL --> | |
| <div class="right-panel"> | |
| <div class="panel-title">AIRCRAFT DETAIL</div> | |
| <div id="ac-detail-area"> | |
| <div class="no-selection"> | |
| Click any aircraft on the map or in the list to view detailed telemetry | |
| </div> | |
| </div> | |
| <div class="compass-wrap" id="compass-wrap" style="display:none"> | |
| <div class="compass-label">HEADING / TRACK</div> | |
| <div class="compass"> | |
| <svg viewBox="0 0 90 90"> | |
| <!-- Outer ring --> | |
| <circle cx="45" cy="45" r="42" fill="none" stroke="rgba(0,255,136,0.2)" stroke-width="1"/> | |
| <circle cx="45" cy="45" r="38" fill="none" stroke="rgba(0,255,136,0.08)" stroke-width="1"/> | |
| <!-- Cardinal ticks --> | |
| <line x1="45" y1="3" x2="45" y2="12" stroke="rgba(0,255,136,0.6)" stroke-width="1.5"/> | |
| <line x1="87" y1="45" x2="78" y2="45" stroke="rgba(0,255,136,0.3)" stroke-width="1"/> | |
| <line x1="45" y1="87" x2="45" y2="78" stroke="rgba(0,255,136,0.3)" stroke-width="1"/> | |
| <line x1="3" y1="45" x2="12" y2="45" stroke="rgba(0,255,136,0.3)" stroke-width="1"/> | |
| <!-- Labels --> | |
| <text x="45" y="20" text-anchor="middle" fill="rgba(0,255,136,0.8)" font-family="Share Tech Mono" font-size="7">N</text> | |
| <text x="72" y="49" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">E</text> | |
| <text x="45" y="75" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">S</text> | |
| <text x="18" y="49" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">W</text> | |
| <!-- Needle --> | |
| <line id="compass-needle" | |
| x1="45" y1="45" x2="45" y2="10" | |
| stroke="#00ff88" stroke-width="2" | |
| stroke-linecap="round" | |
| transform-origin="45 45" | |
| style="filter: drop-shadow(0 0 4px #00ff88)" | |
| /> | |
| <circle cx="45" cy="45" r="3" fill="#00ff88" style="filter: drop-shadow(0 0 6px #00ff88)"/> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="log-section"> | |
| <div class="panel-title">SYSTEM LOG <span id="log-count">0</span></div> | |
| <div class="log-entries" id="log-entries"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- FOOTER --> | |
| <footer> | |
| <span>STATIC DEMO | <a href="https://github.com/eahmeddarwish/adsb-flight-tracker" target="_blank" style="color:var(--radar-green)">Full project + LIVE mode on GitHub</a></span> | |
| <span id="footer-time">LAST UPDATE: —</span> | |
| <span>KCST · FYP · ADS-B TRACKER v2.0</span> | |
| </footer> | |
| <!-- ───────────────────────── SCRIPTS ───────────────────────── --> | |
| <script> | |
| // ── CONFIG ────────────────────────────────────────────────── | |
| const CONFIG = { | |
| // Static demo build: no backend at all. Aircraft are generated entirely | |
| // client-side (see SIM_ENGINE below). The real project (GitHub) runs a | |
| // Flask server that also supports real RTL-SDR hardware in LIVE mode. | |
| center: [29.37, 47.98], | |
| zoom: 7, | |
| refresh: 1000, | |
| repo_url: 'https://github.com/eahmeddarwish/adsb-flight-tracker' | |
| }; | |
| // ── CLIENT-SIDE SIMULATION ENGINE ───────────────────────────── | |
| // A JS port of app/simulate.py's motion model, so this static build needs | |
| // no server at all. Same math, same look, just running in the browser. | |
| const SIM_ENGINE = (() => { | |
| let planes = [ | |
| {hex:'A1B2C3', flight:'KAC123', lat:29.37, lon:47.98, alt:32000, speed:430, track:120}, | |
| {hex:'D4E5F6', flight:'UAE201', lat:29.45, lon:48.10, alt:36000, speed:460, track:300}, | |
| {hex:'G7H8I9', flight:'QTR900', lat:29.20, lon:47.80, alt:38000, speed:480, track:45}, | |
| ]; | |
| function step() { | |
| for (const p of planes) { | |
| const dir = p.track * Math.PI / 180; | |
| p.lat += 0.01 * Math.cos(dir); | |
| p.lon += 0.01 * Math.sin(dir); | |
| if (p.lat < 24.0 || p.lat > 34.0) p.track = (p.track + 180) % 360; | |
| if (p.lon < 43.0 || p.lon > 53.0) p.track = (p.track + 180) % 360; | |
| } | |
| return planes; | |
| } | |
| return { step }; | |
| })(); | |
| let currentSource = 'sim'; | |
| let selectedHex = null; | |
| let planeMarkers = {}; | |
| let planePaths = {}; | |
| let planeData = {}; | |
| let updateCount = 0; | |
| let logCount = 0; | |
| // ── MAP INIT ──────────────────────────────────────────────── | |
| const map = L.map('map', { | |
| zoomControl: true, | |
| attributionControl: false | |
| }).setView(CONFIG.center, CONFIG.zoom); | |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); | |
| // update coords overlay on mouse move | |
| map.on('mousemove', e => { | |
| document.getElementById('overlay-bl').innerHTML = | |
| `LAT: ${e.latlng.lat.toFixed(4)}°N LON: ${e.latlng.lng.toFixed(4)}°E`; | |
| }); | |
| // ── PLANE SVG ICON (real aircraft top-down shape) ──────────── | |
| function makePlaneIcon(track, isSelected) { | |
| const color = isSelected ? '#ffffff' : '#00ff88'; | |
| const glow = isSelected ? '0 0 10px #fff, 0 0 20px #fff' : '0 0 8px #00ff88, 0 0 16px #00aa55'; | |
| const shadow = isSelected ? 'drop-shadow(0 0 5px #fff)' : 'drop-shadow(0 0 4px #00ff88)'; | |
| // Real top-down aircraft path from plane.svg (scaled to 44x44 viewBox) | |
| const planePath = `M2504 5083 c-69 -68 -128 -238 -159 -453 -15 -107 -34 -457 -38 -720 | |
| l-2 -144 -265 -223 -265 -223 -5 130 c-4 101 -8 133 -20 140 -22 14 -184 12 | |
| -198 -2 -9 -9 -12 -76 -12 -239 l0 -226 -232 -195 -233 -196 -3 129 c-3 129 | |
| -3 130 -28 139 -33 13 -145 13 -178 0 l-26 -10 0 -227 0 -228 -358 -301 c-196 | |
| -165 -367 -315 -380 -334 -40 -59 -54 -127 -59 -270 l-4 -135 29 32 c22 26 | |
| 113 70 432 212 223 99 411 181 419 183 8 2 17 -10 23 -29 11 -40 35 -43 44 -5 | |
| 16 73 17 75 115 119 52 23 97 42 102 42 4 1 7 -8 7 -18 0 -62 35 -68 50 -8 6 | |
| 23 12 47 14 52 3 8 118 63 191 91 7 3 17 -12 24 -36 17 -58 38 -51 51 18 l12 | |
| 59 366 162 c202 90 372 165 379 168 8 3 12 -143 16 -584 5 -619 11 -714 56 | |
| -876 l19 -68 -317 -262 c-174 -143 -322 -268 -329 -277 -76 -89 -134 -226 | |
| -148 -348 -11 -98 -18 -99 454 81 389 148 407 154 414 133 3 -11 18 -77 33 | |
| -146 54 -252 76 -252 130 0 15 69 30 135 33 146 7 21 25 15 414 -133 472 -180 | |
| 465 -179 454 -81 -14 122 -72 259 -148 348 -7 9 -155 134 -329 277 l-317 262 | |
| 19 68 c45 162 51 257 56 876 4 441 8 587 16 584 7 -3 177 -78 379 -168 l366 | |
| -162 12 -59 c13 -69 34 -76 51 -18 7 24 17 39 24 36 73 -28 188 -83 191 -91 2 | |
| -5 8 -29 14 -52 15 -60 50 -54 50 8 0 10 3 19 8 18 4 0 49 -19 101 -42 98 -44 | |
| 99 -46 115 -119 9 -38 33 -35 44 5 6 19 15 31 23 29 8 -2 196 -84 419 -183 | |
| 319 -142 410 -186 432 -212 l29 -32 -4 135 c-5 139 -18 205 -56 267 -11 17 | |
| -182 168 -380 335 l-361 303 0 227 c0 214 -1 228 -19 238 -25 13 -151 13 -185 | |
| 0 -25 -9 -25 -10 -28 -139 l-3 -129 -232 196 -233 195 0 226 c0 163 -3 230 | |
| -12 239 -14 14 -176 16 -198 2 -12 -7 -16 -39 -20 -140 l-5 -130 -265 223 | |
| -265 223 -2 144 c-4 263 -23 613 -38 720 -20 138 -58 280 -96 358 -35 71 -90 | |
| 132 -119 132 -11 0 -36 -16 -56 -37z`; | |
| const svg = `<svg xmlns="http://www.w3.org/2000/svg" | |
| viewBox="0 0 512 512" width="42" height="42" | |
| style="transform:rotate(${track}deg); filter:${shadow}; transform-origin:center;"> | |
| <g transform="translate(0,512) scale(0.1,-0.1)"> | |
| <path d="${planePath}" fill="${color}" fill-opacity="0.95"/> | |
| </g> | |
| </svg>`; | |
| return L.divIcon({ | |
| html: svg, | |
| iconSize: [42, 42], | |
| iconAnchor: [21, 21], | |
| className: '' | |
| }); | |
| } | |
| // ── PARSE AIRCRAFT (handles both dump1090 and sim formats) ─── | |
| function parseAircraft(raw) { | |
| return { | |
| hex: (raw.hex || raw.icao || '??????').toUpperCase(), | |
| flight: (raw.flight || raw.callsign || '').trim() || '——', | |
| lat: raw.lat ?? raw.latitude ?? null, | |
| lon: raw.lon ?? raw.longitude ?? null, | |
| alt: raw.alt ?? raw.alt_baro ?? raw.altitude ?? 0, // always numeric, never undefined | |
| speed: raw.speed ?? raw.gs ?? 0, | |
| track: raw.track ?? raw.true_heading ?? 0, | |
| }; | |
| } | |
| // ── UPDATE MARKERS ─────────────────────────────────────────── | |
| function updateMap(aircraft) { | |
| const seen = new Set(); | |
| aircraft.forEach(raw => { | |
| const ac = parseAircraft(raw); | |
| if (ac.lat === null || ac.lon === null) return; | |
| seen.add(ac.hex); | |
| planeData[ac.hex] = ac; | |
| if (planeMarkers[ac.hex]) { | |
| planeMarkers[ac.hex].setLatLng([ac.lat, ac.lon]); | |
| planeMarkers[ac.hex].setIcon(makePlaneIcon(ac.track, ac.hex === selectedHex)); | |
| } else { | |
| const marker = L.marker([ac.lat, ac.lon], { | |
| icon: makePlaneIcon(ac.track, false), | |
| zIndexOffset: 1000 | |
| }).addTo(map); | |
| marker.on('click', () => selectAircraft(ac.hex)); | |
| planeMarkers[ac.hex] = marker; | |
| planePaths[ac.hex] = []; | |
| addLog(`CONTACT: ${ac.flight} [${ac.hex}] ALT:${ac.alt}ft`); | |
| } | |
| // trail dots | |
| planePaths[ac.hex].push([ac.lat, ac.lon]); | |
| if (planePaths[ac.hex].length > 80) planePaths[ac.hex].shift(); | |
| // draw trail — two layers: glow + solid line | |
| if (planeMarkers[ac.hex]._trail) map.removeLayer(planeMarkers[ac.hex]._trail); | |
| if (planeMarkers[ac.hex]._trailGlow) map.removeLayer(planeMarkers[ac.hex]._trailGlow); | |
| if (planePaths[ac.hex].length > 1) { | |
| const isSelected = ac.hex === selectedHex; | |
| const trailColor = isSelected ? 'rgba(255,255,255,0.85)' : 'rgba(0,255,136,0.75)'; | |
| const glowColor = isSelected ? 'rgba(255,255,255,0.2)' : 'rgba(0,255,136,0.2)'; | |
| // glow layer (thick, transparent) | |
| planeMarkers[ac.hex]._trailGlow = L.polyline(planePaths[ac.hex], { | |
| color: glowColor, | |
| weight: 6, | |
| lineCap: 'round' | |
| }).addTo(map); | |
| // solid line on top | |
| planeMarkers[ac.hex]._trail = L.polyline(planePaths[ac.hex], { | |
| color: trailColor, | |
| weight: 2, | |
| dashArray: '6 5', | |
| lineCap: 'round' | |
| }).addTo(map); | |
| } | |
| // popup | |
| planeMarkers[ac.hex].bindPopup( | |
| `<b style="font-size:14px;letter-spacing:2px">${ac.flight}</b><br>` + | |
| `HEX: ${ac.hex}<br>` + | |
| `ALT: ${ac.alt.toLocaleString()} ft<br>` + | |
| `SPD: ${ac.speed} kts<br>` + | |
| `TRK: ${ac.track}°<br>` + | |
| `POS: ${ac.lat.toFixed(4)}° / ${ac.lon.toFixed(4)}°` | |
| ); | |
| }); | |
| // remove stale markers | |
| Object.keys(planeMarkers).forEach(hex => { | |
| if (!seen.has(hex)) { | |
| map.removeLayer(planeMarkers[hex]); | |
| if (planeMarkers[hex]._trail) map.removeLayer(planeMarkers[hex]._trail); | |
| if (planeMarkers[hex]._trailGlow) map.removeLayer(planeMarkers[hex]._trailGlow); | |
| delete planeMarkers[hex]; | |
| delete planePaths[hex]; | |
| delete planeData[hex]; | |
| } | |
| }); | |
| updateFlightList(aircraft.map(parseAircraft).filter(a => a.lat !== null)); | |
| updateStats(aircraft); | |
| updateSelectedDetail(); | |
| } | |
| // ── FLIGHT LIST ────────────────────────────────────────────── | |
| function updateFlightList(acs) { | |
| const list = document.getElementById('flight-list'); | |
| document.getElementById('list-count').textContent = acs.length; | |
| // sort by altitude desc | |
| acs.sort((a, b) => b.alt - a.alt); | |
| list.innerHTML = acs.map(ac => ` | |
| <div class="flight-item ${ac.hex === selectedHex ? 'selected' : ''}" | |
| onclick="selectAircraft('${ac.hex}')"> | |
| <div> | |
| <div class="fi-callsign">${ac.flight}</div> | |
| <div class="fi-hex">${ac.hex}</div> | |
| </div> | |
| <div class="fi-alt"> | |
| <div class="fi-alt-val">${ac.alt.toLocaleString()}</div> | |
| <div class="fi-speed">${ac.speed} kts</div> | |
| </div> | |
| <div class="fi-track"> | |
| <span>LAT ${ac.lat?.toFixed(3) ?? '—'}</span> | |
| <span>LON ${ac.lon?.toFixed(3) ?? '—'}</span> | |
| <span>TRK ${ac.track}°</span> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // ── AVIATIONSTACK — جلب تفاصيل الرحلة ─────────────────────── | |
| const flightCache = {}; | |
| async function fetchFlightDetails(callsign) { | |
| // Static demo has no backend to proxy AviationStack through. | |
| // Run the full project from GitHub to enable this panel. | |
| return null; | |
| } | |
| // ── SELECT AIRCRAFT ────────────────────────────────────────── | |
| function selectAircraft(hex) { | |
| selectedHex = hex; | |
| // refresh icons | |
| Object.keys(planeMarkers).forEach(h => { | |
| const ac = planeData[h]; | |
| if (ac) planeMarkers[h].setIcon(makePlaneIcon(ac.track, h === hex)); | |
| }); | |
| updateSelectedDetail(); | |
| // pan to | |
| if (planeData[hex]) { | |
| map.panTo([planeData[hex].lat, planeData[hex].lon]); | |
| } | |
| } | |
| async function updateSelectedDetail() { | |
| const area = document.getElementById('ac-detail-area'); | |
| const cw = document.getElementById('compass-wrap'); | |
| if (!selectedHex || !planeData[selectedHex]) { | |
| area.innerHTML = `<div class="no-selection">Click any aircraft on the map or in the list to view detailed telemetry</div>`; | |
| cw.style.display = 'none'; | |
| return; | |
| } | |
| const ac = planeData[selectedHex]; | |
| cw.style.display = 'flex'; | |
| // rotate compass needle | |
| document.getElementById('compass-needle').setAttribute( | |
| 'transform', `rotate(${ac.track}, 45, 45)` | |
| ); | |
| // اعرض البيانات الأساسية أول | |
| renderDetail(ac, null); | |
| // بعدين جيب من AviationStack في الخلفية | |
| const info = await fetchFlightDetails(ac.flight); | |
| renderDetail(ac, info); | |
| } | |
| function renderDetail(ac, info) { | |
| const area = document.getElementById('ac-detail-area'); | |
| // ── بيانات AviationStack ── | |
| const airline = info?.airline?.name || '——'; | |
| const acType = info?.aircraft?.iata || info?.aircraft?.icao || '——'; | |
| const acReg = info?.aircraft?.registration || '——'; | |
| const depIata = info?.departure?.iata || '——'; | |
| const depName = info?.departure?.airport || '——'; | |
| const depSched = info?.departure?.scheduled ? info.departure.scheduled.slice(11,16) + 'Z' : '——'; | |
| const depActual = info?.departure?.actual ? info.departure.actual.slice(11,16) + 'Z' : '——'; | |
| const arrIata = info?.arrival?.iata || '——'; | |
| const arrName = info?.arrival?.airport || '——'; | |
| const arrSched = info?.arrival?.scheduled ? info.arrival.scheduled.slice(11,16) + 'Z' : '——'; | |
| const arrEst = info?.arrival?.estimated ? info.arrival.estimated.slice(11,16) + 'Z' : '——'; | |
| const status = info?.flight_status || '——'; | |
| const statusColor = status === 'active' ? 'var(--radar-green)' | |
| : status === 'landed' ? 'var(--alert-yellow)' | |
| : status === 'delayed' ? 'var(--alert-red)' | |
| : 'var(--text-dim)'; | |
| area.innerHTML = ` | |
| <div class="ac-detail"> | |
| <!-- Callsign + Status --> | |
| <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px"> | |
| <div> | |
| <div class="ac-detail-callsign">${ac.flight}</div> | |
| <div class="ac-detail-hex">ICAO: ${ac.hex}</div> | |
| </div> | |
| <div style="text-align:right;font-size:10px;letter-spacing:1px"> | |
| <div style="color:${statusColor};text-transform:uppercase;font-size:11px">${status}</div> | |
| <div style="color:var(--text-dim);margin-top:2px">${acType}</div> | |
| <div style="color:var(--text-dim)">${acReg}</div> | |
| </div> | |
| </div> | |
| <!-- Airline --> | |
| <div style="font-size:11px;color:var(--radar-green);letter-spacing:1px; | |
| border:1px solid var(--border);padding:5px 8px;margin-bottom:8px; | |
| background:var(--radar-glow)"> | |
| ✈ ${airline} | |
| </div> | |
| <!-- FROM / TO --> | |
| <div style="display:grid;grid-template-columns:1fr 8px 1fr;gap:4px;margin-bottom:8px;align-items:center"> | |
| <div style="background:var(--bg-card);border:1px solid var(--border);padding:7px 8px"> | |
| <div style="font-size:8px;color:var(--text-dim);letter-spacing:2px;margin-bottom:3px">FROM 🛫</div> | |
| <div style="font-size:20px;font-weight:700;text-shadow:0 0 8px var(--radar-green)">${depIata}</div> | |
| <div style="font-size:9px;color:var(--text-dim);margin-top:2px;line-height:1.3">${depName.slice(0,18)}</div> | |
| <div style="font-size:9px;color:var(--radar-green);margin-top:3px">SCH ${depSched}</div> | |
| <div style="font-size:9px;color:var(--text-dim)">ACT ${depActual}</div> | |
| </div> | |
| <div style="text-align:center;color:var(--text-dim);font-size:10px">›</div> | |
| <div style="background:var(--bg-card);border:1px solid var(--border);padding:7px 8px"> | |
| <div style="font-size:8px;color:var(--text-dim);letter-spacing:2px;margin-bottom:3px">TO 🛬</div> | |
| <div style="font-size:20px;font-weight:700;text-shadow:0 0 8px var(--radar-green)">${arrIata}</div> | |
| <div style="font-size:9px;color:var(--text-dim);margin-top:2px;line-height:1.3">${arrName.slice(0,18)}</div> | |
| <div style="font-size:9px;color:var(--radar-green);margin-top:3px">SCH ${arrSched}</div> | |
| <div style="font-size:9px;color:var(--text-dim)">EST ${arrEst}</div> | |
| </div> | |
| </div> | |
| <!-- Telemetry grid --> | |
| <div class="data-grid"> | |
| <div class="data-cell"> | |
| <div class="dc-label">ALTITUDE</div> | |
| <div class="dc-value">${ac.alt.toLocaleString()}<span class="dc-unit"> ft</span></div> | |
| </div> | |
| <div class="data-cell"> | |
| <div class="dc-label">SPEED</div> | |
| <div class="dc-value">${ac.speed}<span class="dc-unit"> kts</span></div> | |
| </div> | |
| <div class="data-cell"> | |
| <div class="dc-label">LATITUDE</div> | |
| <div class="dc-value" style="font-size:13px">${ac.lat?.toFixed(4) ?? '—'}°</div> | |
| </div> | |
| <div class="data-cell"> | |
| <div class="dc-label">LONGITUDE</div> | |
| <div class="dc-value" style="font-size:13px">${ac.lon?.toFixed(4) ?? '—'}°</div> | |
| </div> | |
| <div class="data-cell" style="grid-column:1/-1"> | |
| <div class="dc-label">TRACK</div> | |
| <div class="dc-value">${ac.track}<span class="dc-unit">°</span></div> | |
| </div> | |
| </div> | |
| ${!info ? `<div style="font-size:9px;color:var(--text-dim);text-align:center;margin-top:6px;letter-spacing:1px">STATIC DEMO — <a href="${CONFIG.repo_url}" target="_blank" style="color:var(--radar-green)">run locally</a> for flight enrichment</div>` : ''} | |
| </div> | |
| `; | |
| } | |
| // ── STATS ──────────────────────────────────────────────────── | |
| function updateStats(aircraft) { | |
| document.getElementById('stat-count').textContent = aircraft.length; | |
| document.getElementById('stat-msgs').textContent = updateCount; | |
| const maxAlt = aircraft.reduce((m, a) => Math.max(m, a.alt ?? 0), 0); | |
| document.getElementById('stat-range').textContent = maxAlt ? maxAlt.toLocaleString() : '—'; | |
| updateCount = 0; | |
| const now = new Date().toISOString().replace('T',' ').split('.')[0]; | |
| document.getElementById('footer-time').textContent = `LAST UPDATE: ${now}Z`; | |
| } | |
| // ── LOG ────────────────────────────────────────────────────── | |
| function addLog(msg, type='') { | |
| logCount++; | |
| const el = document.getElementById('log-entries'); | |
| const count = document.getElementById('log-count'); | |
| const time = new Date().toTimeString().slice(0,8); | |
| const entry = document.createElement('div'); | |
| entry.className = `log-entry ${type}`; | |
| entry.innerHTML = `<span class="log-time">${time}Z</span><span class="log-msg">${msg}</span>`; | |
| el.insertBefore(entry, el.firstChild); | |
| if (el.children.length > 80) el.removeChild(el.lastChild); | |
| count.textContent = logCount; | |
| } | |
| // ── CLOCK ──────────────────────────────────────────────────── | |
| function updateClock() { | |
| const now = new Date(); | |
| const h = String(now.getUTCHours()).padStart(2,'0'); | |
| const m = String(now.getUTCMinutes()).padStart(2,'0'); | |
| const s = String(now.getUTCSeconds()).padStart(2,'0'); | |
| document.getElementById('utc-clock').textContent = `${h}:${m}:${s}Z`; | |
| } | |
| setInterval(updateClock, 1000); | |
| updateClock(); | |
| // ── DATA SOURCE ────────────────────────────────────────────── | |
| function setSource(src) { | |
| if (src === 'live') { | |
| addLog('LIVE mode needs real RTL-SDR hardware - clone the repo from GitHub to run it', 'alert'); | |
| window.open(CONFIG.repo_url, '_blank'); | |
| return; // stay on simulation - this static build has no backend to switch to | |
| } | |
| currentSource = src; | |
| document.querySelectorAll('.src-btn').forEach(b => b.classList.toggle('active', b.dataset.src === src)); | |
| document.getElementById('src-label').textContent = 'SIMULATION (static demo)'; | |
| } | |
| // ── FETCH LOOP ─────────────────────────────────────────────── | |
| function fetchAircraft() { | |
| // No network at all in the static build - just advance the local simulation. | |
| const list = SIM_ENGINE.step(); | |
| updateCount++; | |
| updateMap(list); | |
| } | |
| // startup log | |
| addLog('SYSTEM INITIALIZED — ADS-B TRACKER v2.0 (STATIC DEMO)'); | |
| addLog('Running client-side simulation — no server, no hardware'); | |
| addLog('Full project with real RTL-SDR support: see GitHub link below'); | |
| fetchAircraft(); | |
| setInterval(fetchAircraft, CONFIG.refresh); | |
| </script> | |
| </body> | |
| </html> |