// Import Three.js modules import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Import our modular components import { WebSocketManager } from './modules/websocket.js?v=2'; import { AircraftManager } from './modules/aircraft-manager.js?v=2'; import { MapManager } from './modules/map-manager.js?v=2'; import { UIManager } from './modules/ui-manager.js?v=2'; class SkyView { constructor() { // Initialize managers this.wsManager = null; this.aircraftManager = null; this.mapManager = null; this.uiManager = null; // 3D Radar this.radar3d = null; // Charts this.charts = {}; // Selected aircraft tracking this.selectedAircraft = null; this.selectedTrailEnabled = false; this.init(); } async init() { try { // Initialize UI manager first this.uiManager = new UIManager(); this.uiManager.initializeViews(); this.uiManager.initializeEventListeners(); // Initialize map manager and get the main map this.mapManager = new MapManager(); const map = await this.mapManager.initializeMap(); // Initialize aircraft manager with the map this.aircraftManager = new AircraftManager(map); // Set up selected aircraft trail callback this.aircraftManager.setSelectedAircraftCallback((icao) => { return this.selectedTrailEnabled && this.selectedAircraft === icao; }); // Initialize WebSocket with callbacks this.wsManager = new WebSocketManager( (message) => this.handleWebSocketMessage(message), (status) => this.uiManager.updateConnectionStatus(status) ); await this.wsManager.connect(); // Initialize other components this.initializeCharts(); this.uiManager.updateClocks(); this.initialize3DRadar(); // Set up map controls this.setupMapControls(); // Set up aircraft selection listener this.setupAircraftSelection(); this.startPeriodicTasks(); } catch (error) { console.error('Initialization failed:', error); this.uiManager.showError('Failed to initialize application'); } } setupMapControls() { const centerMapBtn = document.getElementById('center-map'); const resetMapBtn = document.getElementById('reset-map'); const toggleTrailsBtn = document.getElementById('toggle-trails'); const toggleSourcesBtn = document.getElementById('toggle-sources'); if (centerMapBtn) { centerMapBtn.addEventListener('click', () => { this.aircraftManager.centerMapOnAircraft(() => this.mapManager.getSourcePositions()); }); } if (resetMapBtn) { resetMapBtn.addEventListener('click', () => this.mapManager.resetMap()); } if (toggleTrailsBtn) { toggleTrailsBtn.addEventListener('click', () => { const showTrails = this.aircraftManager.toggleTrails(); toggleTrailsBtn.textContent = showTrails ? 'Hide Trails' : 'Show Trails'; }); } if (toggleSourcesBtn) { toggleSourcesBtn.addEventListener('click', () => { const showSources = this.mapManager.toggleSources(); toggleSourcesBtn.textContent = showSources ? 'Hide Sources' : 'Show Sources'; }); } // Setup collapsible sections this.setupCollapsibleSections(); const toggleDarkModeBtn = document.getElementById('toggle-dark-mode'); if (toggleDarkModeBtn) { toggleDarkModeBtn.addEventListener('click', () => { const isDarkMode = this.mapManager.toggleDarkMode(); toggleDarkModeBtn.innerHTML = isDarkMode ? '☀️ Light Mode' : '🌙 Night Mode'; }); } // Coverage controls const toggleHeatmapBtn = document.getElementById('toggle-heatmap'); const coverageSourceSelect = document.getElementById('coverage-source'); if (toggleHeatmapBtn) { toggleHeatmapBtn.addEventListener('click', async () => { const isActive = await this.mapManager.toggleHeatmap(); toggleHeatmapBtn.textContent = isActive ? 'Hide Heatmap' : 'Show Heatmap'; }); } if (coverageSourceSelect) { coverageSourceSelect.addEventListener('change', (e) => { this.mapManager.setSelectedSource(e.target.value); this.mapManager.updateCoverageDisplay(); }); } // Display option checkboxes const sitePositionsCheckbox = document.getElementById('show-site-positions'); const rangeRingsCheckbox = document.getElementById('show-range-rings'); const selectedTrailCheckbox = document.getElementById('show-selected-trail'); if (sitePositionsCheckbox) { sitePositionsCheckbox.addEventListener('change', (e) => { if (e.target.checked) { this.mapManager.showSources = true; this.mapManager.updateSourceMarkers(); } else { this.mapManager.showSources = false; this.mapManager.sourceMarkers.forEach(marker => this.mapManager.map.removeLayer(marker)); this.mapManager.sourceMarkers.clear(); } }); } if (rangeRingsCheckbox) { rangeRingsCheckbox.addEventListener('change', (e) => { if (e.target.checked) { this.mapManager.showRange = true; this.mapManager.updateRangeCircles(); } else { this.mapManager.showRange = false; this.mapManager.rangeCircles.forEach(circle => this.mapManager.map.removeLayer(circle)); this.mapManager.rangeCircles.clear(); } }); } if (selectedTrailCheckbox) { selectedTrailCheckbox.addEventListener('change', (e) => { this.selectedTrailEnabled = e.target.checked; if (!e.target.checked && this.selectedAircraft) { // Hide currently selected aircraft trail this.aircraftManager.hideAircraftTrail(this.selectedAircraft); } else if (e.target.checked && this.selectedAircraft) { // Show currently selected aircraft trail this.aircraftManager.showAircraftTrail(this.selectedAircraft); } }); } } setupAircraftSelection() { document.addEventListener('aircraftSelected', (e) => { const { icao, aircraft } = e.detail; this.uiManager.switchView('map-view'); // Hide trail for previously selected aircraft if (this.selectedAircraft && this.selectedTrailEnabled) { this.aircraftManager.hideAircraftTrail(this.selectedAircraft); } // Update selected aircraft this.selectedAircraft = icao; // Automatically enable selected aircraft trail when an aircraft is selected if (!this.selectedTrailEnabled) { this.selectedTrailEnabled = true; const selectedTrailCheckbox = document.getElementById('show-selected-trail'); if (selectedTrailCheckbox) { selectedTrailCheckbox.checked = true; } } // Show trail for newly selected aircraft this.aircraftManager.showAircraftTrail(icao); // DON'T change map view - just open popup like Leaflet expects if (this.mapManager.map && aircraft.Latitude && aircraft.Longitude) { const marker = this.aircraftManager.aircraftMarkers.get(icao); if (marker) { marker.openPopup(); } } }); } handleWebSocketMessage(message) { switch (message.type) { case 'initial_data': this.updateData(message.data); // Setup source markers only on initial data load this.mapManager.updateSourceMarkers(); break; case 'aircraft_update': this.updateData(message.data); break; default: } } updateData(data) { // Update all managers with new data this.uiManager.updateData(data); this.aircraftManager.updateAircraftData(data); this.mapManager.updateSourcesData(data); // Update UI components this.aircraftManager.updateMarkers(); this.uiManager.updateAircraftTable(); this.uiManager.updateStatistics(); this.uiManager.updateHeaderInfo(); // Clear selected aircraft if it no longer exists if (this.selectedAircraft && !this.aircraftManager.aircraftData.has(this.selectedAircraft)) { this.selectedAircraft = null; } // Update coverage controls this.mapManager.updateCoverageControls(); if (this.uiManager.currentView === 'radar3d-view') { this.update3DRadar(); } } // View switching async switchView(viewId) { const actualViewId = this.uiManager.switchView(viewId); // Handle view-specific initialization const baseName = actualViewId.replace('-view', ''); switch (baseName) { case 'coverage': await this.mapManager.initializeCoverageMap(); break; case 'radar3d': this.update3DRadar(); break; } } // Charts initializeCharts() { const aircraftChartCanvas = document.getElementById('aircraft-chart'); if (!aircraftChartCanvas) { console.warn('Aircraft chart canvas not found'); return; } try { this.charts.aircraft = new Chart(aircraftChartCanvas, { 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' } } } } }); } catch (error) { console.warn('Chart.js not available, skipping charts initialization'); } } updateCharts() { if (!this.charts.aircraft) return; const now = new Date(); const timeLabel = now.toLocaleTimeString(); // Update aircraft count chart const chart = this.charts.aircraft; chart.data.labels.push(timeLabel); chart.data.datasets[0].data.push(this.aircraftManager.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(); } catch (error) { console.error('Failed to initialize 3D radar:', error); } } update3DRadar() { if (!this.radar3d || !this.radar3d.scene || !this.aircraftManager) return; try { // Update aircraft positions in 3D space this.aircraftManager.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.aircraftManager.aircraftData.has(key)) { 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); } startPeriodicTasks() { // Update clocks every second setInterval(() => this.uiManager.updateClocks(), 1000); // Update charts every 10 seconds setInterval(() => this.updateCharts(), 10000); // Periodic cleanup setInterval(() => { // Clean up old trail data, etc. }, 30000); } setupCollapsibleSections() { // Setup Display Options collapsible const displayHeader = document.getElementById('display-options-header'); const displayContent = document.getElementById('display-options-content'); if (displayHeader && displayContent) { displayHeader.addEventListener('click', () => { const isCollapsed = displayContent.classList.contains('collapsed'); if (isCollapsed) { // Expand displayContent.classList.remove('collapsed'); displayHeader.classList.remove('collapsed'); } else { // Collapse displayContent.classList.add('collapsed'); displayHeader.classList.add('collapsed'); } // Save state to localStorage localStorage.setItem('displayOptionsCollapsed', !isCollapsed); }); // Restore saved state (default to collapsed) const savedState = localStorage.getItem('displayOptionsCollapsed'); const shouldCollapse = savedState === null ? true : savedState === 'true'; if (shouldCollapse) { displayContent.classList.add('collapsed'); displayHeader.classList.add('collapsed'); } else { displayContent.classList.remove('collapsed'); displayHeader.classList.remove('collapsed'); } } } } // Initialize application when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.skyview = new SkyView(); });