2025-08-24 00:19:00 +02:00
// Import Three.js modules
import * as THREE from 'three' ;
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' ;
2025-08-24 14:55:54 +02:00
// 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' ;
2025-08-31 19:43:58 +02:00
import { CallsignManager } from './modules/callsign-manager.js' ;
2025-08-24 14:55:54 +02:00
2025-08-24 00:19:00 +02:00
class SkyView {
constructor ( ) {
2025-08-24 14:55:54 +02:00
// Initialize managers
this . wsManager = null ;
this . aircraftManager = null ;
this . mapManager = null ;
this . uiManager = null ;
2025-08-31 19:43:58 +02:00
this . callsignManager = null ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
// 3D Radar
this . radar3d = null ;
2025-08-24 00:19:00 +02:00
// Charts
this . charts = { } ;
2025-08-24 16:24:46 +02:00
// Selected aircraft tracking
this . selectedAircraft = null ;
this . selectedTrailEnabled = false ;
2025-08-24 00:19:00 +02:00
this . init ( ) ;
}
async init ( ) {
try {
2025-08-24 14:55:54 +02:00
// Initialize UI manager first
this . uiManager = new UIManager ( ) ;
this . uiManager . initializeViews ( ) ;
this . uiManager . initializeEventListeners ( ) ;
2025-08-24 00:24:45 +02:00
2025-08-31 19:43:58 +02:00
// Initialize callsign manager for enriched callsign display
this . callsignManager = new CallsignManager ( ) ;
2025-08-24 14:55:54 +02:00
// Initialize map manager and get the main map
this . mapManager = new MapManager ( ) ;
const map = await this . mapManager . initializeMap ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-31 19:43:58 +02:00
// Initialize aircraft manager with the map and callsign manager
this . aircraftManager = new AircraftManager ( map , this . callsignManager ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 16:24:46 +02:00
// Set up selected aircraft trail callback
this . aircraftManager . setSelectedAircraftCallback ( ( icao ) => {
return this . selectedTrailEnabled && this . selectedAircraft === icao ;
} ) ;
2025-08-24 14:55:54 +02:00
// Initialize WebSocket with callbacks
this . wsManager = new WebSocketManager (
( message ) => this . handleWebSocketMessage ( message ) ,
( status ) => this . uiManager . updateConnectionStatus ( status )
) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
await this . wsManager . connect ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
// Initialize other components
this . initializeCharts ( ) ;
this . uiManager . updateClocks ( ) ;
2025-09-01 20:22:26 +02:00
// Flag to track 3D radar initialization
this . radar3dInitialized = false ;
// Add view change listener for 3D radar initialization
this . setupViewChangeListener ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
// Set up map controls
this . setupMapControls ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
// Set up aircraft selection listener
this . setupAircraftSelection ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
this . startPeriodicTasks ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
} catch ( error ) {
console . error ( 'Initialization failed:' , error ) ;
this . uiManager . showError ( 'Failed to initialize application' ) ;
2025-08-24 00:19:00 +02:00
}
}
2025-08-24 14:55:54 +02:00
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' ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
if ( centerMapBtn ) {
2025-08-24 16:24:46 +02:00
centerMapBtn . addEventListener ( 'click' , ( ) => {
this . aircraftManager . centerMapOnAircraft ( ( ) => this . mapManager . getSourcePositions ( ) ) ;
} ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
if ( resetMapBtn ) {
resetMapBtn . addEventListener ( 'click' , ( ) => this . mapManager . resetMap ( ) ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
if ( toggleTrailsBtn ) {
toggleTrailsBtn . addEventListener ( 'click' , ( ) => {
const showTrails = this . aircraftManager . toggleTrails ( ) ;
toggleTrailsBtn . textContent = showTrails ? 'Hide Trails' : 'Show Trails' ;
} ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
if ( toggleSourcesBtn ) {
toggleSourcesBtn . addEventListener ( 'click' , ( ) => {
const showSources = this . mapManager . toggleSources ( ) ;
toggleSourcesBtn . textContent = showSources ? 'Hide Sources' : 'Show Sources' ;
} ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 18:24:08 +02:00
// Setup collapsible sections
this . setupCollapsibleSections ( ) ;
2025-08-24 15:09:54 +02:00
const toggleDarkModeBtn = document . getElementById ( 'toggle-dark-mode' ) ;
if ( toggleDarkModeBtn ) {
toggleDarkModeBtn . addEventListener ( 'click' , ( ) => {
const isDarkMode = this . mapManager . toggleDarkMode ( ) ;
toggleDarkModeBtn . innerHTML = isDarkMode ? '☀️ Light Mode' : '🌙 Night Mode' ;
} ) ;
}
2025-08-24 14:55:54 +02:00
// Coverage controls
const toggleHeatmapBtn = document . getElementById ( 'toggle-heatmap' ) ;
const coverageSourceSelect = document . getElementById ( 'coverage-source' ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
if ( toggleHeatmapBtn ) {
toggleHeatmapBtn . addEventListener ( 'click' , async ( ) => {
const isActive = await this . mapManager . toggleHeatmap ( ) ;
toggleHeatmapBtn . textContent = isActive ? 'Hide Heatmap' : 'Show Heatmap' ;
} ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
if ( coverageSourceSelect ) {
coverageSourceSelect . addEventListener ( 'change' , ( e ) => {
this . mapManager . setSelectedSource ( e . target . value ) ;
this . mapManager . updateCoverageDisplay ( ) ;
} ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 16:24:46 +02:00
// 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 ) ;
}
} ) ;
}
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
setupAircraftSelection ( ) {
document . addEventListener ( 'aircraftSelected' , ( e ) => {
const { icao , aircraft } = e . detail ;
this . uiManager . switchView ( 'map-view' ) ;
2025-08-24 16:24:46 +02:00
// Hide trail for previously selected aircraft
if ( this . selectedAircraft && this . selectedTrailEnabled ) {
this . aircraftManager . hideAircraftTrail ( this . selectedAircraft ) ;
}
// Update selected aircraft
this . selectedAircraft = icao ;
2025-08-24 17:54:17 +02:00
// 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 ;
}
2025-08-24 16:24:46 +02:00
}
2025-08-24 17:54:17 +02:00
// Show trail for newly selected aircraft
this . aircraftManager . showAircraftTrail ( icao ) ;
2025-08-24 14:55:54 +02:00
// 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 ) ;
2025-08-24 00:19:00 +02:00
if ( marker ) {
marker . openPopup ( ) ;
}
}
} ) ;
}
2025-08-24 14:55:54 +02:00
handleWebSocketMessage ( message ) {
2025-08-25 10:14:03 +02:00
const aircraftCount = Object . keys ( message . data . aircraft || { } ) . length ;
console . debug ( ` WebSocket message: ${ message . type } , ${ aircraftCount } aircraft, timestamp: ${ message . timestamp } ` ) ;
2025-08-24 14:55:54 +02:00
switch ( message . type ) {
case 'initial_data' :
2025-08-25 10:14:03 +02:00
console . log ( ` Received initial data with ${ aircraftCount } aircraft ` ) ;
2025-08-24 14:55:54 +02:00
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 :
2025-08-25 10:14:03 +02:00
console . warn ( ` Unknown WebSocket message type: ${ message . type } ` ) ;
2025-08-24 00:19:00 +02:00
}
}
2025-08-24 14:55:54 +02:00
updateData ( data ) {
2025-08-25 10:14:03 +02:00
// Update all managers with new data - ORDER MATTERS
console . debug ( ` Updating data: ${ Object . keys ( data . aircraft || { } ).length} aircraft ` ) ;
2025-08-24 14:55:54 +02:00
this . uiManager . updateData ( data ) ;
this . aircraftManager . updateAircraftData ( data ) ;
this . mapManager . updateSourcesData ( data ) ;
2025-08-24 00:19:00 +02:00
2025-08-25 10:14:03 +02:00
// Update UI components - CRITICAL: updateMarkers must be called for track propagation
2025-08-24 14:55:54 +02:00
this . aircraftManager . updateMarkers ( ) ;
2025-08-25 10:14:03 +02:00
// Update map components that depend on aircraft data
this . mapManager . updateSourceMarkers ( ) ;
// Update UI tables and statistics
2025-08-24 14:55:54 +02:00
this . uiManager . updateAircraftTable ( ) ;
this . uiManager . updateStatistics ( ) ;
this . uiManager . updateHeaderInfo ( ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 16:24:46 +02:00
// Clear selected aircraft if it no longer exists
if ( this . selectedAircraft && ! this . aircraftManager . aircraftData . has ( this . selectedAircraft ) ) {
2025-08-25 10:14:03 +02:00
console . debug ( ` Selected aircraft ${ this . selectedAircraft } no longer exists, clearing selection ` ) ;
2025-08-24 16:24:46 +02:00
this . selectedAircraft = null ;
}
2025-08-24 14:55:54 +02:00
// Update coverage controls
this . mapManager . updateCoverageControls ( ) ;
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
if ( this . uiManager . currentView === 'radar3d-view' && this . radar3dInitialized ) {
2025-08-24 14:55:54 +02:00
this . update3DRadar ( ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-25 10:14:03 +02:00
console . debug ( ` Data update complete: ${ this . aircraftManager . aircraftMarkers . size } markers displayed ` ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
// View switching
async switchView ( viewId ) {
const actualViewId = this . uiManager . switchView ( viewId ) ;
2025-08-24 00:19:00 +02:00
2025-08-24 14:55:54 +02:00
// Handle view-specific initialization
const baseName = actualViewId . replace ( '-view' , '' ) ;
switch ( baseName ) {
case 'coverage' :
await this . mapManager . initializeCoverageMap ( ) ;
break ;
case 'radar3d' :
this . update3DRadar ( ) ;
break ;
2025-08-24 00:19:00 +02:00
}
}
2025-08-24 14:55:54 +02:00
// Charts
initializeCharts ( ) {
const aircraftChartCanvas = document . getElementById ( 'aircraft-chart' ) ;
if ( ! aircraftChartCanvas ) {
console . warn ( 'Aircraft chart canvas not found' ) ;
return ;
2025-08-24 00:19:00 +02:00
}
try {
2025-08-24 14:55:54 +02:00
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' }
}
}
2025-08-24 00:19:00 +02:00
}
} ) ;
} catch ( error ) {
2025-08-24 14:55:54 +02:00
console . warn ( 'Chart.js not available, skipping charts initialization' ) ;
2025-08-24 00:19:00 +02:00
}
}
updateCharts ( ) {
2025-08-24 14:55:54 +02:00
if ( ! this . charts . aircraft ) return ;
2025-08-24 00:19:00 +02:00
const now = new Date ( ) ;
const timeLabel = now . toLocaleTimeString ( ) ;
// Update aircraft count chart
2025-08-24 14:55:54 +02:00
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 ( ) ;
2025-08-24 00:19:00 +02:00
}
2025-08-24 14:55:54 +02:00
chart . update ( 'none' ) ;
2025-08-24 00:19:00 +02:00
}
2025-09-01 20:22:26 +02:00
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 ;
} ;
}
2025-08-24 00:19:00 +02:00
// 3D Radar (basic implementation)
initialize3DRadar ( ) {
2025-09-01 20:22:26 +02:00
console . log ( '🚀 Starting 3D radar initialization' ) ;
2025-08-24 00:19:00 +02:00
try {
const container = document . getElementById ( 'radar3d-container' ) ;
2025-09-01 20:22:26 +02:00
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 = '<div style="color: red; padding: 20px; text-align: center;">WebGL not supported. Please use a modern browser that supports WebGL.</div>' ;
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' ) ;
2025-08-24 00:19:00 +02:00
this . radar3d . renderer . setClearColor ( 0x0a0a0a , 0.9 ) ;
container . appendChild ( this . radar3d . renderer . domElement ) ;
2025-09-01 20:22:26 +02:00
console . log ( '✅ Renderer added to container' ) ;
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
// Add lighting - ensure the scene is visible
const ambientLight = new THREE . AmbientLight ( 0x404040 , 1.0 ) ; // Increased intensity
2025-08-24 00:19:00 +02:00
this . radar3d . scene . add ( ambientLight ) ;
2025-09-01 20:22:26 +02:00
const directionalLight = new THREE . DirectionalLight ( 0xffffff , 1.0 ) ; // Increased intensity
directionalLight . position . set ( 10 , 50 , 25 ) ;
2025-08-24 00:19:00 +02:00
this . radar3d . scene . add ( directionalLight ) ;
2025-09-01 20:22:26 +02:00
// 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 ) ;
2025-08-24 00:19:00 +02:00
// Set up camera
this . radar3d . camera . position . set ( 0 , 50 , 50 ) ;
this . radar3d . camera . lookAt ( 0 , 0 , 0 ) ;
// Add controls
2025-09-01 20:22:26 +02:00
console . log ( '🎮 Adding OrbitControls...' ) ;
if ( typeof OrbitControls === 'undefined' ) {
console . error ( '❌ OrbitControls not available' ) ;
return ;
}
2025-08-24 00:19:00 +02:00
this . radar3d . controls = new OrbitControls ( this . radar3d . camera , this . radar3d . renderer . domElement ) ;
this . radar3d . controls . enableDamping = true ;
this . radar3d . controls . dampingFactor = 0.05 ;
2025-09-01 20:22:26 +02:00
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
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
// Initialize 3D controls and fetch origin
this . initialize3DControls ( ) ;
this . fetch3DOrigin ( ) ;
// Add ground plane with better visibility
const groundGeometry = new THREE . PlaneGeometry ( 400 , 400 ) ;
2025-08-24 00:19:00 +02:00
const groundMaterial = new THREE . MeshLambertMaterial ( {
2025-09-01 20:22:26 +02:00
color : 0x1a3d2a ,
2025-08-24 00:19:00 +02:00
transparent : true ,
2025-09-01 20:22:26 +02:00
opacity : 0.8
2025-08-24 00:19:00 +02:00
} ) ;
const ground = new THREE . Mesh ( groundGeometry , groundMaterial ) ;
ground . rotation . x = - Math . PI / 2 ;
this . radar3d . scene . add ( ground ) ;
2025-09-01 20:22:26 +02:00
// Add grid with better visibility
const gridHelper = new THREE . GridHelper ( 400 , 40 , 0x44aa44 , 0x44aa44 ) ;
2025-08-24 00:19:00 +02:00
this . radar3d . scene . add ( gridHelper ) ;
2025-09-01 20:22:26 +02:00
// 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
} ) ;
2025-08-24 00:19:00 +02:00
// Start render loop
this . render3DRadar ( ) ;
} catch ( error ) {
console . error ( 'Failed to initialize 3D radar:' , error ) ;
}
}
update3DRadar ( ) {
2025-08-24 14:55:54 +02:00
if ( ! this . radar3d || ! this . radar3d . scene || ! this . aircraftManager ) return ;
2025-08-24 00:19:00 +02:00
try {
2025-09-01 20:22:26 +02:00
// 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
2025-08-24 00:19:00 +02:00
// Update aircraft positions in 3D space
2025-08-24 14:55:54 +02:00
this . aircraftManager . aircraftData . forEach ( ( aircraft , icao ) => {
2025-08-24 00:19:00 +02:00
if ( aircraft . Latitude && aircraft . Longitude ) {
const key = icao . toString ( ) ;
2025-09-01 20:22:26 +02:00
// Calculate distance from origin to filter by range
const distance = this . calculateDistance (
originLat , originLon ,
aircraft . Latitude , aircraft . Longitude
) ;
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
// Check if aircraft is within range
const withinRange = distance <= currentRange ;
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
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 ;
}
2025-08-24 00:19:00 +02:00
2025-09-01 20:22:26 +02:00
// 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 ;
}
2025-08-24 00:19:00 +02:00
}
}
} ) ;
// Remove old aircraft
this . radar3d . aircraftMeshes . forEach ( ( mesh , key ) => {
2025-08-24 14:55:54 +02:00
if ( ! this . aircraftManager . aircraftData . has ( key ) ) {
2025-08-24 00:19:00 +02:00
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 ) ;
}
2025-09-01 20:22:26 +02:00
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 ;
}
2025-09-01 10:29:35 +02:00
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 ) ;
}
}
}
} ) ;
}
2025-08-24 00:19:00 +02:00
startPeriodicTasks ( ) {
// Update clocks every second
2025-08-24 14:55:54 +02:00
setInterval ( ( ) => this . uiManager . updateClocks ( ) , 1000 ) ;
2025-09-01 10:29:35 +02:00
// 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 ) ;
2025-08-24 14:55:54 +02:00
// Update charts every 10 seconds
setInterval ( ( ) => this . updateCharts ( ) , 10000 ) ;
2025-08-24 00:19:00 +02:00
// Periodic cleanup
setInterval ( ( ) => {
// Clean up old trail data, etc.
} , 30000 ) ;
}
2025-08-24 18:24:08 +02:00
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' ) ;
}
}
}
2025-08-24 00:19:00 +02:00
}
// Initialize application when DOM is ready
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
window . skyview = new SkyView ( ) ;
} ) ;