skyview/assets/static/js/app.js

459 lines
17 KiB
JavaScript
Raw Normal View History

// 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';
});
}
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;
// Show trail for newly selected aircraft if enabled
if (this.selectedTrailEnabled) {
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);
}
}
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.skyview = new SkyView();
});