mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
Add ADS-B range rings, statistics, geolocation, and reception stats
New features for both ADS-B tab and dashboard: 1. Range Rings - Concentric circles at 25, 50, 100, 150, 200nm showing distance from observer location with dashed styling and labels 2. Statistics Panel - Tracks: - Max range achieved (with aircraft that achieved it) - Total unique aircraft seen this session - Messages per second rate - Busiest hour of tracking 3. Geolocation Button - Gets user's actual GPS location to: - Center the map on their position - Calculate accurate distances for range statistics - Position range rings correctly 4. Reception Statistics - Real-time msg/sec counter to monitor receiver performance All features work on both the ADS-B tab and full-screen dashboard. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -599,6 +599,32 @@
|
||||
<div class="stat-value" id="statAvgAlt">0</div>
|
||||
<div class="stat-label">Avg Alt (ft)</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><span id="statMaxRange">0.0</span> nm</div>
|
||||
<div class="stat-label">Max Range</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="statTotalSeen">0</div>
|
||||
<div class="stat-label">Total Seen</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><span id="statMsgRate">0.0</span>/s</div>
|
||||
<div class="stat-label">Msg Rate</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="stat-value" id="statBusiestHour">--</div>
|
||||
<div class="stat-label">Busiest Hour</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 0 15px 10px;">
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 12px; color: var(--text-primary); margin-bottom: 8px;">
|
||||
<input type="checkbox" id="showRangeRings" onchange="drawRangeRings()" style="accent-color: var(--accent-green);">
|
||||
Show Range Rings
|
||||
</label>
|
||||
<button id="geolocateBtn" onclick="getGeolocation()" style="width: 100%; padding: 8px; background: rgba(0,255,136,0.2); border: 1px solid rgba(0,255,136,0.3); border-radius: 4px; color: var(--accent-green); font-family: 'JetBrains Mono', monospace; font-size: 11px; cursor: pointer; margin-bottom: 5px;">
|
||||
📍 My Location
|
||||
</button>
|
||||
<div style="text-align: center; font-size: 10px; color: var(--text-secondary);" id="observerLoc">51.5074, -0.1278</div>
|
||||
</div>
|
||||
<div style="padding: 0 15px 10px;">
|
||||
<label style="font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-secondary); display: block; margin-bottom: 5px;">Aircraft Filter</label>
|
||||
@@ -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: `<span style="color: #00ff88; font-size: 10px; background: rgba(0,0,0,0.7); padding: 1px 4px; border-radius: 2px;">${nm} nm</span>`,
|
||||
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: '<div style="width: 12px; height: 12px; background: #ff0; border: 2px solid #000; border-radius: 50%; box-shadow: 0 0 10px #ff0;"></div>',
|
||||
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);
|
||||
|
||||
@@ -3202,6 +3202,16 @@
|
||||
<input type="checkbox" id="adsbEnableClustering" onchange="toggleAircraftClustering()">
|
||||
Cluster Markers
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="adsbShowRangeRings" onchange="drawRangeRings()">
|
||||
Show Range Rings
|
||||
</label>
|
||||
</div>
|
||||
<button class="preset-btn" id="adsbGeolocateBtn" onclick="getAdsbGeolocation()" style="width: 100%; margin-top: 10px;">
|
||||
📍 My Location
|
||||
</button>
|
||||
<div class="info-text" style="margin-top: 5px; text-align: center;">
|
||||
<span id="adsbObserverLoc">51.5074, -0.1278</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<label>Aircraft Filter</label>
|
||||
@@ -3220,6 +3230,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Reception Statistics</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
|
||||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Max Range</div>
|
||||
<div id="adsbMaxRange" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0.0 nm</div>
|
||||
</div>
|
||||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Total Seen</div>
|
||||
<div id="adsbTotalSeen" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0</div>
|
||||
</div>
|
||||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Msg Rate</div>
|
||||
<div id="adsbMsgRate" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0.0/s</div>
|
||||
</div>
|
||||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Busiest Hour</div>
|
||||
<div id="adsbBusiestHour" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="adsbToolStatus">
|
||||
<span>dump1090:</span><span class="tool-status" id="dump1090Status">Checking...</span>
|
||||
<span>rtl_adsb:</span><span class="tool-status" id="rtlAdsbStatus">Checking...</span>
|
||||
@@ -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: `<span style="color: #00d4ff; font-size: 10px; background: rgba(0,0,0,0.7); padding: 1px 4px; border-radius: 2px;">${nm} nm</span>`,
|
||||
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: '<div style="width: 12px; height: 12px; background: #ff0; border: 2px solid #000; border-radius: 50%; box-shadow: 0 0 10px #ff0;"></div>',
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user