diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index dcd84d8..f220e11 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -599,6 +599,32 @@
0
Avg Alt (ft)
+
+
0.0 nm
+
Max Range
+
+
+
0
+
Total Seen
+
+
+
0.0/s
+
Msg Rate
+
+
+
--
+
Busiest Hour
+
+ +
+ + +
51.5074, -0.1278
@@ -664,6 +690,22 @@ let alertedAircraft = {}; // Track aircraft that have already triggered alerts let alertsEnabled = true; // Toggle for audio alerts + // Statistics tracking + let stats = { + totalAircraftSeen: new Set(), + maxRange: 0, + maxRangeAircraft: null, + hourlyCount: {}, + messagesPerSecond: 0, + messageTimestamps: [], + sessionStart: null + }; + + // Observer location and range rings + let observerLocation = { lat: 51.5074, lon: -0.1278 }; + let rangeRingsLayer = null; + let observerMarker = null; + // Audio alert system using Web Audio API let audioContext = null; function getAudioContext() { @@ -755,6 +797,171 @@ alertsEnabled = document.getElementById('alertToggle').checked; } + // Calculate distance between two points in nautical miles + function calculateDistanceNm(lat1, lon1, lat2, lon2) { + const R = 3440.065; // Earth radius in nautical miles + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + // Update statistics + function updateStatistics(icao, ac) { + if (!ac.lat || !ac.lon) return; + + stats.totalAircraftSeen.add(icao); + + const distance = calculateDistanceNm( + observerLocation.lat, observerLocation.lon, + ac.lat, ac.lon + ); + + if (distance > stats.maxRange) { + stats.maxRange = distance; + stats.maxRangeAircraft = ac.callsign || icao; + } + + const hour = new Date().getHours(); + if (!stats.hourlyCount[hour]) { + stats.hourlyCount[hour] = new Set(); + } + stats.hourlyCount[hour].add(icao); + + const now = Date.now(); + stats.messageTimestamps.push(now); + stats.messageTimestamps = stats.messageTimestamps.filter(t => now - t < 5000); + stats.messagesPerSecond = stats.messageTimestamps.length / 5; + + updateStatsDisplay(); + } + + function updateStatsDisplay() { + const maxRangeEl = document.getElementById('statMaxRange'); + const totalSeenEl = document.getElementById('statTotalSeen'); + const msgRateEl = document.getElementById('statMsgRate'); + const busiestEl = document.getElementById('statBusiestHour'); + + if (maxRangeEl) maxRangeEl.textContent = stats.maxRange.toFixed(1); + if (totalSeenEl) totalSeenEl.textContent = stats.totalAircraftSeen.size; + if (msgRateEl) msgRateEl.textContent = stats.messagesPerSecond.toFixed(1); + if (busiestEl) { + let busiestHour = '--'; + let maxCount = 0; + Object.entries(stats.hourlyCount).forEach(([hour, set]) => { + if (set.size > maxCount) { + maxCount = set.size; + busiestHour = `${hour}:00`; + } + }); + busiestEl.textContent = busiestHour; + } + } + + function resetStats() { + stats = { + totalAircraftSeen: new Set(), + maxRange: 0, + maxRangeAircraft: null, + hourlyCount: {}, + messagesPerSecond: 0, + messageTimestamps: [], + sessionStart: Date.now() + }; + updateStatsDisplay(); + } + + // Draw range rings on the map + function drawRangeRings() { + if (!radarMap) return; + + if (rangeRingsLayer) { + radarMap.removeLayer(rangeRingsLayer); + rangeRingsLayer = null; + } + + const showRings = document.getElementById('showRangeRings')?.checked; + if (!showRings) return; + + rangeRingsLayer = L.layerGroup(); + + const distances = [25, 50, 100, 150, 200]; + distances.forEach(nm => { + const meters = nm * 1852; + const circle = L.circle([observerLocation.lat, observerLocation.lon], { + radius: meters, + color: '#00ff88', + fillColor: 'transparent', + fillOpacity: 0, + weight: 1, + opacity: 0.4, + dashArray: '5, 5' + }); + + const labelLat = observerLocation.lat + (nm * 0.0166); + const label = L.marker([labelLat, observerLocation.lon], { + icon: L.divIcon({ + className: 'range-label', + html: `${nm} nm`, + iconSize: [40, 12], + iconAnchor: [20, 6] + }) + }); + + rangeRingsLayer.addLayer(circle); + rangeRingsLayer.addLayer(label); + }); + + // Observer marker + if (observerMarker) radarMap.removeLayer(observerMarker); + observerMarker = L.marker([observerLocation.lat, observerLocation.lon], { + icon: L.divIcon({ + className: 'observer-marker', + html: '
', + iconSize: [12, 12], + iconAnchor: [6, 6] + }) + }).bindPopup('Your Location').addTo(radarMap); + + rangeRingsLayer.addTo(radarMap); + } + + // Get user geolocation + function getGeolocation() { + if (!navigator.geolocation) { + alert('Geolocation not supported'); + return; + } + + const btn = document.getElementById('geolocateBtn'); + if (btn) btn.textContent = '📍 Locating...'; + + navigator.geolocation.getCurrentPosition( + (position) => { + observerLocation.lat = position.coords.latitude; + observerLocation.lon = position.coords.longitude; + + const locEl = document.getElementById('observerLoc'); + if (locEl) locEl.textContent = `${observerLocation.lat.toFixed(4)}, ${observerLocation.lon.toFixed(4)}`; + + if (radarMap) { + radarMap.setView([observerLocation.lat, observerLocation.lon], 8); + } + + drawRangeRings(); + if (btn) btn.textContent = '📍 My Location'; + }, + (error) => { + alert('Location error: ' + error.message); + if (btn) btn.textContent = '📍 My Location'; + }, + { enableHighAccuracy: true, timeout: 10000 } + ); + } + // Military aircraft detection (specific military-only sub-ranges) const MILITARY_RANGES = [ { start: 0xADF7C0, end: 0xADFFFF, country: 'US' }, // US Military @@ -890,7 +1097,9 @@ } if (data.status === 'success' || data.status === 'started' || data.status === 'already_running' || data.status === 'error' && data.message && data.message.includes('already')) { + resetStats(); // Reset statistics for new session startEventStream(); + drawRangeRings(); // Draw range rings if enabled isTracking = true; btn.textContent = 'STOP TRACKING'; btn.classList.add('active'); @@ -1005,6 +1214,9 @@ // Check for military/emergency aircraft and alert checkAndAlertAircraft(icao, aircraft[icao]); + // Update statistics + updateStatistics(icao, aircraft[icao]); + // Queue marker update if (data.lat && data.lon) { pendingMarkerUpdates.add(icao); diff --git a/templates/index.html b/templates/index.html index 421402a..a789779 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3202,6 +3202,16 @@ Cluster Markers + +
+ +
+ 51.5074, -0.1278
@@ -3220,6 +3230,28 @@
+
+

Reception Statistics

+
+
+
Max Range
+
0.0 nm
+
+
+
Total Seen
+
0
+
+
+
Msg Rate
+
0.0/s
+
+
+
Busiest Hour
+
--
+
+
+
+
dump1090:Checking... rtl_adsb:Checking... @@ -3832,6 +3864,22 @@ let alertedAircraft = {}; // Track aircraft that have already triggered alerts let adsbAlertsEnabled = true; // Toggle for audio alerts + // ADS-B Statistics tracking + let adsbStats = { + totalAircraftSeen: new Set(), // Unique ICAO codes seen + maxRange: 0, // Max distance in nm + maxRangeAircraft: null, // Aircraft that achieved max range + hourlyCount: {}, // Hour -> count of aircraft + messagesPerSecond: 0, // Current msg/sec rate + messageTimestamps: [], // Recent message timestamps for rate calc + sessionStart: null // When tracking started + }; + + // Observer location for distance calculations + let observerLocation = { lat: 51.5074, lon: -0.1278 }; // Default London + let rangeRingsLayer = null; + let observerMarkerAdsb = null; + // Audio alert system using Web Audio API (uses shared audioContext declared later) function getAdsbAudioContext() { if (!window.adsbAudioCtx) { @@ -8063,6 +8111,207 @@ } } + // Calculate distance between two points in nautical miles + function calculateDistanceNm(lat1, lon1, lat2, lon2) { + const R = 3440.065; // Earth radius in nautical miles + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon/2) * Math.sin(dLon/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + // Update ADS-B statistics + function updateAdsbStatistics(icao, aircraft) { + if (!aircraft.lat || !aircraft.lon) return; + + // Track unique aircraft + adsbStats.totalAircraftSeen.add(icao); + + // Calculate distance from observer + const distance = calculateDistanceNm( + observerLocation.lat, observerLocation.lon, + aircraft.lat, aircraft.lon + ); + + // Update max range if this is further + if (distance > adsbStats.maxRange) { + adsbStats.maxRange = distance; + adsbStats.maxRangeAircraft = aircraft.callsign || icao; + } + + // Track hourly aircraft count + const hour = new Date().getHours(); + if (!adsbStats.hourlyCount[hour]) { + adsbStats.hourlyCount[hour] = new Set(); + } + adsbStats.hourlyCount[hour].add(icao); + + // Update messages per second calculation + const now = Date.now(); + adsbStats.messageTimestamps.push(now); + // Keep only last 5 seconds of timestamps + adsbStats.messageTimestamps = adsbStats.messageTimestamps.filter(t => now - t < 5000); + adsbStats.messagesPerSecond = adsbStats.messageTimestamps.length / 5; + + // Update stats display + updateStatsDisplay(); + } + + // Update the statistics display + function updateStatsDisplay() { + const maxRangeEl = document.getElementById('adsbMaxRange'); + const totalSeenEl = document.getElementById('adsbTotalSeen'); + const msgRateEl = document.getElementById('adsbMsgRate'); + const busiestHourEl = document.getElementById('adsbBusiestHour'); + + if (maxRangeEl) { + maxRangeEl.textContent = `${adsbStats.maxRange.toFixed(1)} nm`; + if (adsbStats.maxRangeAircraft) { + maxRangeEl.title = `Aircraft: ${adsbStats.maxRangeAircraft}`; + } + } + if (totalSeenEl) { + totalSeenEl.textContent = adsbStats.totalAircraftSeen.size; + } + if (msgRateEl) { + msgRateEl.textContent = `${adsbStats.messagesPerSecond.toFixed(1)}/s`; + } + if (busiestHourEl) { + let busiestHour = 0; + let maxCount = 0; + Object.entries(adsbStats.hourlyCount).forEach(([hour, aircraftSet]) => { + if (aircraftSet.size > maxCount) { + maxCount = aircraftSet.size; + busiestHour = hour; + } + }); + busiestHourEl.textContent = maxCount > 0 ? `${busiestHour}:00 (${maxCount})` : '--'; + } + } + + // Draw range rings on the map + function drawRangeRings() { + if (!aircraftMap) return; + + // Remove existing rings + if (rangeRingsLayer) { + aircraftMap.removeLayer(rangeRingsLayer); + } + + const showRings = document.getElementById('adsbShowRangeRings')?.checked; + if (!showRings) return; + + rangeRingsLayer = L.layerGroup(); + + // Range ring distances in nautical miles + const distances = [25, 50, 100, 150, 200]; + + distances.forEach(nm => { + // Convert nm to meters for Leaflet circle + const meters = nm * 1852; + const circle = L.circle([observerLocation.lat, observerLocation.lon], { + radius: meters, + color: '#00d4ff', + fillColor: 'transparent', + fillOpacity: 0, + weight: 1, + opacity: 0.4, + dashArray: '5, 5' + }); + + // Add label + const labelLatLng = L.latLng( + observerLocation.lat + (nm * 0.0166), // Approx degrees per nm + observerLocation.lon + ); + + const label = L.marker(labelLatLng, { + icon: L.divIcon({ + className: 'range-ring-label', + html: `${nm} nm`, + iconSize: [40, 12], + iconAnchor: [20, 6] + }) + }); + + rangeRingsLayer.addLayer(circle); + rangeRingsLayer.addLayer(label); + }); + + // Add observer marker + if (observerMarkerAdsb) { + aircraftMap.removeLayer(observerMarkerAdsb); + } + observerMarkerAdsb = L.marker([observerLocation.lat, observerLocation.lon], { + icon: L.divIcon({ + className: 'observer-marker', + html: '
', + iconSize: [12, 12], + iconAnchor: [6, 6] + }) + }).bindPopup('Your Location').addTo(aircraftMap); + + rangeRingsLayer.addTo(aircraftMap); + } + + // Get user's geolocation + function getAdsbGeolocation() { + if (!navigator.geolocation) { + alert('Geolocation is not supported by your browser'); + return; + } + + const btn = document.getElementById('adsbGeolocateBtn'); + if (btn) btn.textContent = '📍 Locating...'; + + navigator.geolocation.getCurrentPosition( + (position) => { + observerLocation.lat = position.coords.latitude; + observerLocation.lon = position.coords.longitude; + + // Update display + const locDisplay = document.getElementById('adsbObserverLoc'); + if (locDisplay) { + locDisplay.textContent = `${observerLocation.lat.toFixed(4)}, ${observerLocation.lon.toFixed(4)}`; + } + + // Center map on location + if (aircraftMap) { + aircraftMap.setView([observerLocation.lat, observerLocation.lon], 8); + aircraftMap._userInteracted = true; // Prevent auto-fit + } + + // Redraw range rings + drawRangeRings(); + + if (btn) btn.textContent = '📍 My Location'; + showInfo(`Location set: ${observerLocation.lat.toFixed(4)}, ${observerLocation.lon.toFixed(4)}`); + }, + (error) => { + if (btn) btn.textContent = '📍 My Location'; + alert('Unable to get your location: ' + error.message); + }, + { enableHighAccuracy: true, timeout: 10000 } + ); + } + + // Reset ADS-B statistics + function resetAdsbStats() { + adsbStats = { + totalAircraftSeen: new Set(), + maxRange: 0, + maxRangeAircraft: null, + hourlyCount: {}, + messagesPerSecond: 0, + messageTimestamps: [], + sessionStart: Date.now() + }; + updateStatsDisplay(); + } + function startAdsbScan() { const gain = document.getElementById('adsbGain').value; const device = getSelectedDevice(); @@ -8080,7 +8329,10 @@ document.getElementById('stopAdsbBtn').style.display = 'block'; document.getElementById('statusDot').className = 'status-dot active'; document.getElementById('statusText').textContent = 'ADS-B Tracking'; + resetAdsbStats(); // Reset statistics for new session + adsbStats.sessionStart = Date.now(); startAdsbStream(); + drawRangeRings(); // Draw range rings if enabled } else { alert('Error: ' + data.message); } @@ -8137,6 +8389,8 @@ pendingAircraftData.push(data); // Check for military/emergency aircraft and alert checkAndAlertAircraft(data.icao, adsbAircraft[data.icao]); + // Update statistics + updateAdsbStatistics(data.icao, adsbAircraft[data.icao]); // Use batched update instead of immediate scheduleAircraftUIUpdate(); }