skyview/assets/static/js/app.js
Ole-Morten Duesund 2ba2192044 fix: Clean up excessive debug logging in JavaScript console
- Add conditional verbose logging controlled by ?verbose URL param or localStorage
- Reduce frequent WebSocket message logging to verbose mode only
- Minimize aircraft position update logging to prevent console spam
- Make 3D radar initialization logging conditional on verbose flag
- Reduce WebSocket message counter frequency from every 10 to every 500 messages
- Preserve important error messages and connection status logging

Users can enable verbose logging with:
- URL parameter: http://localhost:8080?verbose
- localStorage: localStorage.setItem('skyview-verbose', 'true')

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-01 20:30:41 +02:00

855 lines
No EOL
34 KiB
JavaScript

// 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;
// Debug/verbose logging control
// Enable verbose logging with: ?verbose in URL or localStorage.setItem('skyview-verbose', 'true')
const urlParams = new URLSearchParams(window.location.search);
this.verbose = urlParams.has('verbose') || localStorage.getItem('skyview-verbose') === 'true';
// Set global verbose flag for other modules
window.skyviewVerbose = this.verbose;
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;
// Only log WebSocket messages in verbose mode
if (this.verbose) {
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
// Only log data updates in verbose mode
if (this.verbose) {
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();
}
// Only log completion messages in verbose mode
if (this.verbose) {
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() {
if (this.verbose) {
console.log('🚀 Starting 3D radar initialization');
}
try {
const container = document.getElementById('radar3d-container');
if (!container) {
console.error('❌ Container radar3d-container not found');
return;
}
if (this.verbose) {
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;
}
if (this.verbose) {
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;
}
if (this.verbose) {
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);
if (this.verbose) {
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);
}
if (this.verbose) {
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;
}
if (this.verbose) {
console.log('✅ WebGL context created');
}
this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9);
container.appendChild(this.radar3d.renderer.domElement);
if (this.verbose) {
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
if (this.verbose) {
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;
if (this.verbose) {
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);
if (this.verbose) {
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;
if (this.verbose) {
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();
});