// 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'; import { CallsignManager } from './modules/callsign-manager.js'; class SkyView { constructor() { // Initialize managers this.wsManager = null; this.aircraftManager = null; this.mapManager = null; this.uiManager = null; this.callsignManager = 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 callsign manager for enriched callsign display this.callsignManager = new CallsignManager(); // Initialize map manager and get the main map this.mapManager = new MapManager(); const map = await this.mapManager.initializeMap(); // Initialize aircraft manager with the map and callsign manager this.aircraftManager = new AircraftManager(map, this.callsignManager); // 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(); // Flag to track 3D radar initialization this.radar3dInitialized = false; // Add view change listener for 3D radar initialization this.setupViewChangeListener(); // 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) { const aircraftCount = Object.keys(message.data.aircraft || {}).length; console.debug(`WebSocket message: ${message.type}, ${aircraftCount} aircraft, timestamp: ${message.timestamp}`); switch (message.type) { case 'initial_data': console.log(`Received initial data with ${aircraftCount} aircraft`); 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: console.warn(`Unknown WebSocket message type: ${message.type}`); } } updateData(data) { // Update all managers with new data - ORDER MATTERS console.debug(`Updating data: ${Object.keys(data.aircraft || {}).length} aircraft`); this.uiManager.updateData(data); this.aircraftManager.updateAircraftData(data); this.mapManager.updateSourcesData(data); // Update UI components - CRITICAL: updateMarkers must be called for track propagation this.aircraftManager.updateMarkers(); // Update map components that depend on aircraft data this.mapManager.updateSourceMarkers(); // Update UI tables and statistics 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)) { console.debug(`Selected aircraft ${this.selectedAircraft} no longer exists, clearing selection`); this.selectedAircraft = null; } // Update coverage controls this.mapManager.updateCoverageControls(); if (this.uiManager.currentView === 'radar3d-view' && this.radar3dInitialized) { this.update3DRadar(); } console.debug(`Data update complete: ${this.aircraftManager.aircraftMarkers.size} markers displayed`); } // 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'); } setupViewChangeListener() { // Override the ui manager's switchView to handle 3D radar initialization const originalSwitchView = this.uiManager.switchView.bind(this.uiManager); this.uiManager.switchView = (viewId) => { const result = originalSwitchView(viewId); // Initialize 3D radar when switching to 3D radar view if (viewId === 'radar3d-view' && !this.radar3dInitialized) { setTimeout(() => { this.initialize3DRadar(); this.radar3dInitialized = true; }, 100); // Small delay to ensure the view is visible } return result; }; } // 3D Radar (basic implementation) initialize3DRadar() { console.log('🚀 Starting 3D radar initialization'); try { const container = document.getElementById('radar3d-container'); if (!container) { console.error('❌ Container radar3d-container not found'); return; } console.log('✅ Container found:', container, 'Size:', container.clientWidth, 'x', container.clientHeight); // Check if container is visible const containerStyles = window.getComputedStyle(container); console.log('📋 Container styles:', { display: containerStyles.display, visibility: containerStyles.visibility, width: containerStyles.width, height: containerStyles.height }); // Check if Three.js is available if (typeof THREE === 'undefined') { console.error('❌ Three.js is not available'); return; } console.log('✅ Three.js available, version:', THREE.REVISION); // Quick WebGL test const testCanvas = document.createElement('canvas'); const gl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); if (!gl) { console.error('❌ WebGL is not supported in this browser'); container.innerHTML = '
WebGL not supported. Please use a modern browser that supports WebGL.
'; return; } console.log('✅ WebGL is supported'); // Check container dimensions first if (container.clientWidth === 0 || container.clientHeight === 0) { console.error('❌ Container has zero dimensions:', container.clientWidth, 'x', container.clientHeight); // Force minimum size for initialization const width = Math.max(container.clientWidth, 800); const height = Math.max(container.clientHeight, 600); console.log('📐 Using forced dimensions:', width, 'x', height); // Create scene with forced dimensions this.radar3d = { scene: new THREE.Scene(), camera: new THREE.PerspectiveCamera(75, width / height, 0.1, 1000), renderer: new THREE.WebGLRenderer({ alpha: true, antialias: true }), controls: null, aircraftMeshes: new Map() }; // Set up renderer with forced dimensions this.radar3d.renderer.setSize(width, height); } else { // Create scene with actual container dimensions 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); } console.log('✅ Three.js objects created:', { scene: this.radar3d.scene, camera: this.radar3d.camera, renderer: this.radar3d.renderer }); // Check WebGL context const rendererGL = this.radar3d.renderer.getContext(); if (!rendererGL) { console.error('❌ Failed to get WebGL context'); return; } console.log('✅ WebGL context created'); this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9); container.appendChild(this.radar3d.renderer.domElement); console.log('✅ Renderer added to container'); // Add lighting - ensure the scene is visible const ambientLight = new THREE.AmbientLight(0x404040, 1.0); // Increased intensity this.radar3d.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0); // Increased intensity directionalLight.position.set(10, 50, 25); this.radar3d.scene.add(directionalLight); // Add hemisphere light for better illumination const hemiLight = new THREE.HemisphereLight(0x0000ff, 0x00ff00, 0.6); hemiLight.position.set(0, 100, 0); this.radar3d.scene.add(hemiLight); // Set up camera this.radar3d.camera.position.set(0, 50, 50); this.radar3d.camera.lookAt(0, 0, 0); // Add controls console.log('🎮 Adding OrbitControls...'); if (typeof OrbitControls === 'undefined') { console.error('❌ OrbitControls not available'); return; } this.radar3d.controls = new OrbitControls(this.radar3d.camera, this.radar3d.renderer.domElement); this.radar3d.controls.enableDamping = true; this.radar3d.controls.dampingFactor = 0.05; console.log('✅ OrbitControls added'); // Store default camera position for reset functionality this.radar3d.defaultPosition = { x: 0, y: 50, z: 50 }; this.radar3d.defaultTarget = { x: 0, y: 0, z: 0 }; // Auto-rotate state this.radar3d.autoRotate = false; // Origin coordinates (will be fetched from server) this.radar3d.origin = { latitude: 59.9081, longitude: 10.8015 }; // fallback // Initialize 3D controls and fetch origin this.initialize3DControls(); this.fetch3DOrigin(); // Add ground plane with better visibility const groundGeometry = new THREE.PlaneGeometry(400, 400); const groundMaterial = new THREE.MeshLambertMaterial({ color: 0x1a3d2a, transparent: true, opacity: 0.8 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; this.radar3d.scene.add(ground); // Add grid with better visibility const gridHelper = new THREE.GridHelper(400, 40, 0x44aa44, 0x44aa44); this.radar3d.scene.add(gridHelper); // Add some reference objects to help with debugging const cubeGeometry = new THREE.BoxGeometry(2, 2, 2); const cubeMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 }); const cube = new THREE.Mesh(cubeGeometry, cubeMaterial); cube.position.set(10, 1, 10); this.radar3d.scene.add(cube); console.log('3D Radar initialized successfully', { scene: this.radar3d.scene, camera: this.radar3d.camera, renderer: this.radar3d.renderer }); // 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 { // Origin coordinates for distance calculation const originLat = this.radar3d.origin.latitude; const originLon = this.radar3d.origin.longitude; const currentRange = this.radar3d.range || 100; // Default to 100km if not set // Update aircraft positions in 3D space this.aircraftManager.aircraftData.forEach((aircraft, icao) => { if (aircraft.Latitude && aircraft.Longitude) { const key = icao.toString(); // Calculate distance from origin to filter by range const distance = this.calculateDistance( originLat, originLon, aircraft.Latitude, aircraft.Longitude ); // Check if aircraft is within range const withinRange = distance <= currentRange; if (!this.radar3d.aircraftMeshes.has(key)) { // Create new aircraft mesh only if within range if (withinRange) { 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); } } else { // Aircraft mesh exists, check if it should be visible const mesh = this.radar3d.aircraftMeshes.get(key); mesh.visible = withinRange; } // Update position if aircraft is within range if (withinRange) { const mesh = this.radar3d.aircraftMeshes.get(key); // Convert lat/lon to local coordinates (simplified) const x = (aircraft.Longitude - originLon) * 111320 * Math.cos(aircraft.Latitude * Math.PI / 180) / 1000; const z = -(aircraft.Latitude - originLat) * 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); } initialize3DControls() { // Enable and initialize the Reset View button const resetButton = document.getElementById('radar3d-reset'); if (resetButton) { resetButton.disabled = false; resetButton.addEventListener('click', () => this.reset3DView()); } // Enable and initialize the Auto Rotate toggle button const autoRotateButton = document.getElementById('radar3d-auto-rotate'); if (autoRotateButton) { autoRotateButton.disabled = false; autoRotateButton.addEventListener('click', () => this.toggle3DAutoRotate()); } // Enable and initialize the Range slider const rangeSlider = document.getElementById('radar3d-range'); const rangeValue = document.getElementById('radar3d-range-value'); if (rangeSlider && rangeValue) { rangeSlider.disabled = false; this.radar3d.range = parseInt(rangeSlider.value); // Store current range rangeSlider.addEventListener('input', (e) => { this.radar3d.range = parseInt(e.target.value); rangeValue.textContent = this.radar3d.range; this.update3DRadarRange(); }); } } reset3DView() { if (!this.radar3d || !this.radar3d.controls) return; // Reset camera to default position this.radar3d.camera.position.set( this.radar3d.defaultPosition.x, this.radar3d.defaultPosition.y, this.radar3d.defaultPosition.z ); // Reset camera target this.radar3d.controls.target.set( this.radar3d.defaultTarget.x, this.radar3d.defaultTarget.y, this.radar3d.defaultTarget.z ); // Update controls to apply changes smoothly this.radar3d.controls.update(); } toggle3DAutoRotate() { if (!this.radar3d || !this.radar3d.controls) return; // Toggle auto-rotate state this.radar3d.autoRotate = !this.radar3d.autoRotate; this.radar3d.controls.autoRotate = this.radar3d.autoRotate; // Update button text to reflect current state const autoRotateButton = document.getElementById('radar3d-auto-rotate'); if (autoRotateButton) { autoRotateButton.textContent = this.radar3d.autoRotate ? 'Stop Auto Rotate' : 'Auto Rotate'; autoRotateButton.classList.toggle('active', this.radar3d.autoRotate); } } async fetch3DOrigin() { try { const response = await fetch('/api/origin'); if (response.ok) { const origin = await response.json(); this.radar3d.origin = origin; console.log('3D Radar origin set to:', origin); } } catch (error) { console.warn('Failed to fetch origin, using fallback:', error); } } update3DRadarRange() { // Simply trigger a full update of the 3D radar with new range filter this.update3DRadar(); } // Calculate distance between two lat/lon points in kilometers calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // Earth's radius in kilometers 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; } updateOpenPopupAges() { // Find any open aircraft popups and update their age displays if (!this.aircraftManager) return; this.aircraftManager.aircraftMarkers.forEach((marker, icao) => { if (marker.isPopupOpen()) { const aircraft = this.aircraftManager.aircraftData.get(icao); if (aircraft) { // Refresh the popup content with current age marker.setPopupContent(this.aircraftManager.createPopupContent(aircraft)); // Re-enhance callsign display for the updated popup const popupElement = marker.getPopup().getElement(); if (popupElement) { this.aircraftManager.enhanceCallsignDisplay(popupElement); } } } }); } startPeriodicTasks() { // Update clocks every second setInterval(() => this.uiManager.updateClocks(), 1000); // Update aircraft ages and refresh displays every second setInterval(() => { // Update aircraft table to show current ages this.uiManager.updateAircraftTable(); // Update any open aircraft popups with current ages this.updateOpenPopupAges(); }, 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(); });