diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1e48ea9..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 SkyView Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 645e56c..42715a2 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec - **Beast Binary Format**: Native support for dump1090 Beast format (port 30005) - **Multiple Receivers**: Connect to unlimited dump1090 sources simultaneously - **Intelligent Merging**: Smart data fusion with signal strength-based source selection -- **High-throughput Processing**: High-performance concurrent message processing +- **Real-time Processing**: High-performance concurrent message processing ### Advanced Web Interface - **Interactive Maps**: Leaflet.js-based mapping with aircraft tracking -- **Low-latency Updates**: WebSocket-powered live data streaming +- **Real-time Updates**: WebSocket-powered live data streaming - **Mobile Responsive**: Optimized for desktop, tablet, and mobile devices - **Multi-view Dashboard**: Map, Table, Statistics, Coverage, and 3D Radar views @@ -21,14 +21,13 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec - **Range Circles**: Configurable range rings for each receiver - **Flight Trails**: Historical aircraft movement tracking - **3D Radar View**: Three.js-powered 3D visualization (optional) -- **Statistics Dashboard**: Live charts and metrics +- **Statistics Dashboard**: Real-time charts and metrics - **Smart Origin**: Auto-calculated map center based on receiver locations - **Map Controls**: Center on aircraft, reset to origin, toggle overlays ### Aircraft Data - **Complete Mode S Decoding**: Position, velocity, altitude, heading - **Aircraft Identification**: Callsign, category, country, registration -- **ICAO Country Database**: Comprehensive embedded database with 70+ allocations covering 40+ countries - **Multi-source Tracking**: Signal strength from each receiver - **Historical Data**: Position history and trail visualization @@ -119,7 +118,7 @@ Access the web interface at `http://localhost:8080` ### Views Available: - **Map View**: Interactive aircraft tracking with receiver locations - **Table View**: Sortable aircraft data with multi-source information -- **Statistics**: Live metrics and historical charts +- **Statistics**: Real-time metrics and historical charts - **Coverage**: Signal strength analysis and heatmaps - **3D Radar**: Three-dimensional aircraft visualization @@ -161,7 +160,7 @@ docker run -p 8080:8080 -v $(pwd)/config.json:/app/config.json skyview - `GET /api/heatmap/{sourceId}` - Signal heatmap ### WebSocket -- `ws://localhost:8080/ws` - Low-latency updates +- `ws://localhost:8080/ws` - Real-time updates ## ๐Ÿ› ๏ธ Development diff --git a/assets/static/css/style.css b/assets/static/css/style.css index 26ce441..0f2e125 100644 --- a/assets/static/css/style.css +++ b/assets/static/css/style.css @@ -193,48 +193,6 @@ body { background: #404040; } -.display-options { - position: absolute; - top: 10px; - left: 10px; - z-index: 1000; - background: rgba(45, 45, 45, 0.95); - border: 1px solid #404040; - border-radius: 8px; - padding: 1rem; - min-width: 200px; -} - -.display-options h4 { - margin-bottom: 0.5rem; - color: #ffffff; - font-size: 0.9rem; -} - -.option-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.option-group label { - display: flex; - align-items: center; - cursor: pointer; - font-size: 0.8rem; - color: #cccccc; -} - -.option-group input[type="checkbox"] { - margin-right: 0.5rem; - accent-color: #00d4ff; - transform: scale(1.1); -} - -.option-group label:hover { - color: #ffffff; -} - .legend { position: absolute; bottom: 10px; @@ -270,7 +228,6 @@ body { .legend-icon.commercial { background: #00ff88; } .legend-icon.cargo { background: #ff8c00; } -.legend-icon.helicopter { background: #00d4ff; } .legend-icon.military { background: #ff4444; } .legend-icon.ga { background: #ffff00; } .legend-icon.ground { background: #888888; } @@ -405,39 +362,20 @@ body { z-index: 1000; } -/* Leaflet popup override - ensure our styles take precedence */ -.leaflet-popup-content-wrapper { - background: #2d2d2d !important; - color: #ffffff !important; - border-radius: 8px; -} - -.leaflet-popup-content { - margin: 12px !important; - color: #ffffff !important; -} - -.leaflet-popup-tip { - background: #2d2d2d !important; -} - .aircraft-popup { min-width: 300px; max-width: 400px; - color: #ffffff !important; } .popup-header { border-bottom: 1px solid #404040; padding-bottom: 0.5rem; margin-bottom: 0.75rem; - color: #ffffff !important; } .flight-info { font-size: 1.1rem; font-weight: bold; - color: #ffffff !important; } .icao-flag { @@ -446,27 +384,21 @@ body { } .flight-id { - color: #00a8ff !important; + color: #00a8ff; font-family: monospace; } .callsign { - color: #00ff88 !important; + color: #00ff88; } .popup-details { font-size: 0.9rem; - color: #ffffff !important; } .detail-row { margin-bottom: 0.5rem; padding: 0.25rem 0; - color: #ffffff !important; -} - -.detail-row strong { - color: #ffffff !important; } .detail-grid { @@ -483,27 +415,13 @@ body { .detail-item .label { font-size: 0.8rem; - color: #888 !important; + color: #888; margin-bottom: 0.1rem; } .detail-item .value { font-weight: bold; - color: #ffffff !important; -} - -/* Ensure all values are visible with strong contrast */ -.aircraft-popup .value, -.aircraft-popup .detail-row, -.aircraft-popup .detail-item .value { - color: #ffffff !important; - text-shadow: 1px 1px 2px rgba(0,0,0,0.5); -} - -/* Style for N/A or empty values - still visible but slightly dimmed */ -.detail-item .value.no-data { - color: #aaaaaa !important; - font-style: italic; + color: #ffffff; } @media (max-width: 768px) { diff --git a/assets/static/icons/cargo.svg b/assets/static/icons/cargo.svg deleted file mode 100644 index b3605b1..0000000 --- a/assets/static/icons/cargo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/assets/static/icons/commercial.svg b/assets/static/icons/commercial.svg deleted file mode 100644 index f1f1b28..0000000 --- a/assets/static/icons/commercial.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/assets/static/icons/ga.svg b/assets/static/icons/ga.svg deleted file mode 100644 index cfba161..0000000 --- a/assets/static/icons/ga.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/assets/static/icons/ground.svg b/assets/static/icons/ground.svg deleted file mode 100644 index ee5af8e..0000000 --- a/assets/static/icons/ground.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/assets/static/icons/helicopter.svg b/assets/static/icons/helicopter.svg deleted file mode 100644 index 5197bea..0000000 --- a/assets/static/icons/helicopter.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/static/icons/military.svg b/assets/static/icons/military.svg deleted file mode 100644 index c4e58a7..0000000 --- a/assets/static/icons/military.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/assets/static/index.html b/assets/static/index.html index 9849126..036a832 100644 --- a/assets/static/index.html +++ b/assets/static/index.html @@ -77,59 +77,32 @@ + - - - - -
-

Display Options

-
- - - -
-

ADS-B Categories

+

Aircraft Types

- Light < 7000kg -
-
- - Medium 7000-34000kg -
-
- - Large 34000-136000kg + Commercial
- Heavy > 136000kg + Cargo
- - Rotorcraft + + Military
- Glider/Ultralight + General Aviation
- Surface Vehicle + Ground

Sources

@@ -249,6 +222,6 @@ - + \ No newline at end of file diff --git a/assets/static/js/app.js b/assets/static/js/app.js index 8570ff7..d1aa62e 100644 --- a/assets/static/js/app.js +++ b/assets/static/js/app.js @@ -2,265 +2,96 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; -// Import our modular components -import { WebSocketManager } from './modules/websocket.js?v=2'; -import { AircraftManager } from './modules/aircraft-manager.js?v=2'; -import { MapManager } from './modules/map-manager.js?v=2'; -import { UIManager } from './modules/ui-manager.js?v=2'; - class SkyView { constructor() { - // Initialize managers - this.wsManager = null; - this.aircraftManager = null; - this.mapManager = null; - this.uiManager = null; - - // 3D Radar + this.map = null; + this.coverageMap = null; this.radar3d = null; + this.websocket = null; + + // Data storage + this.aircraftData = new Map(); + this.sourcesData = new Map(); + this.stats = {}; + + // Map markers and overlays + this.aircraftMarkers = new Map(); + this.sourceMarkers = new Map(); + this.aircraftTrails = new Map(); + this.rangeCircles = new Map(); + this.heatmapLayer = null; + + // UI state + this.currentView = 'map-view'; + this.showTrails = false; + this.showRange = false; + this.showSources = true; + this.selectedSource = null; // Charts this.charts = {}; - // Selected aircraft tracking - this.selectedAircraft = null; - this.selectedTrailEnabled = false; + // Update tracking + this.lastUpdateTime = new Date(); this.init(); } async init() { try { - - // Initialize UI manager first - this.uiManager = new UIManager(); - this.uiManager.initializeViews(); - this.uiManager.initializeEventListeners(); - - // Initialize map manager and get the main map - this.mapManager = new MapManager(); - const map = await this.mapManager.initializeMap(); - - // Initialize aircraft manager with the map - this.aircraftManager = new AircraftManager(map); - - // Set up selected aircraft trail callback - this.aircraftManager.setSelectedAircraftCallback((icao) => { - return this.selectedTrailEnabled && this.selectedAircraft === icao; - }); - - // Initialize WebSocket with callbacks - this.wsManager = new WebSocketManager( - (message) => this.handleWebSocketMessage(message), - (status) => this.uiManager.updateConnectionStatus(status) - ); - - await this.wsManager.connect(); - - // Initialize other components + this.initializeViews(); + this.initializeMap(); + await this.initializeWebSocket(); + this.initializeEventListeners(); this.initializeCharts(); - this.uiManager.updateClocks(); + this.initializeClocks(); this.initialize3DRadar(); - // Set up map controls - this.setupMapControls(); - - // Set up aircraft selection listener - this.setupAircraftSelection(); - this.startPeriodicTasks(); - } catch (error) { console.error('Initialization failed:', error); - this.uiManager.showError('Failed to initialize application'); + this.showError('Failed to initialize application'); } } - setupMapControls() { - const centerMapBtn = document.getElementById('center-map'); - const resetMapBtn = document.getElementById('reset-map'); - const toggleTrailsBtn = document.getElementById('toggle-trails'); - const toggleSourcesBtn = document.getElementById('toggle-sources'); - - if (centerMapBtn) { - centerMapBtn.addEventListener('click', () => { - this.aircraftManager.centerMapOnAircraft(() => this.mapManager.getSourcePositions()); - }); - } - - if (resetMapBtn) { - resetMapBtn.addEventListener('click', () => this.mapManager.resetMap()); - } - - if (toggleTrailsBtn) { - toggleTrailsBtn.addEventListener('click', () => { - const showTrails = this.aircraftManager.toggleTrails(); - toggleTrailsBtn.textContent = showTrails ? 'Hide Trails' : 'Show Trails'; - }); - } - - - if (toggleSourcesBtn) { - toggleSourcesBtn.addEventListener('click', () => { - const showSources = this.mapManager.toggleSources(); - toggleSourcesBtn.textContent = showSources ? 'Hide Sources' : 'Show Sources'; - }); - } - - const toggleDarkModeBtn = document.getElementById('toggle-dark-mode'); - if (toggleDarkModeBtn) { - toggleDarkModeBtn.addEventListener('click', () => { - const isDarkMode = this.mapManager.toggleDarkMode(); - toggleDarkModeBtn.innerHTML = isDarkMode ? 'โ˜€๏ธ Light Mode' : '๐ŸŒ™ Night Mode'; - }); - } - - // Coverage controls - const toggleHeatmapBtn = document.getElementById('toggle-heatmap'); - const coverageSourceSelect = document.getElementById('coverage-source'); - - if (toggleHeatmapBtn) { - toggleHeatmapBtn.addEventListener('click', async () => { - const isActive = await this.mapManager.toggleHeatmap(); - toggleHeatmapBtn.textContent = isActive ? 'Hide Heatmap' : 'Show Heatmap'; - }); - } - - if (coverageSourceSelect) { - coverageSourceSelect.addEventListener('change', (e) => { - this.mapManager.setSelectedSource(e.target.value); - this.mapManager.updateCoverageDisplay(); - }); - } - - // Display option checkboxes - const sitePositionsCheckbox = document.getElementById('show-site-positions'); - const rangeRingsCheckbox = document.getElementById('show-range-rings'); - const selectedTrailCheckbox = document.getElementById('show-selected-trail'); - - if (sitePositionsCheckbox) { - sitePositionsCheckbox.addEventListener('change', (e) => { - if (e.target.checked) { - this.mapManager.showSources = true; - this.mapManager.updateSourceMarkers(); - } else { - this.mapManager.showSources = false; - this.mapManager.sourceMarkers.forEach(marker => this.mapManager.map.removeLayer(marker)); - this.mapManager.sourceMarkers.clear(); - } - }); - } - - if (rangeRingsCheckbox) { - rangeRingsCheckbox.addEventListener('change', (e) => { - if (e.target.checked) { - this.mapManager.showRange = true; - this.mapManager.updateRangeCircles(); - } else { - this.mapManager.showRange = false; - this.mapManager.rangeCircles.forEach(circle => this.mapManager.map.removeLayer(circle)); - this.mapManager.rangeCircles.clear(); - } - }); - } - - if (selectedTrailCheckbox) { - selectedTrailCheckbox.addEventListener('change', (e) => { - this.selectedTrailEnabled = e.target.checked; - if (!e.target.checked && this.selectedAircraft) { - // Hide currently selected aircraft trail - this.aircraftManager.hideAircraftTrail(this.selectedAircraft); - } else if (e.target.checked && this.selectedAircraft) { - // Show currently selected aircraft trail - this.aircraftManager.showAircraftTrail(this.selectedAircraft); - } - }); - } - } + // View Management + initializeViews() { + const viewButtons = document.querySelectorAll('.view-btn'); + const views = document.querySelectorAll('.view'); - setupAircraftSelection() { - document.addEventListener('aircraftSelected', (e) => { - const { icao, aircraft } = e.detail; - this.uiManager.switchView('map-view'); - - // Hide trail for previously selected aircraft - if (this.selectedAircraft && this.selectedTrailEnabled) { - this.aircraftManager.hideAircraftTrail(this.selectedAircraft); - } - - // Update selected aircraft - this.selectedAircraft = icao; - - // Automatically enable selected aircraft trail when an aircraft is selected - if (!this.selectedTrailEnabled) { - this.selectedTrailEnabled = true; - const selectedTrailCheckbox = document.getElementById('show-selected-trail'); - if (selectedTrailCheckbox) { - selectedTrailCheckbox.checked = true; - } - } - - // Show trail for newly selected aircraft - this.aircraftManager.showAircraftTrail(icao); - - // DON'T change map view - just open popup like Leaflet expects - if (this.mapManager.map && aircraft.Latitude && aircraft.Longitude) { - const marker = this.aircraftManager.aircraftMarkers.get(icao); - if (marker) { - marker.openPopup(); - } - } + viewButtons.forEach(btn => { + btn.addEventListener('click', () => { + const viewId = btn.id.replace('-btn', ''); + this.switchView(viewId); + }); }); } - handleWebSocketMessage(message) { - switch (message.type) { - case 'initial_data': - this.updateData(message.data); - // Setup source markers only on initial data load - this.mapManager.updateSourceMarkers(); - break; - case 'aircraft_update': - this.updateData(message.data); - break; - default: - } - } - - updateData(data) { - // Update all managers with new data - this.uiManager.updateData(data); - this.aircraftManager.updateAircraftData(data); - this.mapManager.updateSourcesData(data); - - // Update UI components - this.aircraftManager.updateMarkers(); - this.uiManager.updateAircraftTable(); - this.uiManager.updateStatistics(); - this.uiManager.updateHeaderInfo(); - - // Clear selected aircraft if it no longer exists - if (this.selectedAircraft && !this.aircraftManager.aircraftData.has(this.selectedAircraft)) { - this.selectedAircraft = null; + switchView(viewId) { + // Update buttons + document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active')); + const activeBtn = document.getElementById(`${viewId}-btn`); + if (activeBtn) { + activeBtn.classList.add('active'); } - // Update coverage controls - this.mapManager.updateCoverageControls(); - - if (this.uiManager.currentView === 'radar3d-view') { - this.update3DRadar(); + // Update views (viewId already includes the full view ID like "map-view") + document.querySelectorAll('.view').forEach(view => view.classList.remove('active')); + const activeView = document.getElementById(viewId); + if (activeView) { + activeView.classList.add('active'); + } else { + console.warn(`View element not found: ${viewId}`); + return; } - } - - // View switching - async switchView(viewId) { - const actualViewId = this.uiManager.switchView(viewId); - // Handle view-specific initialization - const baseName = actualViewId.replace('-view', ''); + this.currentView = viewId; + + // Handle view-specific initialization (extract base name for switch) + const baseName = viewId.replace('-view', ''); switch (baseName) { case 'coverage': - await this.mapManager.initializeCoverageMap(); + this.initializeCoverageMap(); break; case 'radar3d': this.update3DRadar(); @@ -268,64 +99,874 @@ class SkyView { } } - // Charts - initializeCharts() { - const aircraftChartCanvas = document.getElementById('aircraft-chart'); - if (!aircraftChartCanvas) { - console.warn('Aircraft chart canvas not found'); - return; + // Map Initialization + async initializeMap() { + // Get origin from server + let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback + try { + const response = await fetch('/api/origin'); + if (response.ok) { + origin = await response.json(); + } + } catch (error) { + console.warn('Could not fetch origin, using default:', error); } - try { - this.charts.aircraft = new Chart(aircraftChartCanvas, { - type: 'line', - data: { - labels: [], - datasets: [{ - label: 'Aircraft Count', - data: [], - borderColor: '#00d4ff', - backgroundColor: 'rgba(0, 212, 255, 0.1)', - tension: 0.4 - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - legend: { display: false } - }, - scales: { - x: { display: false }, - y: { - beginAtZero: true, - ticks: { color: '#888' } - } - } + // Store origin for reset functionality + this.mapOrigin = origin; + + this.map = L.map('map').setView([origin.latitude, origin.longitude], 10); + + // Dark tile layer + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors © CARTO', + subdomains: 'abcd', + maxZoom: 19 + }).addTo(this.map); + + // Map controls + document.getElementById('center-map').addEventListener('click', () => this.centerMapOnAircraft()); + document.getElementById('reset-map').addEventListener('click', () => this.resetMap()); + document.getElementById('toggle-trails').addEventListener('click', () => this.toggleTrails()); + document.getElementById('toggle-range').addEventListener('click', () => this.toggleRangeCircles()); + document.getElementById('toggle-sources').addEventListener('click', () => this.toggleSources()); + } + + async initializeCoverageMap() { + if (!this.coverageMap) { + // Get origin from server + let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback + try { + const response = await fetch('/api/origin'); + if (response.ok) { + origin = await response.json(); } - }); + } catch (error) { + console.warn('Could not fetch origin for coverage map, using default:', error); + } + + this.coverageMap = L.map('coverage-map').setView([origin.latitude, origin.longitude], 10); + + L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(this.coverageMap); + } + + // Update coverage controls + this.updateCoverageControls(); + + // Coverage controls + document.getElementById('toggle-heatmap').addEventListener('click', () => this.toggleHeatmap()); + document.getElementById('coverage-source').addEventListener('change', (e) => { + this.selectedSource = e.target.value; + this.updateCoverageDisplay(); + }); + } + + // WebSocket Connection + async initializeWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws`; + + try { + this.websocket = new WebSocket(wsUrl); + + this.websocket.onopen = () => { + console.log('WebSocket connected'); + this.updateConnectionStatus('connected'); + }; + + this.websocket.onclose = () => { + console.log('WebSocket disconnected'); + this.updateConnectionStatus('disconnected'); + // Reconnect after 5 seconds + setTimeout(() => this.initializeWebSocket(), 5000); + }; + + this.websocket.onerror = (error) => { + console.error('WebSocket error:', error); + this.updateConnectionStatus('disconnected'); + }; + + this.websocket.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + this.handleWebSocketMessage(message); + } catch (error) { + console.error('Failed to parse WebSocket message:', error); + } + }; + } catch (error) { - console.warn('Chart.js not available, skipping charts initialization'); + console.error('WebSocket connection failed:', error); + this.updateConnectionStatus('disconnected'); } } - updateCharts() { - if (!this.charts.aircraft) return; + handleWebSocketMessage(message) { + switch (message.type) { + case 'initial_data': + case 'aircraft_update': + this.updateData(message.data); + break; + default: + console.log('Unknown message type:', message.type); + } + } + + updateData(data) { + // Update aircraft data + if (data.aircraft) { + this.aircraftData.clear(); + for (const [icao, aircraft] of Object.entries(data.aircraft)) { + this.aircraftData.set(icao, aircraft); + } + } + // Update sources data + if (data.sources) { + this.sourcesData.clear(); + data.sources.forEach(source => { + this.sourcesData.set(source.id, source); + }); + } + + // Update statistics + if (data.stats) { + this.stats = data.stats; + } + + this.lastUpdateTime = new Date(); + + // Update UI + this.updateMapMarkers(); + this.updateAircraftTable(); + this.updateStatistics(); + this.updateHeaderInfo(); + + if (this.currentView === 'radar3d-view') { + this.update3DRadar(); + } + } + + // Map Updates + updateMapMarkers() { + // Clear stale aircraft markers + const currentICAOs = new Set(this.aircraftData.keys()); + for (const [icao, marker] of this.aircraftMarkers) { + if (!currentICAOs.has(icao)) { + this.map.removeLayer(marker); + this.aircraftMarkers.delete(icao); + this.aircraftTrails.delete(icao); + } + } + + // Update aircraft markers + for (const [icao, aircraft] of this.aircraftData) { + if (aircraft.Latitude && aircraft.Longitude) { + this.updateAircraftMarker(icao, aircraft); + } + } + + // Update source markers + if (this.showSources) { + this.updateSourceMarkers(); + } + + // Update range circles + if (this.showRange) { + this.updateRangeCircles(); + } + } + + updateAircraftMarker(icao, aircraft) { + const pos = [aircraft.Latitude, aircraft.Longitude]; + + if (this.aircraftMarkers.has(icao)) { + // Update existing marker + const marker = this.aircraftMarkers.get(icao); + marker.setLatLng(pos); + this.updateMarkerRotation(marker, aircraft.Track || 0); + this.updatePopupContent(marker, aircraft); + } else { + // Create new marker + const icon = this.createAircraftIcon(aircraft); + const marker = L.marker(pos, { + icon: icon, + rotationAngle: aircraft.Track || 0 + }).addTo(this.map); + + marker.bindPopup(this.createPopupContent(aircraft), { + maxWidth: 450, + className: 'aircraft-popup' + }); + + this.aircraftMarkers.set(icao, marker); + } + + // Update trails + if (this.showTrails) { + this.updateAircraftTrail(icao, pos); + } + } + + createAircraftIcon(aircraft) { + const type = this.getAircraftType(aircraft); + const color = this.getAircraftColor(type); + const size = aircraft.OnGround ? 12 : 16; + + const svg = ` + + + + + + `; + + return L.divIcon({ + html: svg, + iconSize: [size * 2, size * 2], + iconAnchor: [size, size], + className: 'aircraft-marker' + }); + } + + getAircraftType(aircraft) { + if (aircraft.OnGround) return 'ground'; + if (aircraft.Category) { + const cat = aircraft.Category.toLowerCase(); + if (cat.includes('military')) return 'military'; + if (cat.includes('cargo') || cat.includes('heavy')) return 'cargo'; + if (cat.includes('light') || cat.includes('glider')) return 'ga'; + } + if (aircraft.Callsign) { + const cs = aircraft.Callsign.toLowerCase(); + if (cs.includes('mil') || cs.includes('army') || cs.includes('navy')) return 'military'; + if (cs.includes('cargo') || cs.includes('fedex') || cs.includes('ups')) return 'cargo'; + } + return 'commercial'; + } + + getAircraftColor(type) { + const colors = { + commercial: '#00ff88', + cargo: '#ff8c00', + military: '#ff4444', + ga: '#ffff00', + ground: '#888888' + }; + return colors[type] || colors.commercial; + } + + updateMarkerRotation(marker, track) { + if (marker._icon) { + marker._icon.style.transform = `rotate(${track}deg)`; + } + } + + updateSourceMarkers() { + // Remove markers for sources that no longer exist + const currentSourceIds = new Set(this.sourcesData.keys()); + for (const [id, marker] of this.sourceMarkers) { + if (!currentSourceIds.has(id)) { + this.map.removeLayer(marker); + this.sourceMarkers.delete(id); + } + } + + // Update or create markers for current sources + for (const [id, source] of this.sourcesData) { + if (source.latitude && source.longitude) { + if (this.sourceMarkers.has(id)) { + // Update existing marker + const marker = this.sourceMarkers.get(id); + + // Update marker style if status changed + marker.setStyle({ + radius: source.active ? 10 : 6, + fillColor: source.active ? '#00d4ff' : '#666666', + fillOpacity: 0.8 + }); + + // Update popup content if it's open + if (marker.isPopupOpen()) { + marker.setPopupContent(this.createSourcePopupContent(source)); + } + } else { + // Create new marker + const marker = L.circleMarker([source.latitude, source.longitude], { + radius: source.active ? 10 : 6, + fillColor: source.active ? '#00d4ff' : '#666666', + color: '#ffffff', + weight: 2, + fillOpacity: 0.8, + className: 'source-marker' + }).addTo(this.map); + + marker.bindPopup(this.createSourcePopupContent(source), { + maxWidth: 300 + }); + + this.sourceMarkers.set(id, marker); + } + } + } + + this.updateSourcesLegend(); + } + + updateRangeCircles() { + // Clear existing circles + this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); + this.rangeCircles.clear(); + + // Add range circles for active sources + for (const [id, source] of this.sourcesData) { + if (source.active && source.latitude && source.longitude) { + // Add multiple range circles (50km, 100km, 200km) + const ranges = [50000, 100000, 200000]; + ranges.forEach((range, index) => { + const circle = L.circle([source.latitude, source.longitude], { + radius: range, + fillColor: 'transparent', + color: '#00d4ff', + weight: 1, + opacity: 0.3 - (index * 0.1), + dashArray: '5,5' + }).addTo(this.map); + + this.rangeCircles.set(`${id}_${range}`, circle); + }); + } + } + } + + updateAircraftTrail(icao, pos) { + if (!this.aircraftTrails.has(icao)) { + this.aircraftTrails.set(icao, []); + } + + const trail = this.aircraftTrails.get(icao); + trail.push(pos); + + // Keep only last 50 positions + if (trail.length > 50) { + trail.shift(); + } + + // Draw polyline + const trailLine = L.polyline(trail, { + color: '#00d4ff', + weight: 2, + opacity: 0.6 + }).addTo(this.map); + + // Store reference for cleanup + if (!this.aircraftTrails.get(icao).polyline) { + this.aircraftTrails.get(icao).polyline = trailLine; + } else { + this.map.removeLayer(this.aircraftTrails.get(icao).polyline); + this.aircraftTrails.get(icao).polyline = trailLine; + } + } + + // Popup Content + createPopupContent(aircraft) { + const type = this.getAircraftType(aircraft); + const country = this.getCountryFromICAO(aircraft.ICAO24 ? aircraft.ICAO24.toString(16).toUpperCase() : ''); + const flag = this.getCountryFlag(country); + + const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; + const altitudeM = altitude ? Math.round(altitude * 0.3048) : 0; + const speedKmh = aircraft.GroundSpeed ? Math.round(aircraft.GroundSpeed * 1.852) : 0; + const distance = this.calculateDistance(aircraft); + const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; + + const sources = aircraft.Sources ? Object.keys(aircraft.Sources).map(id => { + const source = this.sourcesData.get(id); + const srcData = aircraft.Sources[id]; + return ` + ${source?.name || id} + `; + }).join('') : 'N/A'; + + return ` +
+ + + +
+ `; + } + + updatePopupContent(marker, aircraft) { + if (marker && marker.isPopupOpen()) { + const newContent = this.createPopupContent(aircraft); + marker.setPopupContent(newContent); + } + } + + createSourcePopupContent(source) { + const aircraftCount = Array.from(this.aircraftData.values()) + .filter(aircraft => aircraft.Sources && aircraft.Sources[source.id]).length; + + return ` +
+

${source.name}

+

ID: ${source.id}

+

Location: ${source.latitude.toFixed(4)}ยฐ, ${source.longitude.toFixed(4)}ยฐ

+

Status: ${source.active ? 'Active' : 'Inactive'}

+

Aircraft: ${aircraftCount}

+

Messages: ${source.messages || 0}

+

Last Seen: ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}

+
+ `; + } + + updateSourcesLegend() { + const legend = document.getElementById('sources-legend'); + legend.innerHTML = ''; + + for (const [id, source] of this.sourcesData) { + const item = document.createElement('div'); + item.className = 'legend-item'; + item.innerHTML = ` + + ${source.name} + `; + legend.appendChild(item); + } + } + + // Table Management + updateAircraftTable() { + const tbody = document.getElementById('aircraft-tbody'); + tbody.innerHTML = ''; + + let filteredData = Array.from(this.aircraftData.values()); + + // Apply filters + const searchTerm = document.getElementById('search-input').value.toLowerCase(); + const sourceFilter = document.getElementById('source-filter').value; + + if (searchTerm) { + filteredData = filteredData.filter(aircraft => + (aircraft.Callsign && aircraft.Callsign.toLowerCase().includes(searchTerm)) || + (aircraft.ICAO24 && aircraft.ICAO24.toString(16).toLowerCase().includes(searchTerm)) || + (aircraft.Squawk && aircraft.Squawk.includes(searchTerm)) + ); + } + + if (sourceFilter) { + filteredData = filteredData.filter(aircraft => + aircraft.Sources && aircraft.Sources[sourceFilter] + ); + } + + // Sort data + const sortBy = document.getElementById('sort-select').value; + this.sortAircraft(filteredData, sortBy); + + // Populate table + filteredData.forEach(aircraft => { + const row = this.createTableRow(aircraft); + tbody.appendChild(row); + }); + + // Update source filter options + this.updateSourceFilter(); + } + + createTableRow(aircraft) { + const type = this.getAircraftType(aircraft); + const icao = aircraft.ICAO24 ? aircraft.ICAO24.toString(16).toUpperCase() : 'N/A'; + const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; + const distance = this.calculateDistance(aircraft); + const sources = aircraft.Sources ? Object.keys(aircraft.Sources).length : 0; + const bestSignal = this.getBestSignalFromSources(aircraft.Sources); + + const row = document.createElement('tr'); + row.innerHTML = ` + ${icao} + ${aircraft.Callsign || '-'} + ${aircraft.Squawk || '-'} + ${altitude ? `${altitude} ft` : '-'} + ${aircraft.GroundSpeed || '-'} kt + ${distance ? distance.toFixed(1) : '-'} km + ${aircraft.Track || '-'}ยฐ + ${sources} + ${bestSignal ? bestSignal.toFixed(1) : '-'} + ${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s + `; + + row.addEventListener('click', () => { + if (aircraft.Latitude && aircraft.Longitude) { + this.switchView('map'); + this.map.setView([aircraft.Latitude, aircraft.Longitude], 12); + const marker = this.aircraftMarkers.get(icao); + if (marker) { + marker.openPopup(); + } + } + }); + + return row; + } + + getBestSignalFromSources(sources) { + if (!sources) return null; + let bestSignal = -999; + for (const [id, data] of Object.entries(sources)) { + if (data.SignalLevel > bestSignal) { + bestSignal = data.SignalLevel; + } + } + return bestSignal === -999 ? null : bestSignal; + } + + getSignalClass(signal) { + if (!signal) return ''; + if (signal > -10) return 'signal-strong'; + if (signal > -20) return 'signal-good'; + if (signal > -30) return 'signal-weak'; + return 'signal-poor'; + } + + updateSourceFilter() { + const select = document.getElementById('source-filter'); + const currentValue = select.value; + + // Clear options except "All Sources" + select.innerHTML = ''; + + // Add source options + for (const [id, source] of this.sourcesData) { + const option = document.createElement('option'); + option.value = id; + option.textContent = source.name; + if (id === currentValue) option.selected = true; + select.appendChild(option); + } + } + + sortAircraft(aircraft, sortBy) { + aircraft.sort((a, b) => { + switch (sortBy) { + case 'distance': + return (this.calculateDistance(a) || Infinity) - (this.calculateDistance(b) || Infinity); + case 'altitude': + return (b.Altitude || b.BaroAltitude || 0) - (a.Altitude || a.BaroAltitude || 0); + case 'speed': + return (b.GroundSpeed || 0) - (a.GroundSpeed || 0); + case 'flight': + return (a.Callsign || a.ICAO24?.toString(16) || '').localeCompare(b.Callsign || b.ICAO24?.toString(16) || ''); + case 'icao': + return (a.ICAO24?.toString(16) || '').localeCompare(b.ICAO24?.toString(16) || ''); + case 'squawk': + return (a.Squawk || '').localeCompare(b.Squawk || ''); + case 'signal': + return (this.getBestSignalFromSources(b.Sources) || -999) - (this.getBestSignalFromSources(a.Sources) || -999); + case 'age': + return (a.Age || 0) - (b.Age || 0); + default: + return 0; + } + }); + } + + // Statistics + updateStatistics() { + document.getElementById('total-aircraft').textContent = this.aircraftData.size; + document.getElementById('active-sources').textContent = + Array.from(this.sourcesData.values()).filter(s => s.active).length; + + // Calculate max range + let maxDistance = 0; + for (const aircraft of this.aircraftData.values()) { + const distance = this.calculateDistance(aircraft); + if (distance && distance > maxDistance) { + maxDistance = distance; + } + } + document.getElementById('max-range').textContent = `${maxDistance.toFixed(1)} km`; + + // Update message rate + const totalMessages = this.stats.total_messages || 0; + document.getElementById('messages-sec').textContent = Math.round(totalMessages / 60); + + this.updateCharts(); + } + + updateHeaderInfo() { + document.getElementById('aircraft-count').textContent = `${this.aircraftData.size} aircraft`; + document.getElementById('sources-count').textContent = `${this.sourcesData.size} sources`; + + this.updateClocks(); + } + + // Event Listeners + initializeEventListeners() { + document.getElementById('search-input').addEventListener('input', () => this.updateAircraftTable()); + document.getElementById('sort-select').addEventListener('change', () => this.updateAircraftTable()); + document.getElementById('source-filter').addEventListener('change', () => this.updateAircraftTable()); + } + + // Map Controls + centerMapOnAircraft() { + if (this.aircraftData.size === 0) return; + + const validAircraft = Array.from(this.aircraftData.values()) + .filter(a => a.Latitude && a.Longitude); + + if (validAircraft.length === 0) return; + + if (validAircraft.length === 1) { + // Center on single aircraft + const aircraft = validAircraft[0]; + this.map.setView([aircraft.Latitude, aircraft.Longitude], 12); + } else { + // Fit bounds to all aircraft + const bounds = L.latLngBounds( + validAircraft.map(a => [a.Latitude, a.Longitude]) + ); + this.map.fitBounds(bounds.pad(0.1)); + } + } + + resetMap() { + if (this.mapOrigin && this.map) { + this.map.setView([this.mapOrigin.latitude, this.mapOrigin.longitude], 10); + } + } + + toggleTrails() { + this.showTrails = !this.showTrails; + document.getElementById('toggle-trails').textContent = + this.showTrails ? 'Hide Trails' : 'Show Trails'; + + if (!this.showTrails) { + // Clear all trails + this.aircraftTrails.forEach((trail, icao) => { + if (trail.polyline) { + this.map.removeLayer(trail.polyline); + } + }); + this.aircraftTrails.clear(); + } + } + + toggleRangeCircles() { + this.showRange = !this.showRange; + document.getElementById('toggle-range').textContent = + this.showRange ? 'Hide Range' : 'Show Range'; + + if (this.showRange) { + this.updateRangeCircles(); + } else { + this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); + this.rangeCircles.clear(); + } + } + + toggleSources() { + this.showSources = !this.showSources; + document.getElementById('toggle-sources').textContent = + this.showSources ? 'Hide Sources' : 'Show Sources'; + + if (this.showSources) { + this.updateSourceMarkers(); + } else { + this.sourceMarkers.forEach(marker => this.map.removeLayer(marker)); + this.sourceMarkers.clear(); + } + } + + // Coverage and Heatmap + updateCoverageControls() { + const select = document.getElementById('coverage-source'); + select.innerHTML = ''; + + for (const [id, source] of this.sourcesData) { + const option = document.createElement('option'); + option.value = id; + option.textContent = source.name; + select.appendChild(option); + } + } + + async updateCoverageDisplay() { + if (!this.selectedSource) return; + + try { + const response = await fetch(`/api/coverage/${this.selectedSource}`); + const data = await response.json(); + + // Clear existing coverage markers + this.coverageMap.eachLayer(layer => { + if (layer instanceof L.CircleMarker) { + this.coverageMap.removeLayer(layer); + } + }); + + // Add coverage points + data.points.forEach(point => { + const intensity = Math.max(0, (point.signal + 50) / 50); // Normalize signal strength + L.circleMarker([point.lat, point.lon], { + radius: 3, + fillColor: this.getSignalColor(point.signal), + color: 'white', + weight: 1, + fillOpacity: intensity + }).addTo(this.coverageMap); + }); + + } catch (error) { + console.error('Failed to load coverage data:', error); + } + } + + async toggleHeatmap() { + if (!this.selectedSource) { + alert('Please select a source first'); + return; + } + + if (this.heatmapLayer) { + this.coverageMap.removeLayer(this.heatmapLayer); + this.heatmapLayer = null; + document.getElementById('toggle-heatmap').textContent = 'Show Heatmap'; + } else { + try { + const response = await fetch(`/api/heatmap/${this.selectedSource}`); + const data = await response.json(); + + // Create heatmap layer (simplified) + // In a real implementation, you'd use a proper heatmap library + this.createHeatmapOverlay(data); + document.getElementById('toggle-heatmap').textContent = 'Hide Heatmap'; + + } catch (error) { + console.error('Failed to load heatmap data:', error); + } + } + } + + getSignalColor(signal) { + if (signal > -10) return '#00ff88'; + if (signal > -20) return '#ffff00'; + if (signal > -30) return '#ff8c00'; + return '#ff4444'; + } + + // Charts (continued in next part due to length) + initializeCharts() { + this.charts.aircraft = new Chart(document.getElementById('aircraft-chart'), { + type: 'line', + data: { + labels: [], + datasets: [{ + label: 'Aircraft Count', + data: [], + borderColor: '#00d4ff', + backgroundColor: 'rgba(0, 212, 255, 0.1)', + tension: 0.4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false } + }, + scales: { + x: { display: false }, + y: { + beginAtZero: true, + ticks: { color: '#888' } + } + } + } + }); + + // Add other charts... + } + + updateCharts() { const now = new Date(); const timeLabel = now.toLocaleTimeString(); // Update aircraft count chart - const chart = this.charts.aircraft; - chart.data.labels.push(timeLabel); - chart.data.datasets[0].data.push(this.aircraftManager.aircraftData.size); - - if (chart.data.labels.length > 20) { - chart.data.labels.shift(); - chart.data.datasets[0].data.shift(); + if (this.charts.aircraft) { + const chart = this.charts.aircraft; + chart.data.labels.push(timeLabel); + chart.data.datasets[0].data.push(this.aircraftData.size); + + if (chart.data.labels.length > 20) { + chart.data.labels.shift(); + chart.data.datasets[0].data.shift(); + } + + chart.update('none'); } - - chart.update('none'); } // 3D Radar (basic implementation) @@ -383,17 +1024,18 @@ class SkyView { // Start render loop this.render3DRadar(); + console.log('3D Radar initialized successfully'); } catch (error) { console.error('Failed to initialize 3D radar:', error); } } update3DRadar() { - if (!this.radar3d || !this.radar3d.scene || !this.aircraftManager) return; + if (!this.radar3d || !this.radar3d.scene) return; try { // Update aircraft positions in 3D space - this.aircraftManager.aircraftData.forEach((aircraft, icao) => { + this.aircraftData.forEach((aircraft, icao) => { if (aircraft.Latitude && aircraft.Longitude) { const key = icao.toString(); @@ -424,7 +1066,7 @@ class SkyView { // Remove old aircraft this.radar3d.aircraftMeshes.forEach((mesh, key) => { - if (!this.aircraftManager.aircraftData.has(key)) { + if (!this.aircraftData.has(parseInt(key, 16))) { this.radar3d.scene.remove(mesh); this.radar3d.aircraftMeshes.delete(key); } @@ -446,18 +1088,99 @@ class SkyView { this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera); } + // Utility functions + updateConnectionStatus(status) { + const statusEl = document.getElementById('connection-status'); + statusEl.className = `connection-status ${status}`; + statusEl.textContent = status === 'connected' ? 'Connected' : 'Disconnected'; + } + + initializeClocks() { + this.updateClocks(); + } + + updateClocks() { + const now = new Date(); + const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000)); + + this.updateClock('utc', utcNow); + this.updateClock('update', this.lastUpdateTime); + } + + updateClock(prefix, time) { + const hours = time.getUTCHours(); + const minutes = time.getUTCMinutes(); + + const hourAngle = (hours % 12) * 30 + minutes * 0.5; + const minuteAngle = minutes * 6; + + const hourHand = document.getElementById(`${prefix}-hour`); + const minuteHand = document.getElementById(`${prefix}-minute`); + + if (hourHand) hourHand.style.transform = `rotate(${hourAngle}deg)`; + if (minuteHand) minuteHand.style.transform = `rotate(${minuteAngle}deg)`; + } + + calculateDistance(aircraft) { + if (!aircraft.Latitude || !aircraft.Longitude) return null; + + // Use closest source as reference point + let minDistance = Infinity; + for (const [id, srcData] of Object.entries(aircraft.Sources || {})) { + if (srcData.Distance && srcData.Distance < minDistance) { + minDistance = srcData.Distance; + } + } + + return minDistance === Infinity ? null : minDistance; + } + + getCountryFromICAO(icao) { + if (!icao || icao.length < 6) return 'Unknown'; + + const prefix = icao[0]; + const countryMap = { + '4': 'Europe', + 'A': 'United States', + 'C': 'Canada', + 'D': 'Germany', + 'F': 'France', + 'G': 'United Kingdom', + 'I': 'Italy', + 'J': 'Japan' + }; + + return countryMap[prefix] || 'Unknown'; + } + + getCountryFlag(country) { + const flags = { + 'United States': '๐Ÿ‡บ๐Ÿ‡ธ', + 'Canada': '๐Ÿ‡จ๐Ÿ‡ฆ', + 'Germany': '๐Ÿ‡ฉ๐Ÿ‡ช', + 'France': '๐Ÿ‡ซ๐Ÿ‡ท', + 'United Kingdom': '๐Ÿ‡ฌ๐Ÿ‡ง', + 'Italy': '๐Ÿ‡ฎ๐Ÿ‡น', + 'Japan': '๐Ÿ‡ฏ๐Ÿ‡ต', + 'Europe': '๐Ÿ‡ช๐Ÿ‡บ' + }; + return flags[country] || '๐Ÿณ๏ธ'; + } + startPeriodicTasks() { // Update clocks every second - setInterval(() => this.uiManager.updateClocks(), 1000); - - // Update charts every 10 seconds - setInterval(() => this.updateCharts(), 10000); + setInterval(() => this.updateClocks(), 1000); // Periodic cleanup setInterval(() => { // Clean up old trail data, etc. }, 30000); } + + showError(message) { + console.error(message); + // Could implement toast notifications here + } } // Initialize application when DOM is ready diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js deleted file mode 100644 index 37e9e0f..0000000 --- a/assets/static/js/modules/aircraft-manager.js +++ /dev/null @@ -1,493 +0,0 @@ -// Aircraft marker and data management module -export class AircraftManager { - constructor(map) { - this.map = map; - this.aircraftData = new Map(); - this.aircraftMarkers = new Map(); - this.aircraftTrails = new Map(); - this.showTrails = false; - - // Debug: Track marker lifecycle - this.markerCreateCount = 0; - this.markerUpdateCount = 0; - this.markerRemoveCount = 0; - - // SVG icon cache - this.iconCache = new Map(); - this.loadIcons(); - - // Selected aircraft trail tracking - this.selectedAircraftCallback = null; - - // Map event listeners removed - let Leaflet handle positioning naturally - } - - async loadIcons() { - const iconTypes = ['commercial', 'helicopter', 'military', 'cargo', 'ga', 'ground']; - - for (const type of iconTypes) { - try { - const response = await fetch(`/static/icons/${type}.svg`); - const svgText = await response.text(); - this.iconCache.set(type, svgText); - } catch (error) { - console.warn(`Failed to load icon for ${type}:`, error); - // Fallback to inline SVG if needed - this.iconCache.set(type, this.createFallbackIcon(type)); - } - } - } - - createFallbackIcon(type) { - // Fallback inline SVG if file loading fails - const color = 'currentColor'; - let path = ''; - - switch (type) { - case 'helicopter': - path = ` - - - `; - break; - case 'military': - path = ``; - break; - case 'cargo': - path = ` - `; - break; - case 'ga': - path = ``; - break; - case 'ground': - path = ` - - `; - break; - default: - path = ``; - } - - return ` - - - ${path} - -`; - } - - updateAircraftData(data) { - if (data.aircraft) { - this.aircraftData.clear(); - for (const [icao, aircraft] of Object.entries(data.aircraft)) { - this.aircraftData.set(icao, aircraft); - } - } - } - - updateMarkers() { - if (!this.map) { - return; - } - - // Clear stale aircraft markers - const currentICAOs = new Set(this.aircraftData.keys()); - for (const [icao, marker] of this.aircraftMarkers) { - if (!currentICAOs.has(icao)) { - this.map.removeLayer(marker); - this.aircraftMarkers.delete(icao); - - // Remove trail if it exists - if (this.aircraftTrails.has(icao)) { - const trail = this.aircraftTrails.get(icao); - if (trail.polyline) { - this.map.removeLayer(trail.polyline); - } - this.aircraftTrails.delete(icao); - } - - // Notify if this was the selected aircraft - if (this.selectedAircraftCallback && this.selectedAircraftCallback(icao)) { - // Aircraft was selected and disappeared - could notify main app - // For now, the callback will return false automatically since selectedAircraft will be cleared - } - - this.markerRemoveCount++; - } - } - - // Update aircraft markers - only for aircraft with valid geographic coordinates - for (const [icao, aircraft] of this.aircraftData) { - const hasCoords = aircraft.Latitude && aircraft.Longitude && aircraft.Latitude !== 0 && aircraft.Longitude !== 0; - const validLat = aircraft.Latitude >= -90 && aircraft.Latitude <= 90; - const validLng = aircraft.Longitude >= -180 && aircraft.Longitude <= 180; - - if (hasCoords && validLat && validLng) { - this.updateAircraftMarker(icao, aircraft); - } - } - } - - updateAircraftMarker(icao, aircraft) { - const pos = [aircraft.Latitude, aircraft.Longitude]; - - - // Check for invalid coordinates - proper geographic bounds - const isValidLat = pos[0] >= -90 && pos[0] <= 90; - const isValidLng = pos[1] >= -180 && pos[1] <= 180; - - if (!isValidLat || !isValidLng || isNaN(pos[0]) || isNaN(pos[1])) { - console.error(`๐Ÿšจ Invalid coordinates for ${icao}: [${pos[0]}, ${pos[1]}] (lat must be -90 to +90, lng must be -180 to +180)`); - return; // Don't create/update marker with invalid coordinates - } - - if (this.aircraftMarkers.has(icao)) { - // Update existing marker - KISS approach - const marker = this.aircraftMarkers.get(icao); - - // Always update position - let Leaflet handle everything - const oldPos = marker.getLatLng(); - marker.setLatLng(pos); - - // Check if icon needs to be updated (track rotation, aircraft type, or ground status changes) - const currentRotation = marker._currentRotation || 0; - const currentType = marker._currentType || null; - const currentOnGround = marker._currentOnGround || false; - - const newType = this.getAircraftIconType(aircraft); - const rotationChanged = aircraft.Track !== undefined && Math.abs(currentRotation - aircraft.Track) > 5; - const typeChanged = currentType !== newType; - const groundStatusChanged = currentOnGround !== aircraft.OnGround; - - if (rotationChanged || typeChanged || groundStatusChanged) { - marker.setIcon(this.createAircraftIcon(aircraft)); - marker._currentRotation = aircraft.Track || 0; - marker._currentType = newType; - marker._currentOnGround = aircraft.OnGround || false; - } - - // Handle popup exactly like Leaflet expects - if (marker.isPopupOpen()) { - marker.setPopupContent(this.createPopupContent(aircraft)); - } - - this.markerUpdateCount++; - - } else { - // Create new marker - const icon = this.createAircraftIcon(aircraft); - - try { - const marker = L.marker(pos, { - icon: icon - }).addTo(this.map); - - // Store current properties for future update comparisons - marker._currentRotation = aircraft.Track || 0; - marker._currentType = this.getAircraftIconType(aircraft); - marker._currentOnGround = aircraft.OnGround || false; - - marker.bindPopup(this.createPopupContent(aircraft), { - maxWidth: 450, - className: 'aircraft-popup' - }); - - this.aircraftMarkers.set(icao, marker); - this.markerCreateCount++; - - // Force immediate visibility - if (marker._icon) { - marker._icon.style.display = 'block'; - marker._icon.style.opacity = '1'; - marker._icon.style.visibility = 'visible'; - } - } catch (error) { - console.error(`Failed to create marker for ${icao}:`, error); - } - } - - // Update trails - check both global trails and individual selected aircraft - if (this.showTrails || this.isSelectedAircraftTrailEnabled(icao)) { - this.updateAircraftTrail(icao, aircraft); - } - } - - createAircraftIcon(aircraft) { - const iconType = this.getAircraftIconType(aircraft); - const color = this.getAircraftColor(iconType); - const size = aircraft.OnGround ? 12 : 16; - const rotation = aircraft.Track || 0; - - // Get SVG template from cache - let svgTemplate = this.iconCache.get(iconType) || this.iconCache.get('commercial'); - - if (!svgTemplate) { - // Ultimate fallback - create a simple circle - svgTemplate = ` - - - - `; - } - - // Apply color and rotation to the SVG - let svg = svgTemplate - .replace(/currentColor/g, color) - .replace(/width="32"/, `width="${size * 2}"`) - .replace(/height="32"/, `height="${size * 2}"`); - - // Add rotation to the transform - if (rotation !== 0) { - svg = svg.replace(/transform="translate\(16,16\)"/, `transform="translate(16,16) rotate(${rotation})"`); - } - - return L.divIcon({ - html: svg, - iconSize: [size * 2, size * 2], - iconAnchor: [size, size], - className: 'aircraft-marker' - }); - } - - getAircraftType(aircraft) { - // For display purposes, return the actual ADS-B category - // This is used in the popup display - if (aircraft.OnGround) return 'On Ground'; - if (aircraft.Category) return aircraft.Category; - return 'Unknown'; - } - - getAircraftIconType(aircraft) { - // For icon selection, we still need basic categories - // This determines which SVG shape to use - if (aircraft.OnGround) return 'ground'; - - if (aircraft.Category) { - const cat = aircraft.Category.toLowerCase(); - - // Map to basic icon types for visual representation - if (cat.includes('helicopter') || cat.includes('rotorcraft')) return 'helicopter'; - if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military'; - if (cat.includes('cargo') || cat.includes('heavy') || cat.includes('super')) return 'cargo'; - if (cat.includes('light') || cat.includes('glider') || cat.includes('ultralight')) return 'ga'; - } - - // Default commercial icon for everything else - return 'commercial'; - } - - getAircraftColor(type) { - const colors = { - commercial: '#00ff88', - cargo: '#ff8c00', - military: '#ff4444', - ga: '#ffff00', - ground: '#888888', - helicopter: '#ff00ff' // Magenta for helicopters - }; - return colors[type] || colors.commercial; - } - - - updateAircraftTrail(icao, aircraft) { - // Use server-provided position history - if (!aircraft.position_history || aircraft.position_history.length < 2) { - // No trail data available or not enough points - if (this.aircraftTrails.has(icao)) { - const trail = this.aircraftTrails.get(icao); - if (trail.polyline) { - this.map.removeLayer(trail.polyline); - } - this.aircraftTrails.delete(icao); - } - return; - } - - // Convert position history to Leaflet format - const trailPoints = aircraft.position_history.map(point => [point.lat, point.lon]); - - // Get or create trail object - if (!this.aircraftTrails.has(icao)) { - this.aircraftTrails.set(icao, {}); - } - const trail = this.aircraftTrails.get(icao); - - // Remove old polyline if it exists - if (trail.polyline) { - this.map.removeLayer(trail.polyline); - } - - // Create gradient effect - newer points are brighter - const segments = []; - for (let i = 1; i < trailPoints.length; i++) { - const opacity = 0.2 + (0.6 * (i / trailPoints.length)); // Fade from 0.2 to 0.8 - const segment = L.polyline([trailPoints[i-1], trailPoints[i]], { - color: '#00d4ff', - weight: 2, - opacity: opacity - }); - segments.push(segment); - } - - // Create a feature group for all segments - trail.polyline = L.featureGroup(segments).addTo(this.map); - } - - createPopupContent(aircraft) { - const type = this.getAircraftType(aircraft); - const country = aircraft.country || 'Unknown'; - const flag = aircraft.flag || '๐Ÿณ๏ธ'; - - const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; - const altitudeM = altitude ? Math.round(altitude * 0.3048) : 0; - const speedKmh = aircraft.GroundSpeed ? Math.round(aircraft.GroundSpeed * 1.852) : 0; - const distance = this.calculateDistance(aircraft); - const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; - - return ` -
- - - -
- `; - } - - - calculateDistance(aircraft) { - if (!aircraft.Latitude || !aircraft.Longitude) return null; - - // Use closest source as reference point - let minDistance = Infinity; - for (const [id, srcData] of Object.entries(aircraft.sources || {})) { - if (srcData.distance && srcData.distance < minDistance) { - minDistance = srcData.distance; - } - } - - return minDistance === Infinity ? null : minDistance; - } - - - toggleTrails() { - this.showTrails = !this.showTrails; - - if (!this.showTrails) { - // Clear all trails - this.aircraftTrails.forEach((trail, icao) => { - if (trail.polyline) { - this.map.removeLayer(trail.polyline); - } - }); - this.aircraftTrails.clear(); - } - - return this.showTrails; - } - - showAircraftTrail(icao) { - const aircraft = this.aircraftData.get(icao); - if (aircraft && aircraft.position_history && aircraft.position_history.length >= 2) { - this.updateAircraftTrail(icao, aircraft); - } - } - - hideAircraftTrail(icao) { - if (this.aircraftTrails.has(icao)) { - const trail = this.aircraftTrails.get(icao); - if (trail.polyline) { - this.map.removeLayer(trail.polyline); - } - this.aircraftTrails.delete(icao); - } - } - - setSelectedAircraftCallback(callback) { - this.selectedAircraftCallback = callback; - } - - isSelectedAircraftTrailEnabled(icao) { - return this.selectedAircraftCallback && this.selectedAircraftCallback(icao); - } - - centerMapOnAircraft(includeSourcesCallback = null) { - const validAircraft = Array.from(this.aircraftData.values()) - .filter(a => a.Latitude && a.Longitude); - - const allPoints = []; - - // Add aircraft positions - validAircraft.forEach(a => { - allPoints.push([a.Latitude, a.Longitude]); - }); - - // Add source positions if callback provided - if (includeSourcesCallback && typeof includeSourcesCallback === 'function') { - const sourcePositions = includeSourcesCallback(); - allPoints.push(...sourcePositions); - } - - if (allPoints.length === 0) return; - - if (allPoints.length === 1) { - // Center on single point - this.map.setView(allPoints[0], 12); - } else { - // Fit bounds to all points (aircraft + sources) - const bounds = L.latLngBounds(allPoints); - this.map.fitBounds(bounds.pad(0.1)); - } - } - -} \ No newline at end of file diff --git a/assets/static/js/modules/map-manager.js b/assets/static/js/modules/map-manager.js deleted file mode 100644 index 94dd323..0000000 --- a/assets/static/js/modules/map-manager.js +++ /dev/null @@ -1,372 +0,0 @@ -// Map and visualization management module -export class MapManager { - constructor() { - this.map = null; - this.coverageMap = null; - this.mapOrigin = null; - - // Source markers and overlays - this.sourceMarkers = new Map(); - this.rangeCircles = new Map(); - this.showSources = true; - this.showRange = false; - this.selectedSource = null; - this.heatmapLayer = null; - - // Data references - this.sourcesData = new Map(); - - // Map theme - this.isDarkMode = false; - this.currentTileLayer = null; - this.coverageTileLayer = null; - } - - async initializeMap() { - // Get origin from server - let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback - try { - const response = await fetch('/api/origin'); - if (response.ok) { - origin = await response.json(); - } - } catch (error) { - console.warn('Could not fetch origin, using default:', error); - } - - // Store origin for reset functionality - this.mapOrigin = origin; - - this.map = L.map('map').setView([origin.latitude, origin.longitude], 10); - - // Light tile layer by default - this.currentTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { - attribution: '© OpenStreetMap contributors © CARTO', - subdomains: 'abcd', - maxZoom: 19 - }).addTo(this.map); - - // Add scale control for distance estimation - L.control.scale({ - metric: true, - imperial: true, - position: 'bottomright' - }).addTo(this.map); - - return this.map; - } - - async initializeCoverageMap() { - if (!this.coverageMap) { - // Get origin from server - let origin = { latitude: 51.4700, longitude: -0.4600 }; // fallback - try { - const response = await fetch('/api/origin'); - if (response.ok) { - origin = await response.json(); - } - } catch (error) { - console.warn('Could not fetch origin for coverage map, using default:', error); - } - - this.coverageMap = L.map('coverage-map').setView([origin.latitude, origin.longitude], 10); - - this.coverageTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(this.coverageMap); - - // Add scale control for distance estimation - L.control.scale({ - metric: true, - imperial: true, - position: 'bottomright' - }).addTo(this.coverageMap); - } - - return this.coverageMap; - } - - updateSourcesData(data) { - if (data.sources) { - this.sourcesData.clear(); - data.sources.forEach(source => { - this.sourcesData.set(source.id, source); - }); - } - } - - updateSourceMarkers() { - if (!this.map || !this.showSources) return; - - // Remove markers for sources that no longer exist - const currentSourceIds = new Set(this.sourcesData.keys()); - for (const [id, marker] of this.sourceMarkers) { - if (!currentSourceIds.has(id)) { - this.map.removeLayer(marker); - this.sourceMarkers.delete(id); - } - } - - // Update or create markers for current sources - for (const [id, source] of this.sourcesData) { - if (source.latitude && source.longitude) { - if (this.sourceMarkers.has(id)) { - // Update existing marker - const marker = this.sourceMarkers.get(id); - - // Update marker style if status changed - marker.setStyle({ - radius: source.active ? 10 : 6, - fillColor: source.active ? '#00d4ff' : '#666666', - fillOpacity: 0.8 - }); - - // Update popup content if it's open - if (marker.isPopupOpen()) { - marker.setPopupContent(this.createSourcePopupContent(source)); - } - } else { - // Create new marker - const marker = L.circleMarker([source.latitude, source.longitude], { - radius: source.active ? 10 : 6, - fillColor: source.active ? '#00d4ff' : '#666666', - color: '#ffffff', - weight: 2, - fillOpacity: 0.8, - className: 'source-marker' - }).addTo(this.map); - - marker.bindPopup(this.createSourcePopupContent(source), { - maxWidth: 300 - }); - - this.sourceMarkers.set(id, marker); - } - } - } - - this.updateSourcesLegend(); - } - - updateRangeCircles() { - if (!this.map || !this.showRange) return; - - // Clear existing circles - this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); - this.rangeCircles.clear(); - - // Add range circles for active sources - for (const [id, source] of this.sourcesData) { - if (source.active && source.latitude && source.longitude) { - // Add multiple range circles (50km, 100km, 200km) - const ranges = [50000, 100000, 200000]; - ranges.forEach((range, index) => { - const circle = L.circle([source.latitude, source.longitude], { - radius: range, - fillColor: 'transparent', - color: '#00d4ff', - weight: 2, - opacity: 0.7 - (index * 0.15), - dashArray: '8,4' - }).addTo(this.map); - - this.rangeCircles.set(`${id}_${range}`, circle); - }); - } - } - } - - createSourcePopupContent(source, aircraftData) { - const aircraftCount = aircraftData ? Array.from(aircraftData.values()) - .filter(aircraft => aircraft.sources && aircraft.sources[source.id]).length : 0; - - return ` -
-

${source.name}

-

ID: ${source.id}

-

Location: ${source.latitude.toFixed(4)}ยฐ, ${source.longitude.toFixed(4)}ยฐ

-

Status: ${source.active ? 'Active' : 'Inactive'}

-

Aircraft: ${aircraftCount}

-

Messages: ${source.messages || 0}

-

Last Seen: ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}

-
- `; - } - - updateSourcesLegend() { - const legend = document.getElementById('sources-legend'); - if (!legend) return; - - legend.innerHTML = ''; - - for (const [id, source] of this.sourcesData) { - const item = document.createElement('div'); - item.className = 'legend-item'; - item.innerHTML = ` - - ${source.name} - `; - legend.appendChild(item); - } - } - - resetMap() { - if (this.mapOrigin && this.map) { - this.map.setView([this.mapOrigin.latitude, this.mapOrigin.longitude], 10); - } - } - - toggleRangeCircles() { - this.showRange = !this.showRange; - - if (this.showRange) { - this.updateRangeCircles(); - } else { - this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); - this.rangeCircles.clear(); - } - - return this.showRange; - } - - toggleSources() { - this.showSources = !this.showSources; - - if (this.showSources) { - this.updateSourceMarkers(); - } else { - this.sourceMarkers.forEach(marker => this.map.removeLayer(marker)); - this.sourceMarkers.clear(); - } - - return this.showSources; - } - - toggleDarkMode() { - this.isDarkMode = !this.isDarkMode; - - const lightUrl = 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'; - const darkUrl = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'; - const tileUrl = this.isDarkMode ? darkUrl : lightUrl; - - const tileOptions = { - attribution: '© OpenStreetMap contributors © CARTO', - subdomains: 'abcd', - maxZoom: 19 - }; - - // Update main map - if (this.map && this.currentTileLayer) { - this.map.removeLayer(this.currentTileLayer); - this.currentTileLayer = L.tileLayer(tileUrl, tileOptions).addTo(this.map); - } - - // Update coverage map - if (this.coverageMap && this.coverageTileLayer) { - this.coverageMap.removeLayer(this.coverageTileLayer); - this.coverageTileLayer = L.tileLayer(tileUrl, { - attribution: '© OpenStreetMap contributors' - }).addTo(this.coverageMap); - } - - return this.isDarkMode; - } - - // Coverage map methods - updateCoverageControls() { - const select = document.getElementById('coverage-source'); - if (!select) return; - - select.innerHTML = ''; - - for (const [id, source] of this.sourcesData) { - const option = document.createElement('option'); - option.value = id; - option.textContent = source.name; - select.appendChild(option); - } - } - - async updateCoverageDisplay() { - if (!this.selectedSource || !this.coverageMap) return; - - try { - const response = await fetch(`/api/coverage/${this.selectedSource}`); - const data = await response.json(); - - // Clear existing coverage markers - this.coverageMap.eachLayer(layer => { - if (layer instanceof L.CircleMarker) { - this.coverageMap.removeLayer(layer); - } - }); - - // Add coverage points - data.points.forEach(point => { - const intensity = Math.max(0, (point.signal + 50) / 50); // Normalize signal strength - L.circleMarker([point.lat, point.lon], { - radius: 3, - fillColor: this.getSignalColor(point.signal), - color: 'white', - weight: 1, - fillOpacity: intensity - }).addTo(this.coverageMap); - }); - - } catch (error) { - console.error('Failed to load coverage data:', error); - } - } - - async toggleHeatmap() { - if (!this.selectedSource) { - alert('Please select a source first'); - return false; - } - - if (this.heatmapLayer) { - this.coverageMap.removeLayer(this.heatmapLayer); - this.heatmapLayer = null; - return false; - } else { - try { - const response = await fetch(`/api/heatmap/${this.selectedSource}`); - const data = await response.json(); - - // Create heatmap layer (simplified) - this.createHeatmapOverlay(data); - return true; - - } catch (error) { - console.error('Failed to load heatmap data:', error); - return false; - } - } - } - - getSignalColor(signal) { - if (signal > -10) return '#00ff88'; - if (signal > -20) return '#ffff00'; - if (signal > -30) return '#ff8c00'; - return '#ff4444'; - } - - createHeatmapOverlay(data) { - // Simplified heatmap implementation - // In production, would use proper heatmap library like Leaflet.heat - } - - setSelectedSource(sourceId) { - this.selectedSource = sourceId; - } - - getSourcePositions() { - const positions = []; - for (const [id, source] of this.sourcesData) { - if (source.latitude && source.longitude) { - positions.push([source.latitude, source.longitude]); - } - } - return positions; - } -} \ No newline at end of file diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js deleted file mode 100644 index 3af6789..0000000 --- a/assets/static/js/modules/ui-manager.js +++ /dev/null @@ -1,321 +0,0 @@ -// UI and table management module -export class UIManager { - constructor() { - this.aircraftData = new Map(); - this.sourcesData = new Map(); - this.stats = {}; - this.currentView = 'map-view'; - this.lastUpdateTime = new Date(); - } - - initializeViews() { - const viewButtons = document.querySelectorAll('.view-btn'); - const views = document.querySelectorAll('.view'); - - viewButtons.forEach(btn => { - btn.addEventListener('click', () => { - const viewId = btn.id.replace('-btn', ''); - this.switchView(viewId); - }); - }); - } - - switchView(viewId) { - // Update buttons - document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active')); - const activeBtn = document.getElementById(`${viewId}-btn`); - if (activeBtn) { - activeBtn.classList.add('active'); - } - - // Update views (viewId already includes the full view ID like "map-view") - document.querySelectorAll('.view').forEach(view => view.classList.remove('active')); - const activeView = document.getElementById(viewId); - if (activeView) { - activeView.classList.add('active'); - } else { - console.warn(`View element not found: ${viewId}`); - return; - } - - this.currentView = viewId; - return viewId; - } - - updateData(data) { - // Update aircraft data - if (data.aircraft) { - this.aircraftData.clear(); - for (const [icao, aircraft] of Object.entries(data.aircraft)) { - this.aircraftData.set(icao, aircraft); - } - } - - // Update sources data - if (data.sources) { - this.sourcesData.clear(); - data.sources.forEach(source => { - this.sourcesData.set(source.id, source); - }); - } - - // Update statistics - if (data.stats) { - this.stats = data.stats; - } - - this.lastUpdateTime = new Date(); - } - - updateAircraftTable() { - // Note: This table shows ALL aircraft we're tracking, including those without - // position data. Aircraft without positions will show "No position" in the - // location column but still provide useful info like callsign, altitude, etc. - const tbody = document.getElementById('aircraft-tbody'); - if (!tbody) return; - - tbody.innerHTML = ''; - - let filteredData = Array.from(this.aircraftData.values()); - - // Apply filters - const searchTerm = document.getElementById('search-input')?.value.toLowerCase() || ''; - const sourceFilter = document.getElementById('source-filter')?.value || ''; - - if (searchTerm) { - filteredData = filteredData.filter(aircraft => - (aircraft.Callsign && aircraft.Callsign.toLowerCase().includes(searchTerm)) || - (aircraft.ICAO24 && aircraft.ICAO24.toLowerCase().includes(searchTerm)) || - (aircraft.Squawk && aircraft.Squawk.includes(searchTerm)) - ); - } - - if (sourceFilter) { - filteredData = filteredData.filter(aircraft => - aircraft.sources && aircraft.sources[sourceFilter] - ); - } - - // Sort data - const sortBy = document.getElementById('sort-select')?.value || 'distance'; - this.sortAircraft(filteredData, sortBy); - - // Populate table - filteredData.forEach(aircraft => { - const row = this.createTableRow(aircraft); - tbody.appendChild(row); - }); - - // Update source filter options - this.updateSourceFilter(); - } - - createTableRow(aircraft) { - const type = this.getAircraftType(aircraft); - const icao = aircraft.ICAO24 || 'N/A'; - const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; - const distance = this.calculateDistance(aircraft); - const sources = aircraft.sources ? Object.keys(aircraft.sources).length : 0; - const bestSignal = this.getBestSignalFromSources(aircraft.sources); - - const row = document.createElement('tr'); - row.innerHTML = ` - ${icao} - ${aircraft.Callsign || '-'} - ${aircraft.Squawk || '-'} - ${altitude ? `${altitude} ft` : '-'} - ${aircraft.GroundSpeed || '-'} kt - ${distance ? distance.toFixed(1) : '-'} km - ${aircraft.Track || '-'}ยฐ - ${sources} - ${bestSignal ? bestSignal.toFixed(1) : '-'} - ${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s - `; - - row.addEventListener('click', () => { - if (aircraft.Latitude && aircraft.Longitude) { - // Trigger event to switch to map and focus on aircraft - const event = new CustomEvent('aircraftSelected', { - detail: { icao, aircraft } - }); - document.dispatchEvent(event); - } - }); - - return row; - } - - getAircraftType(aircraft) { - if (aircraft.OnGround) return 'ground'; - if (aircraft.Category) { - const cat = aircraft.Category.toLowerCase(); - if (cat.includes('military')) return 'military'; - if (cat.includes('cargo') || cat.includes('heavy')) return 'cargo'; - if (cat.includes('light') || cat.includes('glider')) return 'ga'; - } - if (aircraft.Callsign) { - const cs = aircraft.Callsign.toLowerCase(); - if (cs.includes('mil') || cs.includes('army') || cs.includes('navy')) return 'military'; - if (cs.includes('cargo') || cs.includes('fedex') || cs.includes('ups')) return 'cargo'; - } - return 'commercial'; - } - - getBestSignalFromSources(sources) { - if (!sources) return null; - let bestSignal = -999; - for (const [id, data] of Object.entries(sources)) { - if (data.signal_level > bestSignal) { - bestSignal = data.signal_level; - } - } - return bestSignal === -999 ? null : bestSignal; - } - - getSignalClass(signal) { - if (!signal) return ''; - if (signal > -10) return 'signal-strong'; - if (signal > -20) return 'signal-good'; - if (signal > -30) return 'signal-weak'; - return 'signal-poor'; - } - - updateSourceFilter() { - const select = document.getElementById('source-filter'); - if (!select) return; - - const currentValue = select.value; - - // Clear options except "All Sources" - select.innerHTML = ''; - - // Add source options - for (const [id, source] of this.sourcesData) { - const option = document.createElement('option'); - option.value = id; - option.textContent = source.name; - if (id === currentValue) option.selected = true; - select.appendChild(option); - } - } - - sortAircraft(aircraft, sortBy) { - aircraft.sort((a, b) => { - switch (sortBy) { - case 'distance': - return (this.calculateDistance(a) || Infinity) - (this.calculateDistance(b) || Infinity); - case 'altitude': - return (b.Altitude || b.BaroAltitude || 0) - (a.Altitude || a.BaroAltitude || 0); - case 'speed': - return (b.GroundSpeed || 0) - (a.GroundSpeed || 0); - case 'flight': - return (a.Callsign || a.ICAO24 || '').localeCompare(b.Callsign || b.ICAO24 || ''); - case 'icao': - return (a.ICAO24 || '').localeCompare(b.ICAO24 || ''); - case 'squawk': - return (a.Squawk || '').localeCompare(b.Squawk || ''); - case 'signal': - return (this.getBestSignalFromSources(b.sources) || -999) - (this.getBestSignalFromSources(a.sources) || -999); - case 'age': - return (a.Age || 0) - (b.Age || 0); - default: - return 0; - } - }); - } - - calculateDistance(aircraft) { - if (!aircraft.Latitude || !aircraft.Longitude) return null; - - // Use closest source as reference point - let minDistance = Infinity; - for (const [id, srcData] of Object.entries(aircraft.sources || {})) { - if (srcData.distance && srcData.distance < minDistance) { - minDistance = srcData.distance; - } - } - - return minDistance === Infinity ? null : minDistance; - } - - updateStatistics() { - const totalAircraftEl = document.getElementById('total-aircraft'); - const activeSourcesEl = document.getElementById('active-sources'); - const maxRangeEl = document.getElementById('max-range'); - const messagesSecEl = document.getElementById('messages-sec'); - - if (totalAircraftEl) totalAircraftEl.textContent = this.aircraftData.size; - if (activeSourcesEl) { - activeSourcesEl.textContent = Array.from(this.sourcesData.values()).filter(s => s.active).length; - } - - // Calculate max range - let maxDistance = 0; - for (const aircraft of this.aircraftData.values()) { - const distance = this.calculateDistance(aircraft); - if (distance && distance > maxDistance) { - maxDistance = distance; - } - } - if (maxRangeEl) maxRangeEl.textContent = `${maxDistance.toFixed(1)} km`; - - // Update message rate - const totalMessages = this.stats.total_messages || 0; - if (messagesSecEl) messagesSecEl.textContent = Math.round(totalMessages / 60); - } - - updateHeaderInfo() { - const aircraftCountEl = document.getElementById('aircraft-count'); - const sourcesCountEl = document.getElementById('sources-count'); - - if (aircraftCountEl) aircraftCountEl.textContent = `${this.aircraftData.size} aircraft`; - if (sourcesCountEl) sourcesCountEl.textContent = `${this.sourcesData.size} sources`; - - this.updateClocks(); - } - - updateConnectionStatus(status) { - const statusEl = document.getElementById('connection-status'); - if (statusEl) { - statusEl.className = `connection-status ${status}`; - statusEl.textContent = status === 'connected' ? 'Connected' : 'Disconnected'; - } - } - - initializeEventListeners() { - const searchInput = document.getElementById('search-input'); - const sortSelect = document.getElementById('sort-select'); - const sourceFilter = document.getElementById('source-filter'); - - if (searchInput) searchInput.addEventListener('input', () => this.updateAircraftTable()); - if (sortSelect) sortSelect.addEventListener('change', () => this.updateAircraftTable()); - if (sourceFilter) sourceFilter.addEventListener('change', () => this.updateAircraftTable()); - } - - updateClocks() { - const now = new Date(); - const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000)); - - this.updateClock('utc', utcNow); - this.updateClock('update', this.lastUpdateTime); - } - - updateClock(prefix, time) { - const hours = time.getUTCHours(); - const minutes = time.getUTCMinutes(); - - const hourAngle = (hours % 12) * 30 + minutes * 0.5; - const minuteAngle = minutes * 6; - - const hourHand = document.getElementById(`${prefix}-hour`); - const minuteHand = document.getElementById(`${prefix}-minute`); - - if (hourHand) hourHand.style.transform = `rotate(${hourAngle}deg)`; - if (minuteHand) minuteHand.style.transform = `rotate(${minuteAngle}deg)`; - } - - showError(message) { - console.error(message); - // Could implement toast notifications here - } -} \ No newline at end of file diff --git a/assets/static/js/modules/websocket.js b/assets/static/js/modules/websocket.js deleted file mode 100644 index 5fa733c..0000000 --- a/assets/static/js/modules/websocket.js +++ /dev/null @@ -1,54 +0,0 @@ -// WebSocket communication module -export class WebSocketManager { - constructor(onMessage, onStatusChange) { - this.websocket = null; - this.onMessage = onMessage; - this.onStatusChange = onStatusChange; - } - - async connect() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws`; - - try { - this.websocket = new WebSocket(wsUrl); - - this.websocket.onopen = () => { - this.onStatusChange('connected'); - }; - - this.websocket.onclose = () => { - this.onStatusChange('disconnected'); - // Reconnect after 5 seconds - setTimeout(() => this.connect(), 5000); - }; - - this.websocket.onerror = (error) => { - console.error('WebSocket error:', error); - this.onStatusChange('disconnected'); - }; - - this.websocket.onmessage = (event) => { - try { - const message = JSON.parse(event.data); - - - this.onMessage(message); - } catch (error) { - console.error('Failed to parse WebSocket message:', error); - } - }; - - } catch (error) { - console.error('WebSocket connection failed:', error); - this.onStatusChange('disconnected'); - } - } - - disconnect() { - if (this.websocket) { - this.websocket.close(); - this.websocket = null; - } - } -} \ No newline at end of file diff --git a/beast-dump-with-heli.bin b/beast-dump-with-heli.bin deleted file mode 100644 index fa579ea..0000000 Binary files a/beast-dump-with-heli.bin and /dev/null differ diff --git a/cmd/beast-dump/main.go b/cmd/beast-dump/main.go deleted file mode 100644 index 4d585de..0000000 --- a/cmd/beast-dump/main.go +++ /dev/null @@ -1,440 +0,0 @@ -// Package main provides a utility for parsing and displaying Beast format ADS-B data. -// -// beast-dump can read from TCP sockets (dump1090 streams) or files containing -// Beast binary data, decode Mode S/ADS-B messages, and display the results -// in human-readable format on the console. -// -// Usage: -// beast-dump -tcp host:port # Read from TCP socket -// beast-dump -file path/to/file # Read from file -// beast-dump -verbose # Show detailed message parsing -// -// Examples: -// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream -// beast-dump -file beast.test # Parse Beast data from file -// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing -package main - -import ( - "flag" - "fmt" - "io" - "log" - "net" - "os" - "time" - - "skyview/internal/beast" - "skyview/internal/modes" -) - -// Config holds command-line configuration -type Config struct { - TCPAddress string // TCP address for Beast stream (e.g., "localhost:30005") - FilePath string // File path for Beast data - Verbose bool // Enable verbose output - Count int // Maximum messages to process (0 = unlimited) -} - -// BeastDumper handles Beast data parsing and console output -type BeastDumper struct { - config *Config - parser *beast.Parser - decoder *modes.Decoder - stats struct { - totalMessages int64 - validMessages int64 - aircraftSeen map[uint32]bool - startTime time.Time - lastMessageTime time.Time - } -} - -func main() { - config := parseFlags() - - if config.TCPAddress == "" && config.FilePath == "" { - fmt.Fprintf(os.Stderr, "Error: Must specify either -tcp or -file\n") - flag.Usage() - os.Exit(1) - } - - if config.TCPAddress != "" && config.FilePath != "" { - fmt.Fprintf(os.Stderr, "Error: Cannot specify both -tcp and -file\n") - flag.Usage() - os.Exit(1) - } - - dumper := NewBeastDumper(config) - - if err := dumper.Run(); err != nil { - log.Fatalf("Error: %v", err) - } -} - -// parseFlags parses command-line flags and returns configuration -func parseFlags() *Config { - config := &Config{} - - flag.StringVar(&config.TCPAddress, "tcp", "", "TCP address for Beast stream (e.g., localhost:30005)") - flag.StringVar(&config.FilePath, "file", "", "File path for Beast data") - flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output") - flag.IntVar(&config.Count, "count", 0, "Maximum messages to process (0 = unlimited)") - - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0]) - fmt.Fprintf(os.Stderr, "\nBeast format ADS-B data parser and console dumper\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") - flag.PrintDefaults() - fmt.Fprintf(os.Stderr, "\nExamples:\n") - fmt.Fprintf(os.Stderr, " %s -tcp svovel:30005\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " %s -file beast.test\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " %s -tcp localhost:30005 -verbose -count 100\n", os.Args[0]) - } - - flag.Parse() - return config -} - -// NewBeastDumper creates a new Beast data dumper -func NewBeastDumper(config *Config) *BeastDumper { - return &BeastDumper{ - config: config, - decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default - stats: struct { - totalMessages int64 - validMessages int64 - aircraftSeen map[uint32]bool - startTime time.Time - lastMessageTime time.Time - }{ - aircraftSeen: make(map[uint32]bool), - startTime: time.Now(), - }, - } -} - -// Run starts the Beast data processing -func (d *BeastDumper) Run() error { - fmt.Printf("Beast Data Dumper\n") - fmt.Printf("=================\n\n") - - var reader io.Reader - var closer io.Closer - - if d.config.TCPAddress != "" { - conn, err := d.connectTCP() - if err != nil { - return fmt.Errorf("TCP connection failed: %w", err) - } - reader = conn - closer = conn - fmt.Printf("Connected to: %s\n", d.config.TCPAddress) - } else { - file, err := d.openFile() - if err != nil { - return fmt.Errorf("file open failed: %w", err) - } - reader = file - closer = file - fmt.Printf("Reading file: %s\n", d.config.FilePath) - } - - defer closer.Close() - - // Create Beast parser - d.parser = beast.NewParser(reader, "beast-dump") - - fmt.Printf("Verbose mode: %t\n", d.config.Verbose) - if d.config.Count > 0 { - fmt.Printf("Message limit: %d\n", d.config.Count) - } - fmt.Printf("\nStarting Beast data parsing...\n") - fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n", - "Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded") - fmt.Printf("%s\n", - "------------------------------------------------------------------------") - - return d.parseMessages() -} - -// connectTCP establishes TCP connection to Beast stream -func (d *BeastDumper) connectTCP() (net.Conn, error) { - fmt.Printf("Connecting to %s...\n", d.config.TCPAddress) - - conn, err := net.DialTimeout("tcp", d.config.TCPAddress, 10*time.Second) - if err != nil { - return nil, err - } - - return conn, nil -} - -// openFile opens Beast data file -func (d *BeastDumper) openFile() (*os.File, error) { - file, err := os.Open(d.config.FilePath) - if err != nil { - return nil, err - } - - // Check file size - stat, err := file.Stat() - if err != nil { - file.Close() - return nil, err - } - - fmt.Printf("File size: %d bytes\n", stat.Size()) - return file, nil -} - -// parseMessages processes Beast messages and outputs decoded data -func (d *BeastDumper) parseMessages() error { - for { - // Check message count limit - if d.config.Count > 0 && d.stats.totalMessages >= int64(d.config.Count) { - fmt.Printf("\nReached message limit of %d\n", d.config.Count) - break - } - - // Parse Beast message - msg, err := d.parser.ReadMessage() - if err != nil { - if err == io.EOF { - fmt.Printf("\nEnd of data reached\n") - break - } - if d.config.Verbose { - fmt.Printf("Parse error: %v\n", err) - } - continue - } - - d.stats.totalMessages++ - d.stats.lastMessageTime = time.Now() - - // Display Beast message info - d.displayMessage(msg) - - // Decode Mode S data if available - if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong { - d.decodeAndDisplay(msg) - } - - d.stats.validMessages++ - } - - d.displayStatistics() - return nil -} - -// displayMessage shows basic Beast message information -func (d *BeastDumper) displayMessage(msg *beast.Message) { - timestamp := msg.ReceivedAt.Format("15:04:05") - - // Extract ICAO if available - icao := "------" - if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong { - if icaoAddr, err := msg.GetICAO24(); err == nil { - icao = fmt.Sprintf("%06X", icaoAddr) - d.stats.aircraftSeen[icaoAddr] = true - } - } - - // Beast message type - typeStr := d.formatMessageType(msg.Type) - - // Signal strength - signal := msg.GetSignalStrength() - signalStr := fmt.Sprintf("%6.1f", signal) - - // Data preview - dataStr := d.formatDataPreview(msg.Data) - - fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ", - timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data)) -} - -// decodeAndDisplay attempts to decode Mode S message and display results -func (d *BeastDumper) decodeAndDisplay(msg *beast.Message) { - aircraft, err := d.decoder.Decode(msg.Data) - if err != nil { - if d.config.Verbose { - fmt.Printf("Decode error: %v\n", err) - } else { - fmt.Printf("(decode failed)\n") - } - return - } - - // Display decoded information - info := d.formatAircraftInfo(aircraft) - fmt.Printf("%s\n", info) - - // Verbose details - if d.config.Verbose { - d.displayVerboseInfo(aircraft, msg) - } -} - -// formatMessageType converts Beast message type to string -func (d *BeastDumper) formatMessageType(msgType uint8) string { - switch msgType { - case beast.BeastModeAC: - return "Mode A/C" - case beast.BeastModeS: - return "Mode S" - case beast.BeastModeSLong: - return "Mode S Long" - case beast.BeastStatusMsg: - return "Status" - default: - return fmt.Sprintf("Type %02X", msgType) - } -} - -// formatDataPreview creates a hex preview of message data -func (d *BeastDumper) formatDataPreview(data []byte) string { - if len(data) == 0 { - return "" - } - - preview := "" - for i, b := range data { - if i >= 4 { // Show first 4 bytes - break - } - preview += fmt.Sprintf("%02X", b) - } - - if len(data) > 4 { - preview += "..." - } - - return preview -} - -// formatAircraftInfo creates a summary of decoded aircraft information -func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string { - parts := []string{} - - // Callsign - if aircraft.Callsign != "" { - parts = append(parts, fmt.Sprintf("CS:%s", aircraft.Callsign)) - } - - // Position - if aircraft.Latitude != 0 || aircraft.Longitude != 0 { - parts = append(parts, fmt.Sprintf("POS:%.4f,%.4f", aircraft.Latitude, aircraft.Longitude)) - } - - // Altitude - if aircraft.Altitude != 0 { - parts = append(parts, fmt.Sprintf("ALT:%dft", aircraft.Altitude)) - } - - // Speed and track - if aircraft.GroundSpeed != 0 { - parts = append(parts, fmt.Sprintf("SPD:%dkt", aircraft.GroundSpeed)) - } - if aircraft.Track != 0 { - parts = append(parts, fmt.Sprintf("HDG:%dยฐ", aircraft.Track)) - } - - // Vertical rate - if aircraft.VerticalRate != 0 { - parts = append(parts, fmt.Sprintf("VS:%d", aircraft.VerticalRate)) - } - - // Squawk - if aircraft.Squawk != "" { - parts = append(parts, fmt.Sprintf("SQ:%s", aircraft.Squawk)) - } - - // Emergency - if aircraft.Emergency != "" && aircraft.Emergency != "None" { - parts = append(parts, fmt.Sprintf("EMG:%s", aircraft.Emergency)) - } - - if len(parts) == 0 { - return "(no data decoded)" - } - - info := "" - for i, part := range parts { - if i > 0 { - info += " " - } - info += part - } - - return info -} - -// displayVerboseInfo shows detailed aircraft information -func (d *BeastDumper) displayVerboseInfo(aircraft *modes.Aircraft, msg *beast.Message) { - fmt.Printf(" Message Details:\n") - fmt.Printf(" Raw Data: %s\n", d.formatHexData(msg.Data)) - fmt.Printf(" Timestamp: %s\n", msg.ReceivedAt.Format("15:04:05.000")) - fmt.Printf(" Signal: %.2f dBFS\n", msg.GetSignalStrength()) - - fmt.Printf(" Aircraft Data:\n") - if aircraft.Callsign != "" { - fmt.Printf(" Callsign: %s\n", aircraft.Callsign) - } - if aircraft.Latitude != 0 || aircraft.Longitude != 0 { - fmt.Printf(" Position: %.6f, %.6f\n", aircraft.Latitude, aircraft.Longitude) - } - if aircraft.Altitude != 0 { - fmt.Printf(" Altitude: %d ft\n", aircraft.Altitude) - } - if aircraft.GroundSpeed != 0 || aircraft.Track != 0 { - fmt.Printf(" Speed/Track: %d kt @ %dยฐ\n", aircraft.GroundSpeed, aircraft.Track) - } - if aircraft.VerticalRate != 0 { - fmt.Printf(" Vertical Rate: %d ft/min\n", aircraft.VerticalRate) - } - if aircraft.Squawk != "" { - fmt.Printf(" Squawk: %s\n", aircraft.Squawk) - } - if aircraft.Category != "" { - fmt.Printf(" Category: %s\n", aircraft.Category) - } - fmt.Printf("\n") -} - -// formatHexData creates a formatted hex dump of data -func (d *BeastDumper) formatHexData(data []byte) string { - result := "" - for i, b := range data { - if i > 0 { - result += " " - } - result += fmt.Sprintf("%02X", b) - } - return result -} - -// displayStatistics shows final parsing statistics -func (d *BeastDumper) displayStatistics() { - duration := time.Since(d.stats.startTime) - - fmt.Printf("\nStatistics:\n") - fmt.Printf("===========\n") - fmt.Printf("Total messages: %d\n", d.stats.totalMessages) - fmt.Printf("Valid messages: %d\n", d.stats.validMessages) - fmt.Printf("Unique aircraft: %d\n", len(d.stats.aircraftSeen)) - fmt.Printf("Duration: %v\n", duration.Round(time.Second)) - - if d.stats.totalMessages > 0 && duration > 0 { - rate := float64(d.stats.totalMessages) / duration.Seconds() - fmt.Printf("Message rate: %.1f msg/sec\n", rate) - } - - if len(d.stats.aircraftSeen) > 0 { - fmt.Printf("\nAircraft seen:\n") - for icao := range d.stats.aircraftSeen { - fmt.Printf(" %06X\n", icao) - } - } -} \ No newline at end of file diff --git a/debian/DEBIAN/control b/debian/DEBIAN/control index a208957..e0a0327 100644 --- a/debian/DEBIAN/control +++ b/debian/DEBIAN/control @@ -19,5 +19,4 @@ Description: Multi-source ADS-B aircraft tracker with Beast format support - Historical flight tracking - Mobile-responsive design - Systemd integration for service management - - Beast-dump utility for raw ADS-B data analysis Homepage: https://github.com/skyview/skyview diff --git a/debian/usr/bin/beast-dump b/debian/usr/bin/beast-dump deleted file mode 100755 index 99c154e..0000000 Binary files a/debian/usr/bin/beast-dump and /dev/null differ diff --git a/debian/usr/share/man/man1/beast-dump.1 b/debian/usr/share/man/man1/beast-dump.1 deleted file mode 100644 index bc94ad6..0000000 --- a/debian/usr/share/man/man1/beast-dump.1 +++ /dev/null @@ -1,95 +0,0 @@ -.TH BEAST-DUMP 1 "2024-08-24" "SkyView 2.0.0" "User Commands" -.SH NAME -beast-dump \- Utility for analyzing raw ADS-B data in Beast binary format -.SH SYNOPSIS -.B beast-dump -[\fIOPTIONS\fR] [\fIFILE\fR] -.SH DESCRIPTION -beast-dump is a command-line utility for analyzing and decoding ADS-B -(Automatic Dependent Surveillance-Broadcast) data stored in Beast binary -format. It can read from files or connect to Beast format TCP streams -to decode and display aircraft messages. -.PP -The Beast format is a compact binary representation of Mode S/ADS-B -messages commonly used by dump1090 and similar software-defined radio -applications for aircraft tracking. -.SH OPTIONS -.TP -.B \-host \fIstring\fR -Connect to TCP host instead of reading from file -.TP -.B \-port \fIint\fR -TCP port to connect to (default 30005) -.TP -.B \-format \fIstring\fR -Output format: text, json, or csv (default "text") -.TP -.B \-filter \fIstring\fR -Filter by ICAO hex code (e.g., "A1B2C3") -.TP -.B \-types \fIstring\fR -Message types to display (comma-separated) -.TP -.B \-count \fIint\fR -Maximum number of messages to process -.TP -.B \-stats -Show statistics summary -.TP -.B \-verbose -Enable verbose output -.TP -.B \-h, \-help -Show help message and exit -.SH EXAMPLES -.TP -Analyze Beast format file: -.B beast-dump data.bin -.TP -Connect to live Beast stream: -.B beast-dump \-host localhost \-port 30005 -.TP -Export to JSON format with statistics: -.B beast-dump \-format json \-stats data.bin -.TP -Filter messages for specific aircraft: -.B beast-dump \-filter A1B2C3 \-verbose data.bin -.TP -Process only first 1000 messages as CSV: -.B beast-dump \-format csv \-count 1000 data.bin -.SH OUTPUT FORMAT -The default text output shows decoded message fields: -.PP -.nf -ICAO: A1B2C3 Type: 17 Time: 12:34:56.789 - Position: 51.4700, -0.4600 - Altitude: 35000 ft - Speed: 450 kt - Track: 090ยฐ -.fi -.PP -JSON output provides structured data suitable for further processing. -CSV output includes headers and is suitable for spreadsheet import. -.SH MESSAGE TYPES -Common ADS-B message types: -.IP \(bu 2 -Type 4/20: Altitude and identification -.IP \(bu 2 -Type 5/21: Surface position -.IP \(bu 2 -Type 9/18/22: Airborne position (baro altitude) -.IP \(bu 2 -Type 10/18/22: Airborne position (GNSS altitude) -.IP \(bu 2 -Type 17: Extended squitter ADS-B -.IP \(bu 2 -Type 19: Military extended squitter -.SH FILES -Beast format files typically use .bin or .beast extensions. -.SH SEE ALSO -.BR skyview (1), -.BR dump1090 (1) -.SH BUGS -Report bugs at: https://github.com/skyview/skyview/issues -.SH AUTHOR -SkyView Team \ No newline at end of file diff --git a/debian/usr/share/man/man1/skyview.1 b/debian/usr/share/man/man1/skyview.1 deleted file mode 100644 index 34241fc..0000000 --- a/debian/usr/share/man/man1/skyview.1 +++ /dev/null @@ -1,88 +0,0 @@ -.TH SKYVIEW 1 "2024-08-24" "SkyView 2.0.0" "User Commands" -.SH NAME -skyview \- Multi-source ADS-B aircraft tracker with Beast format support -.SH SYNOPSIS -.B skyview -[\fIOPTIONS\fR] -.SH DESCRIPTION -SkyView is a standalone application that connects to multiple dump1090 Beast -format TCP streams and provides a modern web frontend for aircraft tracking. -It features real-time aircraft tracking, signal strength analysis, coverage -mapping, and 3D radar visualization. -.PP -The application serves a web interface on port 8080 by default and connects -to one or more Beast format data sources (typically dump1090 instances) to -aggregate aircraft data from multiple receivers. -.SH OPTIONS -.TP -.B \-config \fIstring\fR -Path to configuration file (default "config.json") -.TP -.B \-port \fIint\fR -HTTP server port (default 8080) -.TP -.B \-debug -Enable debug logging -.TP -.B \-version -Show version information and exit -.TP -.B \-h, \-help -Show help message and exit -.SH FILES -.TP -.I /etc/skyview/config.json -System-wide configuration file -.TP -.I ~/.config/skyview/config.json -Per-user configuration file -.SH EXAMPLES -.TP -Start with default configuration: -.B skyview -.TP -Start with custom config file: -.B skyview \-config /path/to/config.json -.TP -Start on port 9090 with debug logging: -.B skyview \-port 9090 \-debug -.SH CONFIGURATION -The configuration file uses JSON format with the following structure: -.PP -.nf -{ - "sources": [ - { - "id": "source1", - "name": "Local Receiver", - "host": "localhost", - "port": 30005, - "latitude": 51.4700, - "longitude": -0.4600 - } - ], - "web": { - "port": 8080, - "assets_path": "/usr/share/skyview/assets" - } -} -.fi -.SH WEB INTERFACE -The web interface provides: -.IP \(bu 2 -Interactive map view with aircraft markers -.IP \(bu 2 -Aircraft data table with filtering and sorting -.IP \(bu 2 -Real-time statistics and charts -.IP \(bu 2 -Coverage heatmaps and range circles -.IP \(bu 2 -3D radar visualization -.SH SEE ALSO -.BR beast-dump (1), -.BR dump1090 (1) -.SH BUGS -Report bugs at: https://github.com/skyview/skyview/issues -.SH AUTHOR -SkyView Team \ No newline at end of file diff --git a/docs/ADS-B Decoding Guide.pdf b/docs/ADS-B Decoding Guide.pdf deleted file mode 100644 index 87d319a..0000000 Binary files a/docs/ADS-B Decoding Guide.pdf and /dev/null differ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index a018249..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,290 +0,0 @@ -# SkyView Architecture Documentation - -## Overview - -SkyView is a high-performance, multi-source ADS-B aircraft tracking system built in Go with a modern JavaScript frontend. It connects to multiple dump1090 Beast format receivers, performs intelligent data fusion, and provides low-latency aircraft tracking through a responsive web interface. - -## System Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ dump1090 โ”‚ โ”‚ dump1090 โ”‚ โ”‚ dump1090 โ”‚ -โ”‚ Receiver 1 โ”‚ โ”‚ Receiver 2 โ”‚ โ”‚ Receiver N โ”‚ -โ”‚ Port 30005 โ”‚ โ”‚ Port 30005 โ”‚ โ”‚ Port 30005 โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ โ”‚ โ”‚ - โ”‚ Beast Binary โ”‚ Beast Binary โ”‚ Beast Binary - โ”‚ TCP Stream โ”‚ TCP Stream โ”‚ TCP Stream - โ”‚ โ”‚ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ SkyView Server โ”‚ - โ”‚ โ”‚ - โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ โ”‚ Beast Client โ”‚ โ”‚ โ”€โ”€ Multi-source TCP clients - โ”‚ โ”‚ Manager โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ โ”‚ Mode S/ADS-B โ”‚ โ”‚ โ”€โ”€ Message parsing & decoding - โ”‚ โ”‚ Decoder โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ โ”‚ Data Merger โ”‚ โ”‚ โ”€โ”€ Intelligent data fusion - โ”‚ โ”‚ & ICAO DB โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ - โ”‚ โ”‚ HTTP/WebSocket โ”‚ โ”‚ โ”€โ”€ Low-latency web interface - โ”‚ โ”‚ Server โ”‚ โ”‚ - โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” - โ”‚ Web Interface โ”‚ - โ”‚ โ”‚ - โ”‚ โ€ข Interactive Maps โ”‚ - โ”‚ โ€ข Low-latency Updatesโ”‚ - โ”‚ โ€ข Aircraft Details โ”‚ - โ”‚ โ€ข Coverage Analysisโ”‚ - โ”‚ โ€ข 3D Visualization โ”‚ - โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## Core Components - -### 1. Beast Format Clients (`internal/client/`) - -**Purpose**: Manages TCP connections to dump1090 receivers - -**Key Features**: -- Concurrent connection handling for multiple sources -- Automatic reconnection with exponential backoff -- Beast binary format parsing -- Per-source connection monitoring and statistics - -**Files**: -- `beast.go`: Main client implementation - -### 2. Mode S/ADS-B Decoder (`internal/modes/`) - -**Purpose**: Decodes raw Mode S and ADS-B messages into structured aircraft data - -**Key Features**: -- CPR (Compact Position Reporting) decoding with zone ambiguity resolution -- ADS-B message type parsing (position, velocity, identification) -- Aircraft category and type classification -- Signal quality assessment - -**Files**: -- `decoder.go`: Core decoding logic - -### 3. Data Merger (`internal/merger/`) - -**Purpose**: Fuses aircraft data from multiple sources using intelligent conflict resolution - -**Key Features**: -- Signal strength-based source selection -- High-performance data fusion and conflict resolution -- Aircraft state management and lifecycle tracking -- Historical data collection (position, altitude, speed, signal trails) -- Automatic stale aircraft cleanup - -**Files**: -- `merger.go`: Multi-source data fusion engine - -### 4. ICAO Country Database (`internal/icao/`) - -**Purpose**: Provides comprehensive ICAO address to country mapping - -**Key Features**: -- Embedded SQLite database with 70+ allocations covering 40+ countries -- Based on official ICAO Document 8585 -- Fast range-based lookups using database indexing -- Country names, ISO codes, and flag emojis - -**Files**: -- `database.go`: SQLite database interface -- `icao.db`: Embedded SQLite database with ICAO allocations - -### 5. HTTP/WebSocket Server (`internal/server/`) - -**Purpose**: Serves web interface and provides low-latency data streaming - -**Key Features**: -- RESTful API for aircraft and system data -- WebSocket connections for low-latency updates -- Static asset serving with embedded resources -- Coverage analysis and signal heatmaps - -**Files**: -- `server.go`: HTTP server and WebSocket handler - -### 6. Web Frontend (`assets/static/`) - -**Purpose**: Interactive web interface for aircraft tracking and visualization - -**Key Technologies**: -- **Leaflet.js**: Interactive maps and aircraft markers -- **Three.js**: 3D radar visualization -- **Chart.js**: Live statistics and charts -- **WebSockets**: Live data streaming -- **Responsive CSS**: Mobile-optimized interface - -**Files**: -- `index.html`: Main web interface -- `js/app.js`: Main application orchestrator -- `js/modules/`: Modular JavaScript components - - `aircraft-manager.js`: Aircraft marker and trail management - - `map-manager.js`: Map controls and overlays - - `ui-manager.js`: User interface state management - - `websocket.js`: Low-latency data connections -- `css/style.css`: Responsive styling and themes -- `icons/`: SVG aircraft type icons - -## Data Flow - -### 1. Data Ingestion -1. **Beast Clients** connect to dump1090 receivers via TCP -2. **Beast Parser** processes binary message stream -3. **Mode S Decoder** converts raw messages to structured aircraft data -4. **Data Merger** receives aircraft updates with source attribution - -### 2. Data Fusion -1. **Signal Analysis**: Compare signal strength across sources -2. **Conflict Resolution**: Select best data based on signal quality and recency -3. **State Management**: Update aircraft position, velocity, and metadata -4. **History Tracking**: Maintain trails for visualization - -### 3. Country Lookup -1. **ICAO Extraction**: Extract 24-bit ICAO address from aircraft data -2. **Database Query**: Lookup country information in embedded SQLite database -3. **Data Enrichment**: Add country, country code, and flag to aircraft state - -### 4. Data Distribution -1. **REST API**: Provide aircraft data via HTTP endpoints -2. **WebSocket Streaming**: Push low-latency updates to connected clients -3. **Frontend Processing**: Update maps, tables, and visualizations -4. **User Interface**: Display aircraft with country flags and details - -## Configuration System - -### Configuration Sources (Priority Order) -1. Command-line flags (highest priority) -2. Configuration file (JSON) -3. Default values (lowest priority) - -### Configuration Structure -```json -{ - "server": { - "host": "", // Bind address (empty = all interfaces) - "port": 8080 // HTTP server port - }, - "sources": [ - { - "id": "unique-id", // Source identifier - "name": "Display Name", // Human-readable name - "host": "hostname", // Receiver hostname/IP - "port": 30005, // Beast format port - "latitude": 51.4700, // Receiver location - "longitude": -0.4600, - "altitude": 50.0, // Meters above sea level - "enabled": true // Source enable/disable - } - ], - "settings": { - "history_limit": 500, // Max trail points per aircraft - "stale_timeout": 60, // Seconds before aircraft removed - "update_rate": 1 // WebSocket update frequency - }, - "origin": { - "latitude": 51.4700, // Map center point - "longitude": -0.4600, - "name": "Origin Name" - } -} -``` - -## Performance Characteristics - -### Concurrency Model -- **Goroutine per Source**: Each Beast client runs in separate goroutine -- **Mutex-Protected Merger**: Thread-safe aircraft state management -- **WebSocket Broadcasting**: Concurrent client update distribution -- **Non-blocking I/O**: Asynchronous network operations - -### Memory Management -- **Bounded History**: Configurable limits on historical data storage -- **Automatic Cleanup**: Stale aircraft removal to prevent memory leaks -- **Efficient Data Structures**: Maps for O(1) aircraft lookups -- **Embedded Assets**: Static files bundled in binary - -### Scalability -- **Multi-source Support**: Tested with 10+ concurrent receivers -- **High Message Throughput**: Handles 1000+ messages/second per source -- **Low-latency Updates**: Sub-second latency for aircraft updates -- **Responsive Web UI**: Optimized for 100+ concurrent aircraft - -## Security Considerations - -### Network Security -- **No Authentication Required**: Designed for trusted network environments -- **Local Network Operation**: Intended for private receiver networks -- **WebSocket Origin Checking**: Basic CORS protection - -### System Security -- **Unprivileged Execution**: Runs as non-root user in production -- **Filesystem Isolation**: Minimal file system access required -- **Network Isolation**: Only requires outbound TCP to receivers -- **Systemd Hardening**: Security features enabled in service file - -### Data Privacy -- **Public ADS-B Data**: Only processes publicly broadcast aircraft data -- **No Personal Information**: Aircraft tracking only, no passenger data -- **Local Processing**: No data transmitted to external services -- **Historical Limits**: Configurable data retention periods - -## External Resources - -### Official Standards -- **ICAO Document 8585**: Designators for Aircraft Operating Agencies -- **RTCA DO-260B**: ADS-B Message Formats and Protocols -- **ITU-R M.1371-5**: Technical characteristics for universal ADS-B - -### Technology Dependencies -- **Go Language**: https://golang.org/ -- **Leaflet.js**: https://leafletjs.com/ - Interactive maps -- **Three.js**: https://threejs.org/ - 3D visualization -- **Chart.js**: https://www.chartjs.org/ - Statistics charts -- **SQLite**: https://www.sqlite.org/ - ICAO country database -- **WebSocket Protocol**: RFC 6455 - -### ADS-B Ecosystem -- **dump1090**: https://github.com/antirez/dump1090 - SDR ADS-B decoder -- **Beast Binary Format**: Mode S data interchange format -- **FlightAware**: ADS-B network and data provider -- **OpenSky Network**: Research-oriented ADS-B network - -## Development Guidelines - -### Code Organization -- **Package per Component**: Clear separation of concerns -- **Interface Abstractions**: Testable and mockable components -- **Error Handling**: Comprehensive error reporting and recovery -- **Documentation**: Extensive code comments and examples - -### Testing Strategy -- **Unit Tests**: Component-level testing with mocks -- **Integration Tests**: End-to-end data flow validation -- **Performance Tests**: Load testing with simulated data -- **Manual Testing**: Real-world receiver validation - -### Deployment Options -- **Standalone Binary**: Single executable with embedded assets -- **Debian Package**: Systemd service with configuration -- **Docker Container**: Containerized deployment option -- **Development Mode**: Hot-reload for frontend development - ---- - -**SkyView Architecture** - Designed for reliability, performance, and extensibility in multi-source ADS-B tracking applications. \ No newline at end of file diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index 336d043..0000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1,39 +0,0 @@ -# SkyView Project Guidelines - -## Documentation Requirements -- We should always have an up to date document describing our architecture and features -- Include links to any external resources we've used -- We should also always have an up to date README describing the project -- Shell scripts should be validated with shellcheck -- Always make sure the code is well documented with explanations for why and how a particular solution is selected - -## Development Principles -- An overarching principle with all code is KISS, Keep It Simple Stupid -- We do not want to create code that is more complicated than necessary -- When changing code, always make sure to update any relevant tests -- Use proper error handling - aviation applications need reliability - -## SkyView-Specific Guidelines - -### Architecture & Design -- Multi-source ADS-B data fusion is the core feature - prioritize signal strength-based conflict resolution -- Embedded resources (SQLite ICAO database, static assets) over external dependencies -- Low-latency performance is critical - optimize for fast WebSocket updates -- Support concurrent aircraft tracking (100+ aircraft should work smoothly) - -### Code Organization -- Keep Go packages focused: beast parsing, modes decoding, merger, server, clients -- Frontend should be modular: separate managers for aircraft, map, UI, websockets -- Database operations should be fast (use indexes, avoid N+1 queries) - -### Performance Considerations -- Beast binary parsing must handle high message rates (1000+ msg/sec per source) -- WebSocket broadcasting should not block on slow clients -- Memory usage should be bounded (configurable history limits) -- CPU usage should remain low during normal operation - -### Documentation Maintenance -- Always update docs/ARCHITECTURE.md when changing system design -- README.md should stay current with features and usage -- External resources (ICAO docs, ADS-B standards) should be linked in documentation -- Country database updates should be straightforward (replace SQLite file) \ No newline at end of file diff --git a/internal/beast/parser.go b/internal/beast/parser.go index 436a728..178566e 100644 --- a/internal/beast/parser.go +++ b/internal/beast/parser.go @@ -130,13 +130,8 @@ func (p *Parser) ReadMessage() (*Message, error) { case BeastStatusMsg: // Status messages have variable length, skip for now return p.ReadMessage() - case BeastEscape: - // Handle double escape sequence (0x1A 0x1A) - skip and continue - return p.ReadMessage() default: - // Skip unknown message types and continue parsing instead of failing - // This makes the parser more resilient to malformed or extended Beast formats - return p.ReadMessage() + return nil, fmt.Errorf("unknown message type: 0x%02x", msgType) } // Read timestamp (6 bytes, 48-bit) diff --git a/internal/client/beast.go b/internal/client/beast.go index cb30a74..b5830bf 100644 --- a/internal/client/beast.go +++ b/internal/client/beast.go @@ -72,8 +72,8 @@ func NewBeastClient(source *merger.Source, merger *merger.Merger) *BeastClient { return &BeastClient{ source: source, merger: merger, - decoder: modes.NewDecoder(source.Latitude, source.Longitude), - msgChan: make(chan *beast.Message, 5000), + decoder: modes.NewDecoder(), + msgChan: make(chan *beast.Message, 1000), errChan: make(chan error, 10), stopChan: make(chan struct{}), reconnectDelay: 5 * time.Second, @@ -146,7 +146,7 @@ func (c *BeastClient) run(ctx context.Context) { addr := fmt.Sprintf("%s:%d", c.source.Host, c.source.Port) fmt.Printf("Connecting to Beast stream at %s (%s)...\n", addr, c.source.Name) - conn, err := net.DialTimeout("tcp", addr, 30*time.Second) + conn, err := net.DialTimeout("tcp", addr, 10*time.Second) if err != nil { fmt.Printf("Failed to connect to %s: %v\n", c.source.Name, err) c.source.Active = false diff --git a/internal/icao/database.go b/internal/icao/database.go deleted file mode 100644 index ab26af3..0000000 --- a/internal/icao/database.go +++ /dev/null @@ -1,268 +0,0 @@ -package icao - -import ( - "sort" - "strconv" -) - -// Database handles ICAO address to country lookups -type Database struct { - allocations []ICAOAllocation -} - -// ICAOAllocation represents an ICAO address range allocation -type ICAOAllocation struct { - StartAddr int64 - EndAddr int64 - Country string - CountryCode string - Flag string - Description string -} - -// CountryInfo represents country information for an aircraft -type CountryInfo struct { - Country string `json:"country"` - CountryCode string `json:"country_code"` - Flag string `json:"flag"` -} - -// NewDatabase creates a new ICAO database with comprehensive allocation data -func NewDatabase() (*Database, error) { - allocations := getICAOAllocations() - - // Sort allocations by start address for efficient binary search - sort.Slice(allocations, func(i, j int) bool { - return allocations[i].StartAddr < allocations[j].StartAddr - }) - - return &Database{allocations: allocations}, nil -} - -// getICAOAllocations returns comprehensive ICAO allocation data based on official aerotransport.org table -func getICAOAllocations() []ICAOAllocation { - // ICAO allocations based on official ICAO 24-bit address allocation table - // Source: https://www.aerotransport.org/ (unofficial but comprehensive reference) - // Complete coverage of all allocated ICAO 24-bit addresses - return []ICAOAllocation{ - // Africa - {0x004000, 0x0043FF, "Zimbabwe", "ZW", "๐Ÿ‡ฟ๐Ÿ‡ผ", "Republic of Zimbabwe"}, - {0x006000, 0x006FFF, "Mozambique", "MZ", "๐Ÿ‡ฒ๐Ÿ‡ฟ", "Republic of Mozambique"}, - {0x008000, 0x00FFFF, "South Africa", "ZA", "๐Ÿ‡ฟ๐Ÿ‡ฆ", "Republic of South Africa"}, - {0x010000, 0x017FFF, "Egypt", "EG", "๐Ÿ‡ช๐Ÿ‡ฌ", "Arab Republic of Egypt"}, - {0x018000, 0x01FFFF, "Libya", "LY", "๐Ÿ‡ฑ๐Ÿ‡พ", "State of Libya"}, - {0x020000, 0x027FFF, "Morocco", "MA", "๐Ÿ‡ฒ๐Ÿ‡ฆ", "Kingdom of Morocco"}, - {0x028000, 0x02FFFF, "Tunisia", "TN", "๐Ÿ‡น๐Ÿ‡ณ", "Republic of Tunisia"}, - {0x030000, 0x0303FF, "Botswana", "BW", "๐Ÿ‡ง๐Ÿ‡ผ", "Republic of Botswana"}, - {0x032000, 0x032FFF, "Burundi", "BI", "๐Ÿ‡ง๐Ÿ‡ฎ", "Republic of Burundi"}, - {0x034000, 0x034FFF, "Cameroon", "CM", "๐Ÿ‡จ๐Ÿ‡ฒ", "Republic of Cameroon"}, - {0x035000, 0x0353FF, "Comoros", "KM", "๐Ÿ‡ฐ๐Ÿ‡ฒ", "Union of the Comoros"}, - {0x036000, 0x036FFF, "Congo", "CG", "๐Ÿ‡จ๐Ÿ‡ฌ", "Republic of the Congo"}, - {0x038000, 0x038FFF, "Cรดte d'Ivoire", "CI", "๐Ÿ‡จ๐Ÿ‡ฎ", "Republic of Cรดte d'Ivoire"}, - {0x03E000, 0x03EFFF, "Gabon", "GA", "๐Ÿ‡ฌ๐Ÿ‡ฆ", "Gabonese Republic"}, - {0x040000, 0x040FFF, "Ethiopia", "ET", "๐Ÿ‡ช๐Ÿ‡น", "Federal Democratic Republic of Ethiopia"}, - {0x042000, 0x042FFF, "Equatorial Guinea", "GQ", "๐Ÿ‡ฌ๐Ÿ‡ถ", "Republic of Equatorial Guinea"}, - {0x044000, 0x044FFF, "Ghana", "GH", "๐Ÿ‡ฌ๐Ÿ‡ญ", "Republic of Ghana"}, - {0x046000, 0x046FFF, "Guinea", "GN", "๐Ÿ‡ฌ๐Ÿ‡ณ", "Republic of Guinea"}, - {0x048000, 0x0483FF, "Guinea-Bissau", "GW", "๐Ÿ‡ฌ๐Ÿ‡ผ", "Republic of Guinea-Bissau"}, - {0x04A000, 0x04A3FF, "Lesotho", "LS", "๐Ÿ‡ฑ๐Ÿ‡ธ", "Kingdom of Lesotho"}, - {0x04C000, 0x04CFFF, "Kenya", "KE", "๐Ÿ‡ฐ๐Ÿ‡ช", "Republic of Kenya"}, - {0x050000, 0x050FFF, "Liberia", "LR", "๐Ÿ‡ฑ๐Ÿ‡ท", "Republic of Liberia"}, - {0x054000, 0x054FFF, "Madagascar", "MG", "๐Ÿ‡ฒ๐Ÿ‡ฌ", "Republic of Madagascar"}, - {0x058000, 0x058FFF, "Malawi", "MW", "๐Ÿ‡ฒ๐Ÿ‡ผ", "Republic of Malawi"}, - {0x05C000, 0x05CFFF, "Mali", "ML", "๐Ÿ‡ฒ๐Ÿ‡ฑ", "Republic of Mali"}, - {0x05E000, 0x05E3FF, "Mauritania", "MR", "๐Ÿ‡ฒ๐Ÿ‡ท", "Islamic Republic of Mauritania"}, - {0x060000, 0x0603FF, "Mauritius", "MU", "๐Ÿ‡ฒ๐Ÿ‡บ", "Republic of Mauritius"}, - {0x062000, 0x062FFF, "Niger", "NE", "๐Ÿ‡ณ๐Ÿ‡ช", "Republic of Niger"}, - {0x064000, 0x064FFF, "Nigeria", "NG", "๐Ÿ‡ณ๐Ÿ‡ฌ", "Federal Republic of Nigeria"}, - {0x068000, 0x068FFF, "Uganda", "UG", "๐Ÿ‡บ๐Ÿ‡ฌ", "Republic of Uganda"}, - {0x06C000, 0x06CFFF, "Central African Republic", "CF", "๐Ÿ‡จ๐Ÿ‡ซ", "Central African Republic"}, - {0x06E000, 0x06EFFF, "Rwanda", "RW", "๐Ÿ‡ท๐Ÿ‡ผ", "Republic of Rwanda"}, - {0x070000, 0x070FFF, "Senegal", "SN", "๐Ÿ‡ธ๐Ÿ‡ณ", "Republic of Senegal"}, - {0x074000, 0x0743FF, "Seychelles", "SC", "๐Ÿ‡ธ๐Ÿ‡จ", "Republic of Seychelles"}, - {0x076000, 0x0763FF, "Sierra Leone", "SL", "๐Ÿ‡ธ๐Ÿ‡ฑ", "Republic of Sierra Leone"}, - {0x078000, 0x078FFF, "Somalia", "SO", "๐Ÿ‡ธ๐Ÿ‡ด", "Federal Republic of Somalia"}, - {0x07A000, 0x07A3FF, "Swaziland", "SZ", "๐Ÿ‡ธ๐Ÿ‡ฟ", "Kingdom of Swaziland"}, - {0x07C000, 0x07CFFF, "Sudan", "SD", "๐Ÿ‡ธ๐Ÿ‡ฉ", "Republic of Sudan"}, - {0x080000, 0x080FFF, "Tanzania", "TZ", "๐Ÿ‡น๐Ÿ‡ฟ", "United Republic of Tanzania"}, - {0x084000, 0x084FFF, "Chad", "TD", "๐Ÿ‡น๐Ÿ‡ฉ", "Republic of Chad"}, - {0x088000, 0x088FFF, "Togo", "TG", "๐Ÿ‡น๐Ÿ‡ฌ", "Togolese Republic"}, - {0x08A000, 0x08AFFF, "Zambia", "ZM", "๐Ÿ‡ฟ๐Ÿ‡ฒ", "Republic of Zambia"}, - {0x08C000, 0x08CFFF, "D R Congo", "CD", "๐Ÿ‡จ๐Ÿ‡ฉ", "Democratic Republic of the Congo"}, - {0x090000, 0x090FFF, "Angola", "AO", "๐Ÿ‡ฆ๐Ÿ‡ด", "Republic of Angola"}, - {0x094000, 0x0943FF, "Benin", "BJ", "๐Ÿ‡ง๐Ÿ‡ฏ", "Republic of Benin"}, - {0x096000, 0x0963FF, "Cape Verde", "CV", "๐Ÿ‡จ๐Ÿ‡ป", "Republic of Cape Verde"}, - {0x098000, 0x0983FF, "Djibouti", "DJ", "๐Ÿ‡ฉ๐Ÿ‡ฏ", "Republic of Djibouti"}, - {0x0A8000, 0x0A8FFF, "Bahamas", "BS", "๐Ÿ‡ง๐Ÿ‡ธ", "Commonwealth of the Bahamas"}, - {0x0AA000, 0x0AA3FF, "Barbados", "BB", "๐Ÿ‡ง๐Ÿ‡ง", "Barbados"}, - {0x0AB000, 0x0AB3FF, "Belize", "BZ", "๐Ÿ‡ง๐Ÿ‡ฟ", "Belize"}, - {0x0B0000, 0x0B0FFF, "Cuba", "CU", "๐Ÿ‡จ๐Ÿ‡บ", "Republic of Cuba"}, - {0x0B2000, 0x0B2FFF, "El Salvador", "SV", "๐Ÿ‡ธ๐Ÿ‡ป", "Republic of El Salvador"}, - {0x0B8000, 0x0B8FFF, "Haiti", "HT", "๐Ÿ‡ญ๐Ÿ‡น", "Republic of Haiti"}, - {0x0BA000, 0x0BAFFF, "Honduras", "HN", "๐Ÿ‡ญ๐Ÿ‡ณ", "Republic of Honduras"}, - {0x0BC000, 0x0BC3FF, "St. Vincent + Grenadines", "VC", "๐Ÿ‡ป๐Ÿ‡จ", "Saint Vincent and the Grenadines"}, - {0x0BE000, 0x0BEFFF, "Jamaica", "JM", "๐Ÿ‡ฏ๐Ÿ‡ฒ", "Jamaica"}, - {0x0D0000, 0x0D7FFF, "Mexico", "MX", "๐Ÿ‡ฒ๐Ÿ‡ฝ", "United Mexican States"}, - - // Eastern Europe & Russia - {0x100000, 0x1FFFFF, "Russia", "RU", "๐Ÿ‡ท๐Ÿ‡บ", "Russian Federation"}, - {0x201000, 0x2013FF, "Namibia", "NA", "๐Ÿ‡ณ๐Ÿ‡ฆ", "Republic of Namibia"}, - {0x202000, 0x2023FF, "Eritrea", "ER", "๐Ÿ‡ช๐Ÿ‡ท", "State of Eritrea"}, - - // Europe - {0x300000, 0x33FFFF, "Italy", "IT", "๐Ÿ‡ฎ๐Ÿ‡น", "Italian Republic"}, - {0x340000, 0x37FFFF, "Spain", "ES", "๐Ÿ‡ช๐Ÿ‡ธ", "Kingdom of Spain"}, - {0x380000, 0x3BFFFF, "France", "FR", "๐Ÿ‡ซ๐Ÿ‡ท", "French Republic"}, - {0x3C0000, 0x3FFFFF, "Germany", "DE", "๐Ÿ‡ฉ๐Ÿ‡ช", "Federal Republic of Germany"}, - {0x400000, 0x43FFFF, "United Kingdom", "GB", "๐Ÿ‡ฌ๐Ÿ‡ง", "United Kingdom"}, - {0x440000, 0x447FFF, "Austria", "AT", "๐Ÿ‡ฆ๐Ÿ‡น", "Republic of Austria"}, - {0x448000, 0x44FFFF, "Belgium", "BE", "๐Ÿ‡ง๐Ÿ‡ช", "Kingdom of Belgium"}, - {0x450000, 0x457FFF, "Bulgaria", "BG", "๐Ÿ‡ง๐Ÿ‡ฌ", "Republic of Bulgaria"}, - {0x458000, 0x45FFFF, "Denmark", "DK", "๐Ÿ‡ฉ๐Ÿ‡ฐ", "Kingdom of Denmark"}, - {0x460000, 0x467FFF, "Finland", "FI", "๐Ÿ‡ซ๐Ÿ‡ฎ", "Republic of Finland"}, - {0x468000, 0x46FFFF, "Greece", "GR", "๐Ÿ‡ฌ๐Ÿ‡ท", "Hellenic Republic"}, - {0x470000, 0x477FFF, "Hungary", "HU", "๐Ÿ‡ญ๐Ÿ‡บ", "Republic of Hungary"}, - {0x478000, 0x47FFFF, "Norway", "NO", "๐Ÿ‡ณ๐Ÿ‡ด", "Kingdom of Norway"}, - {0x480000, 0x487FFF, "Netherlands", "NL", "๐Ÿ‡ณ๐Ÿ‡ฑ", "Kingdom of the Netherlands"}, - {0x488000, 0x48FFFF, "Poland", "PL", "๐Ÿ‡ต๐Ÿ‡ฑ", "Republic of Poland"}, - {0x490000, 0x497FFF, "Portugal", "PT", "๐Ÿ‡ต๐Ÿ‡น", "Portuguese Republic"}, - {0x498000, 0x49FFFF, "Czech Republic", "CZ", "๐Ÿ‡จ๐Ÿ‡ฟ", "Czech Republic"}, - {0x4A0000, 0x4A7FFF, "Romania", "RO", "๐Ÿ‡ท๐Ÿ‡ด", "Romania"}, - {0x4A8000, 0x4AFFFF, "Sweden", "SE", "๐Ÿ‡ธ๐Ÿ‡ช", "Kingdom of Sweden"}, - {0x4B0000, 0x4B7FFF, "Switzerland", "CH", "๐Ÿ‡จ๐Ÿ‡ญ", "Swiss Confederation"}, - {0x4B8000, 0x4BFFFF, "Turkey", "TR", "๐Ÿ‡น๐Ÿ‡ท", "Republic of Turkey"}, - {0x4C0000, 0x4C7FFF, "Yugoslavia", "YU", "๐Ÿ‡ท๐Ÿ‡ธ", "Yugoslavia"}, - {0x4C8000, 0x4C83FF, "Cyprus", "CY", "๐Ÿ‡จ๐Ÿ‡พ", "Republic of Cyprus"}, - {0x4CA000, 0x4CAFFF, "Ireland", "IE", "๐Ÿ‡ฎ๐Ÿ‡ช", "Republic of Ireland"}, - {0x4CC000, 0x4CCFFF, "Iceland", "IS", "๐Ÿ‡ฎ๐Ÿ‡ธ", "Republic of Iceland"}, - {0x4D0000, 0x4D03FF, "Luxembourg", "LU", "๐Ÿ‡ฑ๐Ÿ‡บ", "Grand Duchy of Luxembourg"}, - {0x4D2000, 0x4D23FF, "Malta", "MT", "๐Ÿ‡ฒ๐Ÿ‡น", "Republic of Malta"}, - {0x4D4000, 0x4D43FF, "Monaco", "MC", "๐Ÿ‡ฒ๐Ÿ‡จ", "Principality of Monaco"}, - {0x500000, 0x5004FF, "San Marino", "SM", "๐Ÿ‡ธ๐Ÿ‡ฒ", "Republic of San Marino"}, - {0x501000, 0x5013FF, "Albania", "AL", "๐Ÿ‡ฆ๐Ÿ‡ฑ", "Republic of Albania"}, - {0x501C00, 0x501FFF, "Croatia", "HR", "๐Ÿ‡ญ๐Ÿ‡ท", "Republic of Croatia"}, - {0x502C00, 0x502FFF, "Latvia", "LV", "๐Ÿ‡ฑ๐Ÿ‡ป", "Republic of Latvia"}, - {0x503C00, 0x503FFF, "Lithuania", "LT", "๐Ÿ‡ฑ๐Ÿ‡น", "Republic of Lithuania"}, - {0x504C00, 0x504FFF, "Moldova", "MD", "๐Ÿ‡ฒ๐Ÿ‡ฉ", "Republic of Moldova"}, - {0x505C00, 0x505FFF, "Slovakia", "SK", "๐Ÿ‡ธ๐Ÿ‡ฐ", "Slovak Republic"}, - {0x506C00, 0x506FFF, "Slovenia", "SI", "๐Ÿ‡ธ๐Ÿ‡ฎ", "Republic of Slovenia"}, - {0x508000, 0x50FFFF, "Ukraine", "UA", "๐Ÿ‡บ๐Ÿ‡ฆ", "Ukraine"}, - {0x510000, 0x5103FF, "Belarus", "BY", "๐Ÿ‡ง๐Ÿ‡พ", "Republic of Belarus"}, - {0x511000, 0x5113FF, "Estonia", "EE", "๐Ÿ‡ช๐Ÿ‡ช", "Republic of Estonia"}, - {0x512000, 0x5123FF, "Macedonia", "MK", "๐Ÿ‡ฒ๐Ÿ‡ฐ", "North Macedonia"}, - {0x513000, 0x5133FF, "Bosnia & Herzegovina", "BA", "๐Ÿ‡ง๐Ÿ‡ฆ", "Bosnia and Herzegovina"}, - {0x514000, 0x5143FF, "Georgia", "GE", "๐Ÿ‡ฌ๐Ÿ‡ช", "Georgia"}, - - // Middle East & Central Asia - {0x600000, 0x6003FF, "Armenia", "AM", "๐Ÿ‡ฆ๐Ÿ‡ฒ", "Republic of Armenia"}, - {0x600800, 0x600BFF, "Azerbaijan", "AZ", "๐Ÿ‡ฆ๐Ÿ‡ฟ", "Republic of Azerbaijan"}, - {0x680000, 0x6803FF, "Bhutan", "BT", "๐Ÿ‡ง๐Ÿ‡น", "Kingdom of Bhutan"}, - {0x681000, 0x6813FF, "Micronesia", "FM", "๐Ÿ‡ซ๐Ÿ‡ฒ", "Federated States of Micronesia"}, - {0x682000, 0x6823FF, "Mongolia", "MN", "๐Ÿ‡ฒ๐Ÿ‡ณ", "Mongolia"}, - {0x683000, 0x6833FF, "Kazakhstan", "KZ", "๐Ÿ‡ฐ๐Ÿ‡ฟ", "Republic of Kazakhstan"}, - {0x06A000, 0x06A3FF, "Qatar", "QA", "๐Ÿ‡ถ๐Ÿ‡ฆ", "State of Qatar"}, - {0x700000, 0x700FFF, "Afghanistan", "AF", "๐Ÿ‡ฆ๐Ÿ‡ซ", "Islamic Republic of Afghanistan"}, - {0x702000, 0x702FFF, "Bangladesh", "BD", "๐Ÿ‡ง๐Ÿ‡ฉ", "People's Republic of Bangladesh"}, - {0x704000, 0x704FFF, "Myanmar", "MM", "๐Ÿ‡ฒ๐Ÿ‡ฒ", "Republic of the Union of Myanmar"}, - {0x706000, 0x706FFF, "Kuwait", "KW", "๐Ÿ‡ฐ๐Ÿ‡ผ", "State of Kuwait"}, - {0x708000, 0x708FFF, "Laos", "LA", "๐Ÿ‡ฑ๐Ÿ‡ฆ", "Lao People's Democratic Republic"}, - {0x70A000, 0x70AFFF, "Nepal", "NP", "๐Ÿ‡ณ๐Ÿ‡ต", "Federal Democratic Republic of Nepal"}, - {0x70C000, 0x70C3FF, "Oman", "OM", "๐Ÿ‡ด๐Ÿ‡ฒ", "Sultanate of Oman"}, - {0x70E000, 0x70EFFF, "Cambodia", "KH", "๐Ÿ‡ฐ๐Ÿ‡ญ", "Kingdom of Cambodia"}, - {0x710000, 0x717FFF, "Saudi Arabia", "SA", "๐Ÿ‡ธ๐Ÿ‡ฆ", "Kingdom of Saudi Arabia"}, - {0x718000, 0x71FFFF, "South Korea", "KR", "๐Ÿ‡ฐ๐Ÿ‡ท", "Republic of Korea"}, - {0x720000, 0x727FFF, "North Korea", "KP", "๐Ÿ‡ฐ๐Ÿ‡ต", "Democratic People's Republic of Korea"}, - {0x728000, 0x72FFFF, "Iraq", "IQ", "๐Ÿ‡ฎ๐Ÿ‡ถ", "Republic of Iraq"}, - {0x730000, 0x737FFF, "Iran", "IR", "๐Ÿ‡ฎ๐Ÿ‡ท", "Islamic Republic of Iran"}, - {0x738000, 0x73FFFF, "Israel", "IL", "๐Ÿ‡ฎ๐Ÿ‡ฑ", "State of Israel"}, - {0x740000, 0x747FFF, "Jordan", "JO", "๐Ÿ‡ฏ๐Ÿ‡ด", "Hashemite Kingdom of Jordan"}, - {0x750000, 0x757FFF, "Malaysia", "MY", "๐Ÿ‡ฒ๐Ÿ‡พ", "Malaysia"}, - {0x758000, 0x75FFFF, "Philippines", "PH", "๐Ÿ‡ต๐Ÿ‡ญ", "Republic of the Philippines"}, - {0x760000, 0x767FFF, "Pakistan", "PK", "๐Ÿ‡ต๐Ÿ‡ฐ", "Islamic Republic of Pakistan"}, - {0x768000, 0x76FFFF, "Singapore", "SG", "๐Ÿ‡ธ๐Ÿ‡ฌ", "Republic of Singapore"}, - {0x770000, 0x777FFF, "Sri Lanka", "LK", "๐Ÿ‡ฑ๐Ÿ‡ฐ", "Democratic Socialist Republic of Sri Lanka"}, - {0x778000, 0x77FFFF, "Syria", "SY", "๐Ÿ‡ธ๐Ÿ‡พ", "Syrian Arab Republic"}, - {0x780000, 0x7BFFFF, "China", "CN", "๐Ÿ‡จ๐Ÿ‡ณ", "People's Republic of China"}, - {0x7C0000, 0x7FFFFF, "Australia", "AU", "๐Ÿ‡ฆ๐Ÿ‡บ", "Commonwealth of Australia"}, - - // Asia-Pacific - {0x800000, 0x83FFFF, "India", "IN", "๐Ÿ‡ฎ๐Ÿ‡ณ", "Republic of India"}, - {0x840000, 0x87FFFF, "Japan", "JP", "๐Ÿ‡ฏ๐Ÿ‡ต", "Japan"}, - {0x880000, 0x887FFF, "Thailand", "TH", "๐Ÿ‡น๐Ÿ‡ญ", "Kingdom of Thailand"}, - {0x888000, 0x88FFFF, "Vietnam", "VN", "๐Ÿ‡ป๐Ÿ‡ณ", "Socialist Republic of Vietnam"}, - {0x890000, 0x890FFF, "Yemen", "YE", "๐Ÿ‡พ๐Ÿ‡ช", "Republic of Yemen"}, - {0x894000, 0x894FFF, "Bahrain", "BH", "๐Ÿ‡ง๐Ÿ‡ญ", "Kingdom of Bahrain"}, - {0x895000, 0x8953FF, "Brunei", "BN", "๐Ÿ‡ง๐Ÿ‡ณ", "Nation of Brunei"}, - {0x896000, 0x8973FF, "United Arab Emirates", "AE", "๐Ÿ‡ฆ๐Ÿ‡ช", "United Arab Emirates"}, - {0x897000, 0x8973FF, "Solomon Islands", "SB", "๐Ÿ‡ธ๐Ÿ‡ง", "Solomon Islands"}, - {0x898000, 0x898FFF, "Papua New Guinea", "PG", "๐Ÿ‡ต๐Ÿ‡ฌ", "Independent State of Papua New Guinea"}, - {0x899000, 0x8993FF, "Taiwan", "TW", "๐Ÿ‡น๐Ÿ‡ผ", "Republic of China (Taiwan)"}, - {0x8A0000, 0x8A7FFF, "Indonesia", "ID", "๐Ÿ‡ฎ๐Ÿ‡ฉ", "Republic of Indonesia"}, - - // North America - {0xA00000, 0xAFFFFF, "United States", "US", "๐Ÿ‡บ๐Ÿ‡ธ", "United States of America"}, - - // North America & Oceania - {0xC00000, 0xC3FFFF, "Canada", "CA", "๐Ÿ‡จ๐Ÿ‡ฆ", "Canada"}, - {0xC80000, 0xC87FFF, "New Zealand", "NZ", "๐Ÿ‡ณ๐Ÿ‡ฟ", "New Zealand"}, - {0xC88000, 0xC88FFF, "Fiji", "FJ", "๐Ÿ‡ซ๐Ÿ‡ฏ", "Republic of Fiji"}, - {0xC8A000, 0xC8A3FF, "Nauru", "NR", "๐Ÿ‡ณ๐Ÿ‡ท", "Republic of Nauru"}, - {0xC8C000, 0xC8C3FF, "Saint Lucia", "LC", "๐Ÿ‡ฑ๐Ÿ‡จ", "Saint Lucia"}, - {0xC8D000, 0xC8D3FF, "Tonga", "TO", "๐Ÿ‡น๐Ÿ‡ด", "Kingdom of Tonga"}, - {0xC8E000, 0xC8E3FF, "Kiribati", "KI", "๐Ÿ‡ฐ๐Ÿ‡ฎ", "Republic of Kiribati"}, - - // South America - {0xE00000, 0xE3FFFF, "Argentina", "AR", "๐Ÿ‡ฆ๐Ÿ‡ท", "Argentine Republic"}, - {0xE40000, 0xE7FFFF, "Brazil", "BR", "๐Ÿ‡ง๐Ÿ‡ท", "Federative Republic of Brazil"}, - {0xE80000, 0xE80FFF, "Chile", "CL", "๐Ÿ‡จ๐Ÿ‡ฑ", "Republic of Chile"}, - {0xE84000, 0xE84FFF, "Ecuador", "EC", "๐Ÿ‡ช๐Ÿ‡จ", "Republic of Ecuador"}, - {0xE88000, 0xE88FFF, "Paraguay", "PY", "๐Ÿ‡ต๐Ÿ‡พ", "Republic of Paraguay"}, - {0xE8C000, 0xE8CFFF, "Peru", "PE", "๐Ÿ‡ต๐Ÿ‡ช", "Republic of Peru"}, - {0xE90000, 0xE90FFF, "Uruguay", "UY", "๐Ÿ‡บ๐Ÿ‡พ", "Oriental Republic of Uruguay"}, - {0xE94000, 0xE94FFF, "Bolivia", "BO", "๐Ÿ‡ง๐Ÿ‡ด", "Plurinational State of Bolivia"}, - } -} - -// LookupCountry returns country information for an ICAO address using binary search -func (d *Database) LookupCountry(icaoHex string) (*CountryInfo, error) { - if len(icaoHex) != 6 { - return &CountryInfo{ - Country: "Unknown", - CountryCode: "XX", - Flag: "๐Ÿณ๏ธ", - }, nil - } - - // Convert hex string to integer - icaoInt, err := strconv.ParseInt(icaoHex, 16, 64) - if err != nil { - return &CountryInfo{ - Country: "Unknown", - CountryCode: "XX", - Flag: "๐Ÿณ๏ธ", - }, nil - } - - // Binary search for the ICAO address range - for _, alloc := range d.allocations { - if icaoInt >= alloc.StartAddr && icaoInt <= alloc.EndAddr { - return &CountryInfo{ - Country: alloc.Country, - CountryCode: alloc.CountryCode, - Flag: alloc.Flag, - }, nil - } - } - - // Not found in any allocation - return &CountryInfo{ - Country: "Unknown", - CountryCode: "XX", - Flag: "๐Ÿณ๏ธ", - }, nil -} - -// Close is a no-op since we don't have any resources to clean up -func (d *Database) Close() error { - return nil -} \ No newline at end of file diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 584d7a6..c673538 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -20,21 +20,13 @@ package merger import ( - "encoding/json" - "fmt" "math" "sync" "time" - "skyview/internal/icao" "skyview/internal/modes" ) -const ( - // MaxDistance represents an infinite distance for initialization - MaxDistance = float64(999999) -) - // Source represents a data source (dump1090 receiver or similar ADS-B source). // It contains both static configuration and dynamic status information used // for data fusion decisions and source monitoring. @@ -78,103 +70,6 @@ type AircraftState struct { MLATSources []string `json:"mlat_sources"` // Sources providing MLAT position data PositionSource string `json:"position_source"` // Source providing current position UpdateRate float64 `json:"update_rate"` // Recent updates per second - Country string `json:"country"` // Country of registration - CountryCode string `json:"country_code"` // ISO country code - Flag string `json:"flag"` // Country flag emoji -} - -// MarshalJSON provides custom JSON marshaling for AircraftState to format ICAO24 as hex. -func (a *AircraftState) MarshalJSON() ([]byte, error) { - // Create a struct that mirrors AircraftState but with ICAO24 as string - return json.Marshal(&struct { - // From embedded modes.Aircraft - ICAO24 string `json:"ICAO24"` - Callsign string `json:"Callsign"` - Latitude float64 `json:"Latitude"` - Longitude float64 `json:"Longitude"` - Altitude int `json:"Altitude"` - BaroAltitude int `json:"BaroAltitude"` - GeomAltitude int `json:"GeomAltitude"` - VerticalRate int `json:"VerticalRate"` - GroundSpeed int `json:"GroundSpeed"` - Track int `json:"Track"` - Heading int `json:"Heading"` - Category string `json:"Category"` - Squawk string `json:"Squawk"` - Emergency string `json:"Emergency"` - OnGround bool `json:"OnGround"` - Alert bool `json:"Alert"` - SPI bool `json:"SPI"` - NACp uint8 `json:"NACp"` - NACv uint8 `json:"NACv"` - SIL uint8 `json:"SIL"` - SelectedAltitude int `json:"SelectedAltitude"` - SelectedHeading float64 `json:"SelectedHeading"` - BaroSetting float64 `json:"BaroSetting"` - - // From AircraftState - Sources map[string]*SourceData `json:"sources"` - LastUpdate time.Time `json:"last_update"` - FirstSeen time.Time `json:"first_seen"` - TotalMessages int64 `json:"total_messages"` - PositionHistory []PositionPoint `json:"position_history"` - SignalHistory []SignalPoint `json:"signal_history"` - AltitudeHistory []AltitudePoint `json:"altitude_history"` - SpeedHistory []SpeedPoint `json:"speed_history"` - Distance float64 `json:"distance"` - Bearing float64 `json:"bearing"` - Age float64 `json:"age"` - MLATSources []string `json:"mlat_sources"` - PositionSource string `json:"position_source"` - UpdateRate float64 `json:"update_rate"` - Country string `json:"country"` - CountryCode string `json:"country_code"` - Flag string `json:"flag"` - }{ - // Copy all fields from Aircraft - ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24), - Callsign: a.Aircraft.Callsign, - Latitude: a.Aircraft.Latitude, - Longitude: a.Aircraft.Longitude, - Altitude: a.Aircraft.Altitude, - BaroAltitude: a.Aircraft.BaroAltitude, - GeomAltitude: a.Aircraft.GeomAltitude, - VerticalRate: a.Aircraft.VerticalRate, - GroundSpeed: a.Aircraft.GroundSpeed, - Track: a.Aircraft.Track, - Heading: a.Aircraft.Heading, - Category: a.Aircraft.Category, - Squawk: a.Aircraft.Squawk, - Emergency: a.Aircraft.Emergency, - OnGround: a.Aircraft.OnGround, - Alert: a.Aircraft.Alert, - SPI: a.Aircraft.SPI, - NACp: a.Aircraft.NACp, - NACv: a.Aircraft.NACv, - SIL: a.Aircraft.SIL, - SelectedAltitude: a.Aircraft.SelectedAltitude, - SelectedHeading: a.Aircraft.SelectedHeading, - BaroSetting: a.Aircraft.BaroSetting, - - // Copy all fields from AircraftState - Sources: a.Sources, - LastUpdate: a.LastUpdate, - FirstSeen: a.FirstSeen, - TotalMessages: a.TotalMessages, - PositionHistory: a.PositionHistory, - SignalHistory: a.SignalHistory, - AltitudeHistory: a.AltitudeHistory, - SpeedHistory: a.SpeedHistory, - Distance: a.Distance, - Bearing: a.Bearing, - Age: a.Age, - MLATSources: a.MLATSources, - PositionSource: a.PositionSource, - UpdateRate: a.UpdateRate, - Country: a.Country, - CountryCode: a.CountryCode, - Flag: a.Flag, - }) } // SourceData represents data quality and statistics for a specific source-aircraft pair. @@ -218,8 +113,8 @@ type AltitudePoint struct { // Used for aircraft performance analysis and track prediction. type SpeedPoint struct { Time time.Time `json:"time"` // Timestamp when speed was received - GroundSpeed int `json:"ground_speed"` // Ground speed in knots (integer) - Track int `json:"track"` // Track angle in degrees (0-359) + GroundSpeed float64 `json:"ground_speed"` // Ground speed in knots + Track float64 `json:"track"` // Track angle in degrees } // Merger handles merging aircraft data from multiple sources with intelligent conflict resolution. @@ -236,10 +131,9 @@ type SpeedPoint struct { type Merger struct { aircraft map[uint32]*AircraftState // ICAO24 -> merged aircraft state sources map[string]*Source // Source ID -> source information - icaoDB *icao.Database // ICAO country lookup database mu sync.RWMutex // Protects all maps and slices historyLimit int // Maximum history points to retain - staleTimeout time.Duration // Time before aircraft considered stale (15 seconds) + staleTimeout time.Duration // Time before aircraft considered stale updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data } @@ -253,25 +147,19 @@ type updateMetric struct { // // Default settings: // - History limit: 500 points per aircraft -// - Stale timeout: 15 seconds +// - Stale timeout: 60 seconds // - Empty aircraft and source maps // - Update metrics tracking enabled // // The merger is ready for immediate use after creation. -func NewMerger() (*Merger, error) { - icaoDB, err := icao.NewDatabase() - if err != nil { - return nil, fmt.Errorf("failed to initialize ICAO database: %w", err) - } - +func NewMerger() *Merger { return &Merger{ aircraft: make(map[uint32]*AircraftState), sources: make(map[string]*Source), - icaoDB: icaoDB, historyLimit: 500, - staleTimeout: 15 * time.Second, // Aircraft timeout - reasonable for ADS-B tracking + staleTimeout: 60 * time.Second, updateMetrics: make(map[uint32]*updateMetric), - }, nil + } } // AddSource registers a new data source with the merger. @@ -326,27 +214,11 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa AltitudeHistory: make([]AltitudePoint, 0), SpeedHistory: make([]SpeedPoint, 0), } - - // Lookup country information for new aircraft - icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) - if countryInfo, err := m.icaoDB.LookupCountry(icaoHex); err == nil { - state.Country = countryInfo.Country - state.CountryCode = countryInfo.CountryCode - state.Flag = countryInfo.Flag - } else { - // Fallback to unknown if lookup fails - state.Country = "Unknown" - state.CountryCode = "XX" - state.Flag = "๐Ÿณ๏ธ" - } - m.aircraft[aircraft.ICAO24] = state m.updateMetrics[aircraft.ICAO24] = &updateMetric{ updates: make([]time.Time, 0), } } - // Note: For existing aircraft, we don't overwrite state.Aircraft here - // The mergeAircraftData function will handle selective field updates // Update or create source data srcData, srcExists := state.Sources[sourceID] @@ -422,16 +294,12 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so updatePosition := false if state.Latitude == 0 { - // First position update updatePosition = true } else if srcData, ok := state.Sources[sourceID]; ok { // Use position from source with strongest signal currentBest := m.getBestSignalSource(state) if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel { updatePosition = true - } else if currentBest == sourceID { - // Same source as current best - allow updates for moving aircraft - updatePosition = true } } @@ -672,7 +540,7 @@ func (m *Merger) GetAircraft() map[uint32]*AircraftState { stateCopy.Age = now.Sub(state.LastUpdate).Seconds() // Find closest receiver distance - minDistance := MaxDistance + minDistance := float64(999999) for _, srcData := range state.Sources { if srcData.Distance > 0 && srcData.Distance < minDistance { minDistance = srcData.Distance @@ -747,7 +615,7 @@ func (m *Merger) GetStatistics() map[string]interface{} { // CleanupStale removes aircraft that haven't been updated recently. // // Aircraft are considered stale if they haven't received updates for longer -// than staleTimeout (default 15 seconds). This cleanup prevents memory +// than staleTimeout (default 60 seconds). This cleanup prevents memory // growth from aircraft that have left the coverage area or stopped transmitting. // // The cleanup also removes associated update metrics to free memory. @@ -808,14 +676,3 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64) return distance, bearing } - -// Close closes the merger and releases resources -func (m *Merger) Close() error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.icaoDB != nil { - return m.icaoDB.Close() - } - return nil -} diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go index 385b460..9d59cc1 100644 --- a/internal/modes/decoder.go +++ b/internal/modes/decoder.go @@ -30,45 +30,8 @@ package modes import ( "fmt" "math" - "sync" ) -// crcTable for Mode S CRC-24 validation -var crcTable [256]uint32 - -func init() { - // Initialize CRC table for Mode S CRC-24 (polynomial 0x1FFF409) - for i := 0; i < 256; i++ { - crc := uint32(i) << 16 - for j := 0; j < 8; j++ { - if crc&0x800000 != 0 { - crc = (crc << 1) ^ 0x1FFF409 - } else { - crc = crc << 1 - } - } - crcTable[i] = crc & 0xFFFFFF - } -} - -// validateModeSCRC validates the 24-bit CRC of a Mode S message -func validateModeSCRC(data []byte) bool { - if len(data) < 4 { - return false - } - - // Calculate CRC for all bytes except the last 3 (which contain the CRC) - crc := uint32(0) - for i := 0; i < len(data)-3; i++ { - crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF - } - - // Extract transmitted CRC from last 3 bytes - transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1]) - - return crc == transmittedCRC -} - // Mode S Downlink Format (DF) constants. // The DF field (first 5 bits) determines the message type and structure. const ( @@ -119,9 +82,9 @@ type Aircraft struct { // Motion and Dynamics VerticalRate int // Vertical rate in feet per minute (climb/descent) - GroundSpeed int // Ground speed in knots (integer) - Track int // Track angle in degrees (0-359, integer) - Heading int // Aircraft heading in degrees (magnetic, integer) + GroundSpeed float64 // Ground speed in knots + Track float64 // Track angle in degrees (direction of movement) + Heading float64 // Aircraft heading in degrees (magnetic) // Aircraft Information Category string // Aircraft category (size, type, performance) @@ -163,36 +126,23 @@ type Decoder struct { cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon) cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison) cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison) - - // Reference position for CPR zone ambiguity resolution (receiver location) - refLatitude float64 // Receiver latitude in decimal degrees - refLongitude float64 // Receiver longitude in decimal degrees - - // Mutex to protect concurrent access to CPR maps - mu sync.RWMutex } // NewDecoder creates a new Mode S/ADS-B decoder with initialized CPR tracking. // -// The reference position (typically the receiver location) is used to resolve -// CPR zone ambiguity during position decoding. Without a proper reference, -// aircraft can appear many degrees away from their actual position. -// -// Parameters: -// - refLat: Reference latitude in decimal degrees (receiver location) -// - refLon: Reference longitude in decimal degrees (receiver location) +// The decoder is ready to process Mode S messages immediately and will +// maintain CPR position state across multiple messages for accurate +// position decoding. // // Returns a configured decoder ready for message processing. -func NewDecoder(refLat, refLon float64) *Decoder { +func NewDecoder() *Decoder { return &Decoder{ - cprEvenLat: make(map[uint32]float64), - cprEvenLon: make(map[uint32]float64), - cprOddLat: make(map[uint32]float64), - cprOddLon: make(map[uint32]float64), - cprEvenTime: make(map[uint32]int64), - cprOddTime: make(map[uint32]int64), - refLatitude: refLat, - refLongitude: refLon, + cprEvenLat: make(map[uint32]float64), + cprEvenLon: make(map[uint32]float64), + cprOddLat: make(map[uint32]float64), + cprOddLon: make(map[uint32]float64), + cprEvenTime: make(map[uint32]int64), + cprOddTime: make(map[uint32]int64), } } @@ -218,11 +168,6 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) { return nil, fmt.Errorf("message too short: %d bytes", len(data)) } - // Validate CRC to reject corrupted messages that create ghost targets - if !validateModeSCRC(data) { - return nil, fmt.Errorf("invalid CRC - corrupted message") - } - df := (data[0] >> 3) & 0x1F icao := d.extractICAO(data, df) @@ -392,8 +337,7 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10]) oddFlag := (data[6] >> 2) & 0x01 - // Store CPR values for later decoding (protected by mutex) - d.mu.Lock() + // Store CPR values for later decoding if oddFlag == 1 { d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0 d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0 @@ -401,7 +345,6 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0 d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0 } - d.mu.Unlock() // Try to decode position if we have both even and odd messages d.decodeCPRPosition(aircraft) @@ -409,32 +352,37 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { // decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding. // -// CRITICAL: The CPR algorithm has zone ambiguity that requires either: -// 1. A reference position (receiver location) to resolve zones correctly, OR -// 2. Message timestamp comparison to choose the most recent valid position +// This is the core algorithm for resolving aircraft positions from CPR-encoded data. +// The algorithm requires both even and odd CPR messages to resolve position ambiguity. // -// Without proper zone resolution, aircraft can appear 6+ degrees away from actual position. -// This implementation uses global decoding which can produce large errors without -// additional context about expected aircraft location. +// CPR Global Decoding Algorithm: +// 1. Check that both even and odd CPR values are available +// 2. Calculate latitude using even/odd zone boundaries +// 3. Determine which latitude zone contains the aircraft +// 4. Calculate longitude based on the resolved latitude +// 5. Apply range corrections to get final position +// +// Mathematical Process: +// - Latitude zones are spaced 360ยฐ/60 = 6ยฐ apart for even messages +// - Latitude zones are spaced 360ยฐ/59 = ~6.1ยฐ apart for odd messages +// - The zone offset calculation resolves which 6ยฐ band contains the aircraft +// - Longitude calculation depends on latitude due to Earth's spherical geometry +// +// Note: This implementation uses a simplified approach. Production systems +// should also consider message timestamps to choose the most recent position. // // Parameters: // - aircraft: Aircraft struct to update with decoded position func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { - // Read CPR values with read lock - d.mu.RLock() evenLat, evenExists := d.cprEvenLat[aircraft.ICAO24] oddLat, oddExists := d.cprOddLat[aircraft.ICAO24] if !evenExists || !oddExists { - d.mu.RUnlock() return } evenLon := d.cprEvenLon[aircraft.ICAO24] oddLon := d.cprOddLon[aircraft.ICAO24] - d.mu.RUnlock() - - // CPR input values ready for decoding // CPR decoding algorithm dLat := 360.0 / 60.0 @@ -450,36 +398,8 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { latOdd -= 360 } - // Additional range correction to ensure valid latitude bounds (-90ยฐ to +90ยฐ) - if latEven > 90 { - latEven = 180 - latEven - } else if latEven < -90 { - latEven = -180 - latEven - } - - if latOdd > 90 { - latOdd = 180 - latOdd - } else if latOdd < -90 { - latOdd = -180 - latOdd - } - - // Validate final latitude values are within acceptable range - if math.Abs(latOdd) > 90 || math.Abs(latEven) > 90 { - // Invalid CPR decoding - skip position update - return - } - - // Zone ambiguity resolution using receiver reference position - // Calculate which decoded latitude is closer to the receiver - distToEven := math.Abs(latEven - d.refLatitude) - distToOdd := math.Abs(latOdd - d.refLatitude) - - // Choose the latitude solution that's closer to the receiver position - if distToOdd < distToEven { - aircraft.Latitude = latOdd - } else { - aircraft.Latitude = latEven - } + // Choose the most recent position + aircraft.Latitude = latOdd // Use odd for now, should check timestamps // Longitude calculation nl := d.nlFunction(aircraft.Latitude) @@ -490,19 +410,9 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { if lon >= 180 { lon -= 360 - } else if lon <= -180 { - lon += 360 - } - - // Validate longitude is within acceptable range - if math.Abs(lon) > 180 { - // Invalid longitude - skip position update - return } aircraft.Longitude = lon - - // CPR decoding completed successfully } // nlFunction calculates the number of longitude zones (NL) for a given latitude. @@ -574,21 +484,11 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) { nsVel = -nsVel } - // Calculate ground speed in knots (rounded to integer) - speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel) - - // Validate speed range (0-600 knots for civilian aircraft) - if speedKnots > 600 { - speedKnots = 600 // Cap at reasonable maximum + aircraft.GroundSpeed = math.Sqrt(ewVel*ewVel + nsVel*nsVel) + aircraft.Track = math.Atan2(ewVel, nsVel) * 180 / math.Pi + if aircraft.Track < 0 { + aircraft.Track += 360 } - aircraft.GroundSpeed = int(math.Round(speedKnots)) - - // Calculate track in degrees (0-359) - trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi - if trackDeg < 0 { - trackDeg += 360 - } - aircraft.Track = int(math.Round(trackDeg)) } // Vertical rate @@ -638,36 +538,19 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int { return 0 } - // Standard altitude encoding with 25 ft increments - // Check Q-bit (bit 4) for encoding type - qBit := (altCode >> 4) & 1 - - if qBit == 1 { - // Standard altitude with Q-bit set - // Remove Q-bit and reassemble 11-bit altitude code - n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F) - alt := int(n)*25 - 1000 - - // Validate altitude range - if alt < -1000 || alt > 60000 { - return 0 - } + // Gray code to binary conversion + var n uint16 + for i := uint(0); i < 12; i++ { + n ^= altCode >> i + } + + alt := int(n)*25 - 1000 + + if tc >= 20 && tc <= 22 { + // GNSS altitude return alt } - - // Gray code altitude (100 ft increments) - legacy encoding - // Convert from Gray code to binary - n := altCode - n ^= n >> 8 - n ^= n >> 4 - n ^= n >> 2 - n ^= n >> 1 - - // Convert to altitude in feet - alt := int(n&0x7FF) * 100 - if alt < 0 || alt > 60000 { - return 0 - } + return alt } @@ -873,14 +756,14 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) { // Movement movement := uint8(data[4]&0x07)<<4 | uint8(data[5])>>4 if movement > 0 && movement < 125 { - aircraft.GroundSpeed = int(math.Round(d.decodeGroundSpeed(movement))) + aircraft.GroundSpeed = d.decodeGroundSpeed(movement) } // Track trackValid := (data[5] >> 3) & 0x01 if trackValid != 0 { trackBits := uint16(data[5]&0x07)<<4 | uint16(data[6])>>4 - aircraft.Track = int(math.Round(float64(trackBits) * 360.0 / 128.0)) + aircraft.Track = float64(trackBits) * 360.0 / 128.0 } // CPR position (similar to airborne) @@ -888,8 +771,6 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) { cprLon := uint32(data[8]&0x01)<<16 | uint32(data[9])<<8 | uint32(data[10]) oddFlag := (data[6] >> 2) & 0x01 - // Store CPR values for later decoding (protected by mutex) - d.mu.Lock() if oddFlag == 1 { d.cprOddLat[aircraft.ICAO24] = float64(cprLat) / 131072.0 d.cprOddLon[aircraft.ICAO24] = float64(cprLon) / 131072.0 @@ -897,7 +778,6 @@ func (d *Decoder) decodeSurfacePosition(data []byte, aircraft *Aircraft) { d.cprEvenLat[aircraft.ICAO24] = float64(cprLat) / 131072.0 d.cprEvenLon[aircraft.ICAO24] = float64(cprLon) / 131072.0 } - d.mu.Unlock() d.decodeCPRPosition(aircraft) } diff --git a/internal/server/server.go b/internal/server/server.go index caeb1ed..e972c49 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -110,10 +110,10 @@ func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin Ori CheckOrigin: func(r *http.Request) bool { return true // Allow all origins in development }, - ReadBufferSize: 8192, - WriteBufferSize: 8192, + ReadBufferSize: 1024, + WriteBufferSize: 1024, }, - broadcastChan: make(chan []byte, 1000), + broadcastChan: make(chan []byte, 100), stopChan: make(chan struct{}), } } @@ -183,7 +183,6 @@ func (s *Server) setupRoutes() http.Handler { api := router.PathPrefix("/api").Subrouter() api.HandleFunc("/aircraft", s.handleGetAircraft).Methods("GET") api.HandleFunc("/aircraft/{icao}", s.handleGetAircraftDetails).Methods("GET") - api.HandleFunc("/debug/aircraft", s.handleDebugAircraft).Methods("GET") api.HandleFunc("/sources", s.handleGetSources).Methods("GET") api.HandleFunc("/stats", s.handleGetStats).Methods("GET") api.HandleFunc("/origin", s.handleGetOrigin).Methods("GET") @@ -204,60 +203,29 @@ func (s *Server) setupRoutes() http.Handler { return s.enableCORS(router) } -// isAircraftUseful determines if an aircraft has enough data to be useful for the frontend. -// -// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they -// don't have enough data to appear on the map. This provides users visibility into -// all tracked aircraft, not just those with complete position data. -// -// Aircraft are considered useful if they have ANY of: -// - Valid position data (both latitude and longitude non-zero) -> Can show on map -// - Callsign (flight identification) -> Can show in table with "No position" status -// - Altitude information -> Can show in table as "Aircraft at X feet" -// - Any other identifying information that makes it a "real" aircraft -// -// This inclusive approach ensures the table view shows all aircraft we're tracking, -// while the map view only shows those with valid positions (handled by frontend filtering). -func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool { - // Aircraft is useful if it has any meaningful data: - hasValidPosition := aircraft.Latitude != 0 && aircraft.Longitude != 0 - hasCallsign := aircraft.Callsign != "" - hasAltitude := aircraft.Altitude != 0 - hasSquawk := aircraft.Squawk != "" - - // Include aircraft with any identifying or operational data - return hasValidPosition || hasCallsign || hasAltitude || hasSquawk -} - // handleGetAircraft serves the /api/aircraft endpoint. // Returns all currently tracked aircraft with their latest state information. // -// Only "useful" aircraft are returned - those with position data or callsign. -// This filters out incomplete aircraft that only have altitude or squawk codes, -// which are not actionable for frontend mapping and flight tracking. -// // The response includes: // - timestamp: Unix timestamp of the response // - aircraft: Map of aircraft keyed by ICAO hex strings -// - count: Total number of useful aircraft (filtered count) +// - count: Total number of aircraft // // Aircraft ICAO addresses are converted from uint32 to 6-digit hex strings // for consistent JSON representation (e.g., 0xABC123 -> "ABC123"). func (s *Server) handleGetAircraft(w http.ResponseWriter, r *http.Request) { aircraft := s.merger.GetAircraft() - // Convert ICAO keys to hex strings for JSON and filter useful aircraft + // Convert ICAO keys to hex strings for JSON aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { - if s.isAircraftUseful(state) { - aircraftMap[fmt.Sprintf("%06X", icao)] = state - } + aircraftMap[fmt.Sprintf("%06X", icao)] = state } response := map[string]interface{}{ "timestamp": time.Now().Unix(), "aircraft": aircraftMap, - "count": len(aircraftMap), // Count of filtered useful aircraft + "count": len(aircraft), } w.Header().Set("Content-Type", "application/json") @@ -510,12 +478,10 @@ func (s *Server) sendInitialData(conn *websocket.Conn) { sources := s.merger.GetSources() stats := s.merger.GetStatistics() - // Convert ICAO keys to hex strings and filter useful aircraft + // Convert ICAO keys to hex strings aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { - if s.isAircraftUseful(state) { - aircraftMap[fmt.Sprintf("%06X", icao)] = state - } + aircraftMap[fmt.Sprintf("%06X", icao)] = state } update := AircraftUpdate{ @@ -589,10 +555,9 @@ func (s *Server) periodicUpdateRoutine() { // // This function: // 1. Collects current aircraft data from the merger -// 2. Filters aircraft to only include "useful" ones (with position or callsign) -// 3. Formats the data as a WebSocketMessage with type "aircraft_update" -// 4. Converts ICAO addresses to hex strings for JSON compatibility -// 5. Queues the message for broadcast (non-blocking) +// 2. Formats the data as a WebSocketMessage with type "aircraft_update" +// 3. Converts ICAO addresses to hex strings for JSON compatibility +// 4. Queues the message for broadcast (non-blocking) // // If the broadcast channel is full, the update is dropped to prevent blocking. // This ensures the system continues operating even if WebSocket clients @@ -602,12 +567,10 @@ func (s *Server) broadcastUpdate() { sources := s.merger.GetSources() stats := s.merger.GetStatistics() - // Convert ICAO keys to hex strings and filter useful aircraft + // Convert ICAO keys to hex strings aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { - if s.isAircraftUseful(state) { - aircraftMap[fmt.Sprintf("%06X", icao)] = state - } + aircraftMap[fmt.Sprintf("%06X", icao)] = state } update := AircraftUpdate{ @@ -748,34 +711,3 @@ func (s *Server) enableCORS(handler http.Handler) http.Handler { handler.ServeHTTP(w, r) }) } - -// handleDebugAircraft serves the /api/debug/aircraft endpoint. -// Returns all aircraft (filtered and unfiltered) for debugging position issues. -func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) { - aircraft := s.merger.GetAircraft() - - // All aircraft (unfiltered) - allAircraftMap := make(map[string]*merger.AircraftState) - for icao, state := range aircraft { - allAircraftMap[fmt.Sprintf("%06X", icao)] = state - } - - // Filtered aircraft (useful ones) - filteredAircraftMap := make(map[string]*merger.AircraftState) - for icao, state := range aircraft { - if s.isAircraftUseful(state) { - filteredAircraftMap[fmt.Sprintf("%06X", icao)] = state - } - } - - response := map[string]interface{}{ - "timestamp": time.Now().Unix(), - "all_aircraft": allAircraftMap, - "filtered_aircraft": filteredAircraftMap, - "all_count": len(allAircraftMap), - "filtered_count": len(filteredAircraftMap), - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} diff --git a/main b/main deleted file mode 100755 index 12ac49d..0000000 Binary files a/main and /dev/null differ diff --git a/ux.png b/ux.png deleted file mode 100644 index ec40c79..0000000 Binary files a/ux.png and /dev/null differ