feat: Complete 3D radar enhancements with labels, interactions, and trails
Implements comprehensive 3D radar view enhancements: - Fixed map tile alignment using proper Web Mercator projection calculations - Added dynamic aircraft labels showing callsign, altitude, and speed - Implemented click interactions with raycasting for aircraft selection - Created flight trail visualization with toggle functionality - Enhanced visual feedback with selection highlighting - Improved tile positioning with precise geographic calculations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2ba2192044
commit
fec2f111a1
2 changed files with 553 additions and 29 deletions
|
|
@ -413,6 +413,7 @@
|
|||
<div class="radar3d-controls">
|
||||
<button id="radar3d-reset" disabled>Reset View</button>
|
||||
<button id="radar3d-auto-rotate" disabled>Auto Rotate</button>
|
||||
<button id="radar3d-trails" disabled>Show Trails</button>
|
||||
<label>
|
||||
<input type="range" id="radar3d-range" min="10" max="500" value="100" disabled>
|
||||
Range: <span id="radar3d-range-value">100</span> km
|
||||
|
|
|
|||
|
|
@ -9,6 +9,184 @@ 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';
|
||||
|
||||
// Tile Management for 3D Map Base
|
||||
class TileManager {
|
||||
constructor(scene, origin) {
|
||||
this.scene = scene;
|
||||
this.origin = origin;
|
||||
this.loadedTiles = new Map();
|
||||
this.zoom = 12; // OSM zoom level
|
||||
|
||||
// Calculate tile bounds around origin
|
||||
this.originTileX = this.lonToTileX(origin.longitude, this.zoom);
|
||||
this.originTileY = this.latToTileY(origin.latitude, this.zoom);
|
||||
|
||||
// Calculate actual tile size based on Web Mercator projection at this latitude
|
||||
this.tileSize = this.calculateTileSize(origin.latitude, this.zoom);
|
||||
}
|
||||
|
||||
// Calculate the ground size of a tile at given latitude and zoom
|
||||
calculateTileSize(latitude, zoom) {
|
||||
// Earth's circumference at equator in km
|
||||
const earthCircumference = 40075.016686;
|
||||
// Circumference at given latitude
|
||||
const latCircumference = earthCircumference * Math.cos(latitude * Math.PI / 180);
|
||||
// Size of one tile at this latitude and zoom level
|
||||
return latCircumference / Math.pow(2, zoom);
|
||||
}
|
||||
|
||||
// Convert longitude to OSM tile X coordinate
|
||||
lonToTileX(lon, zoom) {
|
||||
return Math.floor((lon + 180) / 360 * Math.pow(2, zoom));
|
||||
}
|
||||
|
||||
// Convert latitude to OSM tile Y coordinate
|
||||
latToTileY(lat, zoom) {
|
||||
return Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom));
|
||||
}
|
||||
|
||||
// Convert tile coordinates to world position relative to origin
|
||||
tileToWorldPosition(tileX, tileY) {
|
||||
// Use precise positioning based on actual Web Mercator coordinates
|
||||
const originLon = this.tileXToLon(this.originTileX, this.zoom);
|
||||
const originLat = this.tileYToLat(this.originTileY, this.zoom);
|
||||
const tileLon = this.tileXToLon(tileX, this.zoom);
|
||||
const tileLat = this.tileYToLat(tileY, this.zoom);
|
||||
|
||||
// Convert to world coordinates using precise geographic distance
|
||||
const deltaX = (tileLon - originLon) * 111320 * Math.cos(this.origin.latitude * Math.PI / 180) / 1000;
|
||||
const deltaZ = -(tileLat - originLat) * 111320 / 1000; // Negative for Three.js coordinates
|
||||
|
||||
return { x: deltaX, z: deltaZ };
|
||||
}
|
||||
|
||||
// Convert OSM tile X to longitude
|
||||
tileXToLon(x, zoom) {
|
||||
return x / Math.pow(2, zoom) * 360 - 180;
|
||||
}
|
||||
|
||||
// Convert OSM tile Y to latitude
|
||||
tileYToLat(y, zoom) {
|
||||
const n = Math.PI - 2 * Math.PI * y / Math.pow(2, zoom);
|
||||
return 180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
||||
}
|
||||
|
||||
async loadTile(tileX, tileY) {
|
||||
const key = `${tileX}_${tileY}`;
|
||||
if (this.loadedTiles.has(key)) return;
|
||||
|
||||
try {
|
||||
// OpenStreetMap tile server
|
||||
const url = `https://tile.openstreetmap.org/${this.zoom}/${tileX}/${tileY}.png`;
|
||||
|
||||
const texture = await new THREE.TextureLoader().loadAsync(url);
|
||||
// Use ClampToEdgeWrapping to prevent gaps and improve alignment
|
||||
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
texture.magFilter = THREE.LinearFilter;
|
||||
texture.minFilter = THREE.LinearFilter;
|
||||
|
||||
// Create tile geometry with exact size for precise alignment
|
||||
const geometry = new THREE.PlaneGeometry(this.tileSize, this.tileSize);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: false, // Remove transparency to avoid visual artifacts
|
||||
side: THREE.FrontSide
|
||||
});
|
||||
|
||||
const tile = new THREE.Mesh(geometry, material);
|
||||
tile.rotation.x = -Math.PI / 2; // Lay flat on ground
|
||||
|
||||
// Position tile relative to origin with precise positioning
|
||||
const worldPos = this.tileToWorldPosition(tileX, tileY);
|
||||
tile.position.set(worldPos.x, 0.01, worldPos.z); // Slightly above ground (1cm)
|
||||
|
||||
this.scene.add(tile);
|
||||
this.loadedTiles.set(key, tile);
|
||||
|
||||
if (window.skyviewVerbose) {
|
||||
console.log(`Loaded tile ${tileX},${tileY} at position ${worldPos.x},${worldPos.z}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load tile ${tileX},${tileY}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadInitialTiles() {
|
||||
// Load a larger grid of tiles around the origin for better coverage
|
||||
const tileRange = 3; // Load 3 tiles in each direction (7x7 = ~70km x 70km)
|
||||
|
||||
// Load center tiles first, then expand outward for better user experience
|
||||
const loadOrder = [];
|
||||
for (let distance = 0; distance <= tileRange; distance++) {
|
||||
for (let x = -distance; x <= distance; x++) {
|
||||
for (let y = -distance; y <= distance; y++) {
|
||||
if (Math.abs(x) === distance || Math.abs(y) === distance) {
|
||||
loadOrder.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load tiles with small delays to avoid overwhelming the server
|
||||
for (const { x, y } of loadOrder) {
|
||||
const tileX = this.originTileX + x;
|
||||
const tileY = this.originTileY + y;
|
||||
await this.loadTile(tileX, tileY);
|
||||
|
||||
// Small delay to prevent rate limiting
|
||||
if (loadOrder.length > 10) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
// Add origin indicator
|
||||
this.addOriginIndicator();
|
||||
}
|
||||
|
||||
addOriginIndicator() {
|
||||
// Create a visible marker at the origin point
|
||||
const originGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 8);
|
||||
const originMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0xff0000,
|
||||
emissive: 0x440000
|
||||
});
|
||||
const originMarker = new THREE.Mesh(originGeometry, originMaterial);
|
||||
originMarker.position.set(0, 1, 0); // Position at origin, 1km above ground
|
||||
this.scene.add(originMarker);
|
||||
|
||||
// Add ring indicator on ground
|
||||
const ringGeometry = new THREE.RingGeometry(2, 3, 16);
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({
|
||||
color: 0xff0000,
|
||||
side: THREE.DoubleSide,
|
||||
transparent: true,
|
||||
opacity: 0.7
|
||||
});
|
||||
const originRing = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||
originRing.rotation.x = -Math.PI / 2; // Lay flat on ground
|
||||
originRing.position.set(0, 0.1, 0); // Slightly above ground to prevent z-fighting
|
||||
this.scene.add(originRing);
|
||||
|
||||
if (window.skyviewVerbose) {
|
||||
console.log('Added origin indicator at (0, 0, 0)');
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup method for removing tiles
|
||||
removeTile(tileX, tileY) {
|
||||
const key = `${tileX}_${tileY}`;
|
||||
const tile = this.loadedTiles.get(key);
|
||||
if (tile) {
|
||||
this.scene.remove(tile);
|
||||
tile.geometry.dispose();
|
||||
tile.material.map?.dispose();
|
||||
tile.material.dispose();
|
||||
this.loadedTiles.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SkyView {
|
||||
constructor() {
|
||||
// Initialize managers
|
||||
|
|
@ -382,8 +560,8 @@ class SkyView {
|
|||
|
||||
// Initialize 3D radar when switching to 3D radar view
|
||||
if (viewId === 'radar3d-view' && !this.radar3dInitialized) {
|
||||
setTimeout(() => {
|
||||
this.initialize3DRadar();
|
||||
setTimeout(async () => {
|
||||
await this.initialize3DRadar();
|
||||
this.radar3dInitialized = true;
|
||||
}, 100); // Small delay to ensure the view is visible
|
||||
}
|
||||
|
|
@ -393,7 +571,7 @@ class SkyView {
|
|||
}
|
||||
|
||||
// 3D Radar (basic implementation)
|
||||
initialize3DRadar() {
|
||||
async initialize3DRadar() {
|
||||
if (this.verbose) {
|
||||
console.log('🚀 Starting 3D radar initialization');
|
||||
}
|
||||
|
|
@ -457,7 +635,11 @@ class SkyView {
|
|||
camera: new THREE.PerspectiveCamera(75, width / height, 0.1, 1000),
|
||||
renderer: new THREE.WebGLRenderer({ alpha: true, antialias: true }),
|
||||
controls: null,
|
||||
aircraftMeshes: new Map()
|
||||
aircraftMeshes: new Map(),
|
||||
aircraftLabels: new Map(),
|
||||
labelContainer: null,
|
||||
aircraftTrails: new Map(),
|
||||
showTrails: false
|
||||
};
|
||||
|
||||
// Set up renderer with forced dimensions
|
||||
|
|
@ -469,7 +651,11 @@ class SkyView {
|
|||
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()
|
||||
aircraftMeshes: new Map(),
|
||||
aircraftLabels: new Map(),
|
||||
labelContainer: null,
|
||||
aircraftTrails: new Map(),
|
||||
showTrails: false
|
||||
};
|
||||
|
||||
// Set up renderer
|
||||
|
|
@ -498,6 +684,17 @@ class SkyView {
|
|||
this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9);
|
||||
container.appendChild(this.radar3d.renderer.domElement);
|
||||
|
||||
// Create label container for aircraft labels
|
||||
this.radar3d.labelContainer = document.createElement('div');
|
||||
this.radar3d.labelContainer.style.position = 'absolute';
|
||||
this.radar3d.labelContainer.style.top = '0';
|
||||
this.radar3d.labelContainer.style.left = '0';
|
||||
this.radar3d.labelContainer.style.width = '100%';
|
||||
this.radar3d.labelContainer.style.height = '100%';
|
||||
this.radar3d.labelContainer.style.pointerEvents = 'none';
|
||||
this.radar3d.labelContainer.style.zIndex = '1000';
|
||||
container.appendChild(this.radar3d.labelContainer);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log('✅ Renderer added to container');
|
||||
}
|
||||
|
|
@ -515,8 +712,8 @@ class SkyView {
|
|||
hemiLight.position.set(0, 100, 0);
|
||||
this.radar3d.scene.add(hemiLight);
|
||||
|
||||
// Set up camera
|
||||
this.radar3d.camera.position.set(0, 50, 50);
|
||||
// Set up camera with better view of larger map area
|
||||
this.radar3d.camera.position.set(0, 80, 80);
|
||||
this.radar3d.camera.lookAt(0, 0, 0);
|
||||
|
||||
// Add controls
|
||||
|
|
@ -537,7 +734,7 @@ class SkyView {
|
|||
}
|
||||
|
||||
// Store default camera position for reset functionality
|
||||
this.radar3d.defaultPosition = { x: 0, y: 50, z: 50 };
|
||||
this.radar3d.defaultPosition = { x: 0, y: 80, z: 80 };
|
||||
this.radar3d.defaultTarget = { x: 0, y: 0, z: 0 };
|
||||
|
||||
// Auto-rotate state
|
||||
|
|
@ -546,31 +743,21 @@ class SkyView {
|
|||
// Origin coordinates (will be fetched from server)
|
||||
this.radar3d.origin = { latitude: 59.9081, longitude: 10.8015 }; // fallback
|
||||
|
||||
// Initialize raycaster for click interactions
|
||||
this.radar3d.raycaster = new THREE.Raycaster();
|
||||
this.radar3d.mouse = new THREE.Vector2();
|
||||
this.radar3d.selectedAircraft = null;
|
||||
|
||||
// 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 click event listener for aircraft selection
|
||||
this.radar3d.renderer.domElement.addEventListener('click', (event) => this.handle3DClick(event));
|
||||
|
||||
// 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);
|
||||
// Initialize map tiles instead of simple ground plane
|
||||
this.radar3d.tileManager = new TileManager(this.radar3d.scene, this.radar3d.origin);
|
||||
await this.radar3d.tileManager.loadInitialTiles();
|
||||
|
||||
if (this.verbose) {
|
||||
console.log('3D Radar initialized successfully', {
|
||||
|
|
@ -619,11 +806,25 @@ class SkyView {
|
|||
const mesh = new THREE.Mesh(geometry, material);
|
||||
this.radar3d.aircraftMeshes.set(key, mesh);
|
||||
this.radar3d.scene.add(mesh);
|
||||
|
||||
// Create aircraft label
|
||||
this.create3DAircraftLabel(key, aircraft);
|
||||
|
||||
// Create trail if trails are enabled
|
||||
if (this.radar3d.showTrails) {
|
||||
this.create3DAircraftTrail(key, aircraft);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Aircraft mesh exists, check if it should be visible
|
||||
const mesh = this.radar3d.aircraftMeshes.get(key);
|
||||
mesh.visible = withinRange;
|
||||
|
||||
// Update label visibility
|
||||
const label = this.radar3d.aircraftLabels.get(key);
|
||||
if (label) {
|
||||
label.style.display = withinRange ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Update position if aircraft is within range
|
||||
|
|
@ -641,15 +842,34 @@ class SkyView {
|
|||
if (aircraft.Track !== undefined) {
|
||||
mesh.rotation.y = -aircraft.Track * Math.PI / 180;
|
||||
}
|
||||
|
||||
// Update trail if trails are enabled
|
||||
if (this.radar3d.showTrails && aircraft.position_history) {
|
||||
this.update3DAircraftTrail(key, aircraft);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old aircraft
|
||||
// Remove old aircraft and their labels
|
||||
this.radar3d.aircraftMeshes.forEach((mesh, key) => {
|
||||
if (!this.aircraftManager.aircraftData.has(key)) {
|
||||
this.radar3d.scene.remove(mesh);
|
||||
this.radar3d.aircraftMeshes.delete(key);
|
||||
|
||||
// Remove associated label
|
||||
const label = this.radar3d.aircraftLabels.get(key);
|
||||
if (label) {
|
||||
this.radar3d.labelContainer.removeChild(label);
|
||||
this.radar3d.aircraftLabels.delete(key);
|
||||
}
|
||||
|
||||
// Remove associated trail
|
||||
const trail = this.radar3d.aircraftTrails.get(key);
|
||||
if (trail) {
|
||||
this.radar3d.scene.remove(trail);
|
||||
this.radar3d.aircraftTrails.delete(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -667,6 +887,302 @@ class SkyView {
|
|||
}
|
||||
|
||||
this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera);
|
||||
|
||||
// Update aircraft labels after rendering
|
||||
this.update3DAircraftLabels();
|
||||
}
|
||||
|
||||
create3DAircraftLabel(icao, aircraft) {
|
||||
const label = document.createElement('div');
|
||||
label.style.position = 'absolute';
|
||||
label.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
label.style.color = 'white';
|
||||
label.style.padding = '4px 8px';
|
||||
label.style.borderRadius = '4px';
|
||||
label.style.fontSize = '12px';
|
||||
label.style.fontFamily = 'monospace';
|
||||
label.style.whiteSpace = 'nowrap';
|
||||
label.style.border = '1px solid rgba(255, 255, 255, 0.3)';
|
||||
label.style.pointerEvents = 'auto';
|
||||
label.style.cursor = 'pointer';
|
||||
label.style.transition = 'opacity 0.3s';
|
||||
label.style.zIndex = '1001';
|
||||
|
||||
// Create label content
|
||||
this.update3DAircraftLabelContent(label, aircraft);
|
||||
|
||||
// Add click handler to label
|
||||
label.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.select3DAircraft(icao, aircraft);
|
||||
});
|
||||
|
||||
this.radar3d.labelContainer.appendChild(label);
|
||||
this.radar3d.aircraftLabels.set(icao, label);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
update3DAircraftLabelContent(label, aircraft) {
|
||||
const callsign = aircraft.Callsign || aircraft.Icao || 'N/A';
|
||||
const altitude = aircraft.Altitude ? `${Math.round(aircraft.Altitude)}ft` : 'N/A';
|
||||
const speed = aircraft.Speed ? `${Math.round(aircraft.Speed)}kts` : 'N/A';
|
||||
|
||||
label.innerHTML = `
|
||||
<div style="font-weight: bold;">${callsign}</div>
|
||||
<div style="font-size: 10px; opacity: 0.8;">${altitude} • ${speed}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
update3DAircraftLabels() {
|
||||
if (!this.radar3d || !this.radar3d.aircraftLabels || !this.aircraftManager) return;
|
||||
|
||||
// Vector for 3D to screen coordinate conversion
|
||||
const vector = new THREE.Vector3();
|
||||
const canvas = this.radar3d.renderer.domElement;
|
||||
|
||||
this.radar3d.aircraftLabels.forEach((label, icao) => {
|
||||
const mesh = this.radar3d.aircraftMeshes.get(icao);
|
||||
const aircraft = this.aircraftManager.aircraftData.get(icao);
|
||||
|
||||
if (mesh && aircraft && mesh.visible) {
|
||||
// Get aircraft world position
|
||||
vector.setFromMatrixPosition(mesh.matrixWorld);
|
||||
|
||||
// Project to screen coordinates
|
||||
vector.project(this.radar3d.camera);
|
||||
|
||||
// Check if aircraft is behind camera
|
||||
if (vector.z > 1) {
|
||||
label.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to screen pixels
|
||||
const x = (vector.x * 0.5 + 0.5) * canvas.clientWidth;
|
||||
const y = (vector.y * -0.5 + 0.5) * canvas.clientHeight;
|
||||
|
||||
// Position label slightly offset from aircraft position
|
||||
label.style.left = `${x + 10}px`;
|
||||
label.style.top = `${y - 30}px`;
|
||||
label.style.display = 'block';
|
||||
label.style.opacity = '1';
|
||||
|
||||
// Update label content with current aircraft data
|
||||
this.update3DAircraftLabelContent(label, aircraft);
|
||||
} else {
|
||||
label.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handle3DClick(event) {
|
||||
if (!this.radar3d || !this.radar3d.raycaster || !this.aircraftManager) return;
|
||||
|
||||
// Prevent event from propagating to orbit controls
|
||||
event.preventDefault();
|
||||
|
||||
const canvas = this.radar3d.renderer.domElement;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
// Calculate mouse position in normalized device coordinates (-1 to +1)
|
||||
this.radar3d.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.radar3d.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
|
||||
// Update raycaster with camera and mouse position
|
||||
this.radar3d.raycaster.setFromCamera(this.radar3d.mouse, this.radar3d.camera);
|
||||
|
||||
// Get all visible aircraft meshes
|
||||
const aircraftMeshes = [];
|
||||
this.radar3d.aircraftMeshes.forEach((mesh, icao) => {
|
||||
if (mesh.visible) {
|
||||
mesh.userData.icao = icao; // Store ICAO for identification
|
||||
aircraftMeshes.push(mesh);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for intersections
|
||||
const intersects = this.radar3d.raycaster.intersectObjects(aircraftMeshes);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const selectedMesh = intersects[0].object;
|
||||
const selectedIcao = selectedMesh.userData.icao;
|
||||
const aircraft = this.aircraftManager.aircraftData.get(selectedIcao);
|
||||
|
||||
if (aircraft) {
|
||||
this.select3DAircraft(selectedIcao, aircraft);
|
||||
}
|
||||
} else {
|
||||
// Clicked on empty space, deselect
|
||||
this.deselect3DAircraft();
|
||||
}
|
||||
}
|
||||
|
||||
select3DAircraft(icao, aircraft) {
|
||||
// Deselect previous aircraft
|
||||
this.deselect3DAircraft();
|
||||
|
||||
this.radar3d.selectedAircraft = icao;
|
||||
|
||||
// Highlight selected aircraft mesh
|
||||
const mesh = this.radar3d.aircraftMeshes.get(icao);
|
||||
if (mesh) {
|
||||
mesh.material.color.setHex(0xff8800); // Orange color for selection
|
||||
mesh.material.emissive.setHex(0x441100); // Subtle glow
|
||||
}
|
||||
|
||||
// Highlight selected aircraft label
|
||||
const label = this.radar3d.aircraftLabels.get(icao);
|
||||
if (label) {
|
||||
label.style.background = 'rgba(255, 136, 0, 0.9)';
|
||||
label.style.borderColor = 'rgba(255, 136, 0, 0.8)';
|
||||
label.style.transform = 'scale(1.1)';
|
||||
}
|
||||
|
||||
// Log selection for debugging
|
||||
if (this.verbose) {
|
||||
console.log(`Selected aircraft: ${icao}`, aircraft);
|
||||
}
|
||||
|
||||
// Trigger aircraft selection in main aircraft manager (for consistency with 2D map)
|
||||
if (this.aircraftManager.onAircraftSelected) {
|
||||
this.aircraftManager.onAircraftSelected(icao, aircraft);
|
||||
}
|
||||
}
|
||||
|
||||
deselect3DAircraft() {
|
||||
if (!this.radar3d.selectedAircraft) return;
|
||||
|
||||
const icao = this.radar3d.selectedAircraft;
|
||||
|
||||
// Reset mesh appearance
|
||||
const mesh = this.radar3d.aircraftMeshes.get(icao);
|
||||
if (mesh) {
|
||||
mesh.material.color.setHex(0x00ff00); // Back to green
|
||||
mesh.material.emissive.setHex(0x000000); // Remove glow
|
||||
}
|
||||
|
||||
// Reset label appearance
|
||||
const label = this.radar3d.aircraftLabels.get(icao);
|
||||
if (label) {
|
||||
label.style.background = 'rgba(0, 0, 0, 0.8)';
|
||||
label.style.borderColor = 'rgba(255, 255, 255, 0.3)';
|
||||
label.style.transform = 'scale(1)';
|
||||
}
|
||||
|
||||
this.radar3d.selectedAircraft = null;
|
||||
}
|
||||
|
||||
create3DAircraftTrail(icao, aircraft) {
|
||||
if (!aircraft.position_history || aircraft.position_history.length < 2) return;
|
||||
|
||||
// Create line geometry for trail
|
||||
const points = [];
|
||||
const originLat = this.radar3d.origin.latitude;
|
||||
const originLon = this.radar3d.origin.longitude;
|
||||
|
||||
// Convert position history to 3D world coordinates
|
||||
aircraft.position_history.forEach(pos => {
|
||||
if (pos.latitude && pos.longitude) {
|
||||
const x = (pos.longitude - originLon) * 111320 * Math.cos(pos.latitude * Math.PI / 180) / 1000;
|
||||
const z = -(pos.latitude - originLat) * 111320 / 1000;
|
||||
const y = (pos.altitude || 0) / 1000; // Convert feet to km
|
||||
points.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
});
|
||||
|
||||
if (points.length < 2) return;
|
||||
|
||||
// Create line geometry
|
||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
||||
|
||||
// Create gradient material for trail (older parts more transparent)
|
||||
const material = new THREE.LineBasicMaterial({
|
||||
color: 0x00aa88,
|
||||
transparent: true,
|
||||
opacity: 0.7,
|
||||
linewidth: 2
|
||||
});
|
||||
|
||||
const trail = new THREE.Line(geometry, material);
|
||||
|
||||
this.radar3d.scene.add(trail);
|
||||
this.radar3d.aircraftTrails.set(icao, trail);
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`Created trail for ${icao} with ${points.length} points`);
|
||||
}
|
||||
}
|
||||
|
||||
update3DAircraftTrail(icao, aircraft) {
|
||||
if (!aircraft.position_history || aircraft.position_history.length < 2) {
|
||||
// Remove trail if no history
|
||||
const trail = this.radar3d.aircraftTrails.get(icao);
|
||||
if (trail) {
|
||||
this.radar3d.scene.remove(trail);
|
||||
this.radar3d.aircraftTrails.delete(icao);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const trail = this.radar3d.aircraftTrails.get(icao);
|
||||
|
||||
if (trail) {
|
||||
// Update existing trail
|
||||
const points = [];
|
||||
const originLat = this.radar3d.origin.latitude;
|
||||
const originLon = this.radar3d.origin.longitude;
|
||||
|
||||
// Convert position history to 3D world coordinates
|
||||
aircraft.position_history.forEach(pos => {
|
||||
if (pos.latitude && pos.longitude) {
|
||||
const x = (pos.longitude - originLon) * 111320 * Math.cos(pos.latitude * Math.PI / 180) / 1000;
|
||||
const z = -(pos.latitude - originLat) * 111320 / 1000;
|
||||
const y = (pos.altitude || 0) / 1000; // Convert feet to km
|
||||
points.push(new THREE.Vector3(x, y, z));
|
||||
}
|
||||
});
|
||||
|
||||
if (points.length >= 2) {
|
||||
// Update geometry with new points
|
||||
trail.geometry.setFromPoints(points);
|
||||
trail.geometry.needsUpdate = true;
|
||||
}
|
||||
} else {
|
||||
// Create new trail
|
||||
this.create3DAircraftTrail(icao, aircraft);
|
||||
}
|
||||
}
|
||||
|
||||
toggle3DTrails() {
|
||||
this.radar3d.showTrails = !this.radar3d.showTrails;
|
||||
|
||||
if (this.radar3d.showTrails) {
|
||||
// Create trails for all existing aircraft
|
||||
this.aircraftManager.aircraftData.forEach((aircraft, icao) => {
|
||||
if (this.radar3d.aircraftMeshes.has(icao)) {
|
||||
this.create3DAircraftTrail(icao, aircraft);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Remove all trails
|
||||
this.radar3d.aircraftTrails.forEach((trail, icao) => {
|
||||
this.radar3d.scene.remove(trail);
|
||||
});
|
||||
this.radar3d.aircraftTrails.clear();
|
||||
}
|
||||
|
||||
// Update button state
|
||||
const trailButton = document.getElementById('radar3d-trails');
|
||||
if (trailButton) {
|
||||
trailButton.textContent = this.radar3d.showTrails ? 'Hide Trails' : 'Show Trails';
|
||||
trailButton.classList.toggle('active', this.radar3d.showTrails);
|
||||
}
|
||||
|
||||
if (this.verbose) {
|
||||
console.log(`3D trails ${this.radar3d.showTrails ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
}
|
||||
|
||||
initialize3DControls() {
|
||||
|
|
@ -684,6 +1200,13 @@ class SkyView {
|
|||
autoRotateButton.addEventListener('click', () => this.toggle3DAutoRotate());
|
||||
}
|
||||
|
||||
// Enable and initialize the Trails toggle button
|
||||
const trailsButton = document.getElementById('radar3d-trails');
|
||||
if (trailsButton) {
|
||||
trailsButton.disabled = false;
|
||||
trailsButton.addEventListener('click', () => this.toggle3DTrails());
|
||||
}
|
||||
|
||||
// Enable and initialize the Range slider
|
||||
const rangeSlider = document.getElementById('radar3d-range');
|
||||
const rangeValue = document.getElementById('radar3d-range-value');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue