feat: Implement 3D Radar View Controls - resolves #9
- Add Reset View button functionality to reset camera to default position - Implement Auto Rotate toggle with visual feedback and button text updates - Add Range slider (10-500km) with real-time aircraft distance filtering - Use server-provided origin coordinates via /api/origin API endpoint - Implement Haversine formula for accurate geographic distance calculations - Add deferred initialization to prevent black screen issue - Enhanced lighting and ground plane visibility for better 3D orientation - Add comprehensive debugging and error handling for WebGL/Three.js - Style 3D radar controls with proper CSS for consistent UI - Remove construction notice as controls are now fully functional 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4c9215b535
commit
a2cf9d8fdb
3 changed files with 352 additions and 44 deletions
|
|
@ -858,3 +858,59 @@ body {
|
|||
.squawk-standard:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* 3D Radar Styles */
|
||||
#radar3d-view {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#radar3d-container {
|
||||
flex: 1;
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
background: #0a0a0a;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.radar3d-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #2d2d2d;
|
||||
border-bottom: 1px solid #404040;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radar3d-controls button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #404040;
|
||||
color: white;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radar3d-controls button:hover:not(:disabled) {
|
||||
background: #505050;
|
||||
}
|
||||
|
||||
.radar3d-controls button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.radar3d-controls button.active {
|
||||
background: #00a8ff;
|
||||
border-color: #0088cc;
|
||||
}
|
||||
|
||||
.radar3d-controls label {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.radar3d-controls input[type="range"] {
|
||||
width: 150px;
|
||||
}
|
||||
|
|
@ -411,7 +411,6 @@
|
|||
<!-- 3D Radar View -->
|
||||
<div id="radar3d-view" class="view">
|
||||
<div class="radar3d-controls">
|
||||
<div class="construction-notice">🚧 3D Controls Under Construction</div>
|
||||
<button id="radar3d-reset" disabled>Reset View</button>
|
||||
<button id="radar3d-auto-rotate" disabled>Auto Rotate</button>
|
||||
<label>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,12 @@ class SkyView {
|
|||
// Initialize other components
|
||||
this.initializeCharts();
|
||||
this.uiManager.updateClocks();
|
||||
this.initialize3DRadar();
|
||||
|
||||
// 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();
|
||||
|
|
@ -268,7 +273,7 @@ class SkyView {
|
|||
// Update coverage controls
|
||||
this.mapManager.updateCoverageControls();
|
||||
|
||||
if (this.uiManager.currentView === 'radar3d-view') {
|
||||
if (this.uiManager.currentView === 'radar3d-view' && this.radar3dInitialized) {
|
||||
this.update3DRadar();
|
||||
}
|
||||
|
||||
|
|
@ -351,58 +356,186 @@ class SkyView {
|
|||
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) return;
|
||||
if (!container) {
|
||||
console.error('❌ Container radar3d-container not found');
|
||||
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()
|
||||
};
|
||||
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');
|
||||
|
||||
// Set up renderer
|
||||
this.radar3d.renderer.setSize(container.clientWidth, container.clientHeight);
|
||||
this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9);
|
||||
container.appendChild(this.radar3d.renderer.domElement);
|
||||
console.log('✅ Renderer added to container');
|
||||
|
||||
// Add lighting
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
|
||||
// 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, 0.8);
|
||||
directionalLight.position.set(10, 10, 5);
|
||||
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');
|
||||
|
||||
// Add ground plane
|
||||
const groundGeometry = new THREE.PlaneGeometry(200, 200);
|
||||
// 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: 0x2a4d3a,
|
||||
color: 0x1a3d2a,
|
||||
transparent: true,
|
||||
opacity: 0.5
|
||||
opacity: 0.8
|
||||
});
|
||||
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);
|
||||
// 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();
|
||||
|
||||
|
|
@ -415,32 +548,55 @@ class SkyView {
|
|||
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
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
const mesh = this.radar3d.aircraftMeshes.get(key);
|
||||
// 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 - (-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
|
||||
// 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);
|
||||
mesh.position.set(x, y, z);
|
||||
|
||||
// Orient mesh based on track
|
||||
if (aircraft.Track !== undefined) {
|
||||
mesh.rotation.y = -aircraft.Track * Math.PI / 180;
|
||||
// Orient mesh based on track
|
||||
if (aircraft.Track !== undefined) {
|
||||
mesh.rotation.y = -aircraft.Track * Math.PI / 180;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -469,6 +625,103 @@ class SkyView {
|
|||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue