diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index abe68c3..2cc42ef 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -198,6 +198,11 @@ 50% { opacity: 0.3; } } + @keyframes slideDown { + from { opacity: 0; transform: translateX(-50%) translateY(-20px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } + .panel-content { padding: 0; height: calc(100% - 45px); @@ -604,6 +609,12 @@ +
+ +
@@ -650,6 +661,99 @@ let eventSource = null; let isTracking = false; let currentFilter = 'all'; + let alertedAircraft = {}; // Track aircraft that have already triggered alerts + let alertsEnabled = true; // Toggle for audio alerts + + // Audio alert system using Web Audio API + let audioContext = null; + function getAudioContext() { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioContext; + } + + function playAlertSound(type) { + if (!alertsEnabled) return; + try { + const ctx = getAudioContext(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + + if (type === 'emergency') { + // Urgent two-tone alert for emergencies + oscillator.frequency.setValueAtTime(880, ctx.currentTime); + oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15); + oscillator.frequency.setValueAtTime(880, ctx.currentTime + 0.3); + gainNode.gain.setValueAtTime(0.3, ctx.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5); + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.5); + } else if (type === 'military') { + // Single tone for military + oscillator.frequency.setValueAtTime(523, ctx.currentTime); + gainNode.gain.setValueAtTime(0.2, ctx.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.3); + } + } catch (e) { + console.warn('Audio alert failed:', e); + } + } + + function checkAndAlertAircraft(icao, ac) { + // Skip if already alerted for this aircraft + if (alertedAircraft[icao]) return; + + const militaryInfo = isMilitaryAircraft(icao, ac.callsign); + const squawkInfo = checkSquawkCode(ac); + + if (squawkInfo) { + alertedAircraft[icao] = 'emergency'; + playAlertSound('emergency'); + showAlertBanner(`⚠️ EMERGENCY: ${squawkInfo.name} - ${ac.callsign || icao}`, '#ff0000'); + } else if (militaryInfo.military) { + alertedAircraft[icao] = 'military'; + playAlertSound('military'); + showAlertBanner(`🎖️ MILITARY: ${ac.callsign || icao}${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}`, '#556b2f'); + } + } + + function showAlertBanner(message, color) { + const banner = document.createElement('div'); + banner.style.cssText = ` + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + background: ${color}; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: bold; + font-size: 14px; + z-index: 10000; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + animation: slideDown 0.3s ease-out; + `; + banner.textContent = message; + document.body.appendChild(banner); + + // Auto-remove after 5 seconds + setTimeout(() => { + banner.style.opacity = '0'; + banner.style.transition = 'opacity 0.3s'; + setTimeout(() => banner.remove(), 300); + }, 5000); + } + + function toggleAlerts() { + alertsEnabled = document.getElementById('alertToggle').checked; + } // Military aircraft detection const MILITARY_RANGES = [ @@ -899,6 +1003,9 @@ lastSeen: Date.now() }; + // Check for military/emergency aircraft and alert + checkAndAlertAircraft(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 1d05f5b..75fd61b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2608,6 +2608,16 @@ to { background: #cc0000; } } + @keyframes slideDown { + from { opacity: 0; transform: translateX(-50%) translateY(-20px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } + + @keyframes fadeOut { + from { opacity: 1; } + to { opacity: 0; } + } + /* Military Aircraft */ .military-aircraft { border-left: 3px solid #556b2f; @@ -3202,6 +3212,12 @@ +
+ +
@@ -3813,6 +3829,94 @@ let adsbEventSource = null; let aircraftTrails = {}; // ICAO -> array of positions let activeSquawkAlerts = {}; // Active emergency squawk alerts + let alertedAircraft = {}; // Track aircraft that have already triggered alerts + let adsbAlertsEnabled = true; // Toggle for audio alerts + + // Audio alert system using Web Audio API + let audioContext = null; + function getAudioContext() { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + return audioContext; + } + + function playAlertSound(type) { + if (!adsbAlertsEnabled) return; + try { + const ctx = getAudioContext(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + + if (type === 'emergency') { + // Urgent two-tone alert for emergencies + oscillator.frequency.setValueAtTime(880, ctx.currentTime); + oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15); + oscillator.frequency.setValueAtTime(880, ctx.currentTime + 0.3); + gainNode.gain.setValueAtTime(0.3, ctx.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5); + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.5); + } else if (type === 'military') { + // Single tone for military + oscillator.frequency.setValueAtTime(523, ctx.currentTime); + gainNode.gain.setValueAtTime(0.2, ctx.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.3); + } + } catch (e) { + console.warn('Audio alert failed:', e); + } + } + + function checkAndAlertAircraft(icao, aircraft) { + // Skip if already alerted for this aircraft + if (alertedAircraft[icao]) return; + + const militaryInfo = isMilitaryAircraft(icao, aircraft.callsign); + const squawkInfo = checkSquawkCode(aircraft); + + if (squawkInfo) { + alertedAircraft[icao] = 'emergency'; + playAlertSound('emergency'); + showAlertBanner(`⚠️ EMERGENCY: ${squawkInfo.name} - ${aircraft.callsign || icao}`, squawkInfo.color); + } else if (militaryInfo.military) { + alertedAircraft[icao] = 'military'; + playAlertSound('military'); + showAlertBanner(`🎖️ MILITARY: ${aircraft.callsign || icao}${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}`, '#556b2f'); + } + } + + function showAlertBanner(message, color) { + const banner = document.createElement('div'); + banner.style.cssText = ` + position: fixed; + top: 80px; + left: 50%; + transform: translateX(-50%); + background: ${color}; + color: white; + padding: 12px 24px; + border-radius: 8px; + font-weight: bold; + font-size: 14px; + z-index: 10000; + box-shadow: 0 4px 20px rgba(0,0,0,0.5); + animation: slideDown 0.3s ease-out; + `; + banner.textContent = message; + document.body.appendChild(banner); + + // Auto-remove after 5 seconds + setTimeout(() => { + banner.style.animation = 'fadeOut 0.3s ease-out forwards'; + setTimeout(() => banner.remove(), 300); + }, 5000); + } // Emergency squawk codes const SQUAWK_CODES = { @@ -8033,6 +8137,8 @@ }; adsbMsgCount++; pendingAircraftData.push(data); + // Check for military/emergency aircraft and alert + checkAndAlertAircraft(data.icao, adsbAircraft[data.icao]); // Use batched update instead of immediate scheduleAircraftUIUpdate(); }