// UI and table management module import { escapeHtml } from './html-utils.js'; 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 esc = escapeHtml; const row = document.createElement('tr'); row.innerHTML = ` ${esc(icao)} ${esc(aircraft.Callsign) || '-'} ${this.formatSquawk(aircraft)} ${altitude ? `${esc(altitude)} ft` : '-'} ${esc(aircraft.GroundSpeed) || '-'} kt ${distance ? esc(distance.toFixed(1)) : '-'} km ${esc(aircraft.Track) || '-'}° ${esc(sources)} ${bestSignal ? esc(bestSignal.toFixed(1)) : '-'} ${esc(this.calculateAge(aircraft).toFixed(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'; } calculateAge(aircraft) { if (!aircraft.last_update) return 0; const lastUpdate = new Date(aircraft.last_update); const now = new Date(); const ageMs = now.getTime() - lastUpdate.getTime(); return Math.max(0, ageMs / 1000); // Return age in seconds, minimum 0 } 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'; } formatSquawk(aircraft) { if (!aircraft.Squawk) return '-'; const esc = escapeHtml; // If we have a description, format it nicely if (aircraft.SquawkDescription) { // Check if it's an emergency code (contains warning emoji) if (aircraft.SquawkDescription.includes('⚠️')) { return `${esc(aircraft.Squawk)}`; } // Check if it's a special code (contains special emoji) else if (aircraft.SquawkDescription.includes('🔸')) { return `${esc(aircraft.Squawk)}`; } // Check if it's a military code (contains military emoji) else if (aircraft.SquawkDescription.includes('🔰')) { return `${esc(aircraft.Squawk)}`; } // Standard codes else { return `${esc(aircraft.Squawk)}`; } } // No description available, show just the code return esc(aircraft.Squawk); } updateSourceFilter() { const select = document.getElementById('source-filter'); if (!select) return; const currentValue = select.value; // Clear options except "All Sources" select.innerHTML = ''; // Sort sources alphabetically by name and add options const sortedSources = Array.from(this.sourcesData.entries()).sort((a, b) => a[1].name.localeCompare(b[1].name) ); for (const [id, source] of sortedSources) { 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 activeViewersEl = document.getElementById('active-viewers'); const maxRangeEl = document.getElementById('max-range'); const messagesSecEl = document.getElementById('messages-sec'); const aircraftWithPositionEl = document.getElementById('aircraft-with-position'); const aircraftWithoutPositionEl = document.getElementById('aircraft-without-position'); if (totalAircraftEl) totalAircraftEl.textContent = this.aircraftData.size; if (activeSourcesEl) { activeSourcesEl.textContent = Array.from(this.sourcesData.values()).filter(s => s.active).length; } if (activeViewersEl) { activeViewersEl.textContent = this.stats.active_clients || 1; } // Update position tracking statistics from backend if (aircraftWithPositionEl) { aircraftWithPositionEl.textContent = this.stats.aircraft_with_position || 0; } if (aircraftWithoutPositionEl) { aircraftWithoutPositionEl.textContent = this.stats.aircraft_without_position || 0; } // 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 positionSummaryEl = document.getElementById('position-summary'); const sourcesCountEl = document.getElementById('sources-count'); const activeClientsEl = document.getElementById('active-clients'); if (aircraftCountEl) aircraftCountEl.textContent = `${this.aircraftData.size} aircraft`; // Update position summary in header if (positionSummaryEl) { const positioned = this.stats.aircraft_with_position || 0; positionSummaryEl.textContent = `${positioned} positioned`; } if (sourcesCountEl) sourcesCountEl.textContent = `${this.sourcesData.size} sources`; // Update active clients count const clientCount = this.stats.active_clients || 1; if (activeClientsEl) { const clientText = clientCount === 1 ? 'viewer' : 'viewers'; activeClientsEl.textContent = `${clientCount} ${clientText}`; } 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(); this.updateClock('utc', now, true); // UTC clock - use UTC methods this.updateClock('update', this.lastUpdateTime, false); // Last update clock - use local methods } updateClock(prefix, time, useUTC = false) { // Use appropriate time methods based on whether we want UTC or local time const hours = useUTC ? time.getUTCHours() : time.getHours(); const minutes = useUTC ? time.getUTCMinutes() : time.getMinutes(); 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); // Simple toast notification implementation const toast = document.createElement('div'); toast.className = 'toast-notification error'; toast.textContent = message; // Add to page document.body.appendChild(toast); // Show toast with animation setTimeout(() => toast.classList.add('show'), 100); // Auto-remove after 5 seconds setTimeout(() => { toast.classList.remove('show'); setTimeout(() => document.body.removeChild(toast), 300); }, 5000); } }