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)
+
+
+
+
+
+
+
+
+
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
+
+
+
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();
}