// Import Three.js modules import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; class SkyView { constructor() { this.map = null; this.coverageMap = null; this.radar3d = null; this.websocket = null; // Data storage this.aircraftData = new Map(); this.sourcesData = new Map(); this.stats = {}; // Map markers and overlays this.aircraftMarkers = new Map(); this.sourceMarkers = new Map(); this.aircraftTrails = new Map(); this.rangeCircles = new Map(); this.heatmapLayer = null; // UI state this.currentView = 'map-view'; this.showTrails = false; this.showRange = false; this.showSources = true; this.selectedSource = null; // Charts this.charts = {}; // Update tracking this.lastUpdateTime = new Date(); this.init(); } async init() { try { this.initializeViews(); this.initializeMap(); await this.initializeWebSocket(); this.initializeEventListeners(); this.initializeCharts(); this.initializeClocks(); this.initialize3DRadar(); this.startPeriodicTasks(); } catch (error) { console.error('Initialization failed:', error); this.showError('Failed to initialize application'); } } // View Management 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; // Handle view-specific initialization (extract base name for switch) const baseName = viewId.replace('-view', ''); switch (baseName) { case 'coverage': this.initializeCoverageMap(); break; case 'radar3d': this.update3DRadar(); break; } } // Map Initialization initializeMap() { this.map = L.map('map').setView([51.4700, -0.4600], 10); // Dark tile layer L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 19 }).addTo(this.map); // Map controls document.getElementById('center-map').addEventListener('click', () => this.centerMapOnAircraft()); document.getElementById('toggle-trails').addEventListener('click', () => this.toggleTrails()); document.getElementById('toggle-range').addEventListener('click', () => this.toggleRangeCircles()); document.getElementById('toggle-sources').addEventListener('click', () => this.toggleSources()); } initializeCoverageMap() { if (!this.coverageMap) { this.coverageMap = L.map('coverage-map').setView([51.4700, -0.4600], 10); L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors' }).addTo(this.coverageMap); } // Update coverage controls this.updateCoverageControls(); // Coverage controls document.getElementById('toggle-heatmap').addEventListener('click', () => this.toggleHeatmap()); document.getElementById('coverage-source').addEventListener('change', (e) => { this.selectedSource = e.target.value; this.updateCoverageDisplay(); }); } // WebSocket Connection async initializeWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; try { this.websocket = new WebSocket(wsUrl); this.websocket.onopen = () => { console.log('WebSocket connected'); this.updateConnectionStatus('connected'); }; this.websocket.onclose = () => { console.log('WebSocket disconnected'); this.updateConnectionStatus('disconnected'); // Reconnect after 5 seconds setTimeout(() => this.initializeWebSocket(), 5000); }; this.websocket.onerror = (error) => { console.error('WebSocket error:', error); this.updateConnectionStatus('disconnected'); }; this.websocket.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleWebSocketMessage(message); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; } catch (error) { console.error('WebSocket connection failed:', error); this.updateConnectionStatus('disconnected'); } } handleWebSocketMessage(message) { switch (message.type) { case 'initial_data': case 'aircraft_update': this.updateData(message.data); break; default: console.log('Unknown message type:', message.type); } } 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(); // Update UI this.updateMapMarkers(); this.updateAircraftTable(); this.updateStatistics(); this.updateHeaderInfo(); if (this.currentView === 'radar3d-view') { this.update3DRadar(); } } // Map Updates updateMapMarkers() { // Clear stale aircraft markers const currentICAOs = new Set(this.aircraftData.keys()); for (const [icao, marker] of this.aircraftMarkers) { if (!currentICAOs.has(icao)) { this.map.removeLayer(marker); this.aircraftMarkers.delete(icao); this.aircraftTrails.delete(icao); } } // Update aircraft markers for (const [icao, aircraft] of this.aircraftData) { if (aircraft.Latitude && aircraft.Longitude) { this.updateAircraftMarker(icao, aircraft); } } // Update source markers if (this.showSources) { this.updateSourceMarkers(); } // Update range circles if (this.showRange) { this.updateRangeCircles(); } } updateAircraftMarker(icao, aircraft) { const pos = [aircraft.Latitude, aircraft.Longitude]; if (this.aircraftMarkers.has(icao)) { // Update existing marker const marker = this.aircraftMarkers.get(icao); marker.setLatLng(pos); this.updateMarkerRotation(marker, aircraft.Track || 0); this.updatePopupContent(marker, aircraft); } else { // Create new marker const icon = this.createAircraftIcon(aircraft); const marker = L.marker(pos, { icon: icon, rotationAngle: aircraft.Track || 0 }).addTo(this.map); marker.bindPopup(this.createPopupContent(aircraft), { maxWidth: 450, className: 'aircraft-popup' }); this.aircraftMarkers.set(icao, marker); } // Update trails if (this.showTrails) { this.updateAircraftTrail(icao, pos); } } createAircraftIcon(aircraft) { const type = this.getAircraftType(aircraft); const color = this.getAircraftColor(type); const size = aircraft.OnGround ? 12 : 16; const svg = ` `; return L.divIcon({ html: svg, iconSize: [size * 2, size * 2], iconAnchor: [size, size], className: 'aircraft-marker' }); } 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'; } getAircraftColor(type) { const colors = { commercial: '#00ff88', cargo: '#ff8c00', military: '#ff4444', ga: '#ffff00', ground: '#888888' }; return colors[type] || colors.commercial; } updateMarkerRotation(marker, track) { if (marker._icon) { marker._icon.style.transform = `rotate(${track}deg)`; } } updateSourceMarkers() { // Remove markers for sources that no longer exist const currentSourceIds = new Set(this.sourcesData.keys()); for (const [id, marker] of this.sourceMarkers) { if (!currentSourceIds.has(id)) { this.map.removeLayer(marker); this.sourceMarkers.delete(id); } } // Update or create markers for current sources for (const [id, source] of this.sourcesData) { if (source.latitude && source.longitude) { if (this.sourceMarkers.has(id)) { // Update existing marker const marker = this.sourceMarkers.get(id); // Update marker style if status changed marker.setStyle({ radius: source.active ? 10 : 6, fillColor: source.active ? '#00d4ff' : '#666666', fillOpacity: 0.8 }); // Update popup content if it's open if (marker.isPopupOpen()) { marker.setPopupContent(this.createSourcePopupContent(source)); } } else { // Create new marker const marker = L.circleMarker([source.latitude, source.longitude], { radius: source.active ? 10 : 6, fillColor: source.active ? '#00d4ff' : '#666666', color: '#ffffff', weight: 2, fillOpacity: 0.8, className: 'source-marker' }).addTo(this.map); marker.bindPopup(this.createSourcePopupContent(source), { maxWidth: 300 }); this.sourceMarkers.set(id, marker); } } } this.updateSourcesLegend(); } updateRangeCircles() { // Clear existing circles this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); this.rangeCircles.clear(); // Add range circles for active sources for (const [id, source] of this.sourcesData) { if (source.active && source.latitude && source.longitude) { // Add multiple range circles (50km, 100km, 200km) const ranges = [50000, 100000, 200000]; ranges.forEach((range, index) => { const circle = L.circle([source.latitude, source.longitude], { radius: range, fillColor: 'transparent', color: '#00d4ff', weight: 1, opacity: 0.3 - (index * 0.1), dashArray: '5,5' }).addTo(this.map); this.rangeCircles.set(`${id}_${range}`, circle); }); } } } updateAircraftTrail(icao, pos) { if (!this.aircraftTrails.has(icao)) { this.aircraftTrails.set(icao, []); } const trail = this.aircraftTrails.get(icao); trail.push(pos); // Keep only last 50 positions if (trail.length > 50) { trail.shift(); } // Draw polyline const trailLine = L.polyline(trail, { color: '#00d4ff', weight: 2, opacity: 0.6 }).addTo(this.map); // Store reference for cleanup if (!this.aircraftTrails.get(icao).polyline) { this.aircraftTrails.get(icao).polyline = trailLine; } else { this.map.removeLayer(this.aircraftTrails.get(icao).polyline); this.aircraftTrails.get(icao).polyline = trailLine; } } // Popup Content createPopupContent(aircraft) { const type = this.getAircraftType(aircraft); const country = this.getCountryFromICAO(aircraft.ICAO24 ? aircraft.ICAO24.toString(16).toUpperCase() : ''); const flag = this.getCountryFlag(country); const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0; const altitudeM = altitude ? Math.round(altitude * 0.3048) : 0; const speedKmh = aircraft.GroundSpeed ? Math.round(aircraft.GroundSpeed * 1.852) : 0; const distance = this.calculateDistance(aircraft); const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; const sources = aircraft.Sources ? Object.keys(aircraft.Sources).map(id => { const source = this.sourcesData.get(id); const srcData = aircraft.Sources[id]; return ` ${source?.name || id} `; }).join('') : 'N/A'; return `
`; } updatePopupContent(marker, aircraft) { if (marker && marker.isPopupOpen()) { const newContent = this.createPopupContent(aircraft); marker.setPopupContent(newContent); } } createSourcePopupContent(source) { const aircraftCount = Array.from(this.aircraftData.values()) .filter(aircraft => aircraft.Sources && aircraft.Sources[source.id]).length; return `

${source.name}

ID: ${source.id}

Location: ${source.latitude.toFixed(4)}°, ${source.longitude.toFixed(4)}°

Status: ${source.active ? 'Active' : 'Inactive'}

Aircraft: ${aircraftCount}

Messages: ${source.messages || 0}

Last Seen: ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}

`; } updateSourcesLegend() { const legend = document.getElementById('sources-legend'); legend.innerHTML = ''; for (const [id, source] of this.sourcesData) { const item = document.createElement('div'); item.className = 'legend-item'; item.innerHTML = ` ${source.name} `; legend.appendChild(item); } } // Table Management updateAircraftTable() { const tbody = document.getElementById('aircraft-tbody'); 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.toString(16).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; 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 ? aircraft.ICAO24.toString(16).toUpperCase() : '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 row = document.createElement('tr'); row.innerHTML = ` ${icao} ${aircraft.Callsign || '-'} ${aircraft.Squawk || '-'} ${altitude ? `${altitude} ft` : '-'} ${aircraft.GroundSpeed || '-'} kt ${distance ? distance.toFixed(1) : '-'} km ${aircraft.Track || '-'}° ${sources} ${bestSignal ? bestSignal.toFixed(1) : '-'} ${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s `; row.addEventListener('click', () => { if (aircraft.Latitude && aircraft.Longitude) { this.switchView('map'); this.map.setView([aircraft.Latitude, aircraft.Longitude], 12); const marker = this.aircraftMarkers.get(icao); if (marker) { marker.openPopup(); } } }); return row; } getBestSignalFromSources(sources) { if (!sources) return null; let bestSignal = -999; for (const [id, data] of Object.entries(sources)) { if (data.SignalLevel > bestSignal) { bestSignal = data.SignalLevel; } } 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'; } updateSourceFilter() { const select = document.getElementById('source-filter'); const currentValue = select.value; // Clear options except "All Sources" select.innerHTML = ''; // Add source options for (const [id, source] of this.sourcesData) { 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?.toString(16) || '').localeCompare(b.Callsign || b.ICAO24?.toString(16) || ''); case 'icao': return (a.ICAO24?.toString(16) || '').localeCompare(b.ICAO24?.toString(16) || ''); 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; } }); } // Statistics updateStatistics() { document.getElementById('total-aircraft').textContent = this.aircraftData.size; document.getElementById('active-sources').textContent = Array.from(this.sourcesData.values()).filter(s => s.active).length; // Calculate max range let maxDistance = 0; for (const aircraft of this.aircraftData.values()) { const distance = this.calculateDistance(aircraft); if (distance && distance > maxDistance) { maxDistance = distance; } } document.getElementById('max-range').textContent = `${maxDistance.toFixed(1)} km`; // Update message rate const totalMessages = this.stats.total_messages || 0; document.getElementById('messages-sec').textContent = Math.round(totalMessages / 60); this.updateCharts(); } updateHeaderInfo() { document.getElementById('aircraft-count').textContent = `${this.aircraftData.size} aircraft`; document.getElementById('sources-count').textContent = `${this.sourcesData.size} sources`; this.updateClocks(); } // Event Listeners initializeEventListeners() { document.getElementById('search-input').addEventListener('input', () => this.updateAircraftTable()); document.getElementById('sort-select').addEventListener('change', () => this.updateAircraftTable()); document.getElementById('source-filter').addEventListener('change', () => this.updateAircraftTable()); } // Map Controls centerMapOnAircraft() { if (this.aircraftData.size === 0) return; const validAircraft = Array.from(this.aircraftData.values()) .filter(a => a.Latitude && a.Longitude); if (validAircraft.length === 0) return; const group = new L.featureGroup( validAircraft.map(a => L.marker([a.Latitude, a.Longitude])) ); this.map.fitBounds(group.getBounds().pad(0.1)); } toggleTrails() { this.showTrails = !this.showTrails; document.getElementById('toggle-trails').textContent = this.showTrails ? 'Hide Trails' : 'Show Trails'; if (!this.showTrails) { // Clear all trails this.aircraftTrails.forEach((trail, icao) => { if (trail.polyline) { this.map.removeLayer(trail.polyline); } }); this.aircraftTrails.clear(); } } toggleRangeCircles() { this.showRange = !this.showRange; document.getElementById('toggle-range').textContent = this.showRange ? 'Hide Range' : 'Show Range'; if (this.showRange) { this.updateRangeCircles(); } else { this.rangeCircles.forEach(circle => this.map.removeLayer(circle)); this.rangeCircles.clear(); } } toggleSources() { this.showSources = !this.showSources; document.getElementById('toggle-sources').textContent = this.showSources ? 'Hide Sources' : 'Show Sources'; if (this.showSources) { this.updateSourceMarkers(); } else { this.sourceMarkers.forEach(marker => this.map.removeLayer(marker)); this.sourceMarkers.clear(); } } // Coverage and Heatmap updateCoverageControls() { const select = document.getElementById('coverage-source'); select.innerHTML = ''; for (const [id, source] of this.sourcesData) { const option = document.createElement('option'); option.value = id; option.textContent = source.name; select.appendChild(option); } } async updateCoverageDisplay() { if (!this.selectedSource) return; try { const response = await fetch(`/api/coverage/${this.selectedSource}`); const data = await response.json(); // Clear existing coverage markers this.coverageMap.eachLayer(layer => { if (layer instanceof L.CircleMarker) { this.coverageMap.removeLayer(layer); } }); // Add coverage points data.points.forEach(point => { const intensity = Math.max(0, (point.signal + 50) / 50); // Normalize signal strength L.circleMarker([point.lat, point.lon], { radius: 3, fillColor: this.getSignalColor(point.signal), color: 'white', weight: 1, fillOpacity: intensity }).addTo(this.coverageMap); }); } catch (error) { console.error('Failed to load coverage data:', error); } } async toggleHeatmap() { if (!this.selectedSource) { alert('Please select a source first'); return; } if (this.heatmapLayer) { this.coverageMap.removeLayer(this.heatmapLayer); this.heatmapLayer = null; document.getElementById('toggle-heatmap').textContent = 'Show Heatmap'; } else { try { const response = await fetch(`/api/heatmap/${this.selectedSource}`); const data = await response.json(); // Create heatmap layer (simplified) // In a real implementation, you'd use a proper heatmap library this.createHeatmapOverlay(data); document.getElementById('toggle-heatmap').textContent = 'Hide Heatmap'; } catch (error) { console.error('Failed to load heatmap data:', error); } } } getSignalColor(signal) { if (signal > -10) return '#00ff88'; if (signal > -20) return '#ffff00'; if (signal > -30) return '#ff8c00'; return '#ff4444'; } // Charts (continued in next part due to length) initializeCharts() { this.charts.aircraft = new Chart(document.getElementById('aircraft-chart'), { type: 'line', data: { labels: [], datasets: [{ label: 'Aircraft Count', data: [], borderColor: '#00d4ff', backgroundColor: 'rgba(0, 212, 255, 0.1)', tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { beginAtZero: true, ticks: { color: '#888' } } } } }); // Add other charts... } updateCharts() { const now = new Date(); const timeLabel = now.toLocaleTimeString(); // Update aircraft count chart if (this.charts.aircraft) { const chart = this.charts.aircraft; chart.data.labels.push(timeLabel); chart.data.datasets[0].data.push(this.aircraftData.size); if (chart.data.labels.length > 20) { chart.data.labels.shift(); chart.data.datasets[0].data.shift(); } chart.update('none'); } } // 3D Radar (basic implementation) initialize3DRadar() { try { const container = document.getElementById('radar3d-container'); if (!container) return; // Create scene this.radar3d = { scene: new THREE.Scene(), camera: new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000), renderer: new THREE.WebGLRenderer({ alpha: true, antialias: true }), controls: null, aircraftMeshes: new Map() }; // Set up renderer this.radar3d.renderer.setSize(container.clientWidth, container.clientHeight); this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9); container.appendChild(this.radar3d.renderer.domElement); // Add lighting const ambientLight = new THREE.AmbientLight(0x404040, 0.6); this.radar3d.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 10, 5); this.radar3d.scene.add(directionalLight); // Set up camera this.radar3d.camera.position.set(0, 50, 50); this.radar3d.camera.lookAt(0, 0, 0); // Add controls this.radar3d.controls = new OrbitControls(this.radar3d.camera, this.radar3d.renderer.domElement); this.radar3d.controls.enableDamping = true; this.radar3d.controls.dampingFactor = 0.05; // Add ground plane const groundGeometry = new THREE.PlaneGeometry(200, 200); const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x2a4d3a, transparent: true, opacity: 0.5 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; this.radar3d.scene.add(ground); // Add grid const gridHelper = new THREE.GridHelper(200, 20, 0x44aa44, 0x44aa44); this.radar3d.scene.add(gridHelper); // Start render loop this.render3DRadar(); console.log('3D Radar initialized successfully'); } catch (error) { console.error('Failed to initialize 3D radar:', error); } } update3DRadar() { if (!this.radar3d || !this.radar3d.scene) return; try { // Update aircraft positions in 3D space this.aircraftData.forEach((aircraft, icao) => { if (aircraft.Latitude && aircraft.Longitude) { const key = icao.toString(); if (!this.radar3d.aircraftMeshes.has(key)) { // Create new aircraft mesh const geometry = new THREE.ConeGeometry(0.5, 2, 6); const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 }); const mesh = new THREE.Mesh(geometry, material); this.radar3d.aircraftMeshes.set(key, mesh); this.radar3d.scene.add(mesh); } const mesh = this.radar3d.aircraftMeshes.get(key); // Convert lat/lon to local coordinates (simplified) const x = (aircraft.Longitude - (-0.4600)) * 111320 * Math.cos(aircraft.Latitude * Math.PI / 180) / 1000; const z = -(aircraft.Latitude - 51.4700) * 111320 / 1000; const y = (aircraft.Altitude || 0) / 1000; // Convert feet to km for display mesh.position.set(x, y, z); // Orient mesh based on track if (aircraft.Track !== undefined) { mesh.rotation.y = -aircraft.Track * Math.PI / 180; } } }); // Remove old aircraft this.radar3d.aircraftMeshes.forEach((mesh, key) => { if (!this.aircraftData.has(parseInt(key, 16))) { this.radar3d.scene.remove(mesh); this.radar3d.aircraftMeshes.delete(key); } }); } catch (error) { console.error('Failed to update 3D radar:', error); } } render3DRadar() { if (!this.radar3d) return; requestAnimationFrame(() => this.render3DRadar()); if (this.radar3d.controls) { this.radar3d.controls.update(); } this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera); } // Utility functions updateConnectionStatus(status) { const statusEl = document.getElementById('connection-status'); statusEl.className = `connection-status ${status}`; statusEl.textContent = status === 'connected' ? 'Connected' : 'Disconnected'; } initializeClocks() { this.updateClocks(); } updateClocks() { const now = new Date(); const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000)); this.updateClock('utc', utcNow); this.updateClock('update', this.lastUpdateTime); } updateClock(prefix, time) { const hours = time.getUTCHours(); const minutes = time.getUTCMinutes(); 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)`; } 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; } getCountryFromICAO(icao) { if (!icao || icao.length < 6) return 'Unknown'; const prefix = icao[0]; const countryMap = { '4': 'Europe', 'A': 'United States', 'C': 'Canada', 'D': 'Germany', 'F': 'France', 'G': 'United Kingdom', 'I': 'Italy', 'J': 'Japan' }; return countryMap[prefix] || 'Unknown'; } getCountryFlag(country) { const flags = { 'United States': '🇺🇸', 'Canada': '🇨🇦', 'Germany': '🇩🇪', 'France': '🇫🇷', 'United Kingdom': '🇬🇧', 'Italy': '🇮🇹', 'Japan': '🇯🇵', 'Europe': '🇪🇺' }; return flags[country] || '🏳️'; } startPeriodicTasks() { // Update clocks every second setInterval(() => this.updateClocks(), 1000); // Periodic cleanup setInterval(() => { // Clean up old trail data, etc. }, 30000); } showError(message) { console.error(message); // Could implement toast notifications here } } // Initialize application when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.skyview = new SkyView(); });