diff --git a/assets/static/js/app.js b/assets/static/js/app.js index 0d780fb..836feb0 100644 --- a/assets/static/js/app.js +++ b/assets/static/js/app.js @@ -803,6 +803,10 @@ class SkyView { if (withinRange) { const { geometry, material } = this.create3DAircraftGeometry(aircraft); const mesh = new THREE.Mesh(geometry, material); + + // Apply signal quality visual effects + this.apply3DSignalQualityEffects(mesh, aircraft); + this.radar3d.aircraftMeshes.set(key, mesh); this.radar3d.scene.add(mesh); @@ -840,6 +844,9 @@ class SkyView { // Update aircraft visual indicators (direction, climb/descent) this.update3DAircraftVisuals(mesh, aircraft); + // Update signal quality effects + this.apply3DSignalQualityEffects(mesh, aircraft); + // Update trail if trails are enabled if (this.radar3d.showTrails && aircraft.position_history) { this.update3DAircraftTrail(key, aircraft); @@ -1095,15 +1102,14 @@ class SkyView { if (points.length < 2) return; - // Create line geometry - const geometry = new THREE.BufferGeometry().setFromPoints(points); + // Create enhanced line geometry with gradient colors + const geometry = this.create3DTrailGeometry(points, aircraft); - // Create gradient material for trail (older parts more transparent) + // Create material with vertex colors for gradient effect const material = new THREE.LineBasicMaterial({ - color: 0x00aa88, + vertexColors: true, transparent: true, - opacity: 0.7, - linewidth: 2 + opacity: 0.8 }); const trail = new THREE.Line(geometry, material); @@ -1146,9 +1152,10 @@ class SkyView { }); if (points.length >= 2) { - // Update geometry with new points - trail.geometry.setFromPoints(points); - trail.geometry.needsUpdate = true; + // Update geometry with enhanced trail visualization + const newGeometry = this.create3DTrailGeometry(points, aircraft); + trail.geometry.dispose(); // Clean up old geometry + trail.geometry = newGeometry; } } else { // Create new trail @@ -1339,6 +1346,209 @@ class SkyView { } } } + + // Apply signal quality visual effects to aircraft mesh + apply3DSignalQualityEffects(mesh, aircraft) { + // Get signal quality data from sources + const signalData = this.getAircraftSignalData(aircraft); + + // Apply opacity based on signal strength + this.updateAircraftOpacity(mesh, signalData); + + // Apply emissive intensity based on signal quality + this.updateAircraftEmissiveIntensity(mesh, signalData); + + // Add data source indicator + this.addDataSourceIndicator(mesh, signalData); + } + + // Extract signal data from aircraft sources + getAircraftSignalData(aircraft) { + if (!aircraft.sources || Object.keys(aircraft.sources).length === 0) { + return { + bestSignal: -50, // Default medium signal + sourceCount: 0, + primarySource: null, + signalQuality: 'Unknown' + }; + } + + let bestSignal = -999; + let sourceCount = 0; + let primarySource = null; + + // Find strongest signal and count sources + Object.entries(aircraft.sources).forEach(([sourceId, sourceData]) => { + sourceCount++; + if (sourceData.signal_level > bestSignal) { + bestSignal = sourceData.signal_level; + primarySource = sourceId; + } + }); + + return { + bestSignal, + sourceCount, + primarySource, + signalQuality: aircraft.SignalQuality || 'Unknown' + }; + } + + // Update aircraft opacity based on signal strength + updateAircraftOpacity(mesh, signalData) { + // Map signal strength to opacity (stronger signal = more opaque) + // Typical ADS-B signal range: -60 dBFS (weak) to 0 dBFS (strong) + const minSignal = -60; + const maxSignal = -5; + const minOpacity = 0.3; + const maxOpacity = 1.0; + + // Clamp signal to expected range + const clampedSignal = Math.max(minSignal, Math.min(maxSignal, signalData.bestSignal)); + + // Calculate opacity (linear mapping) + const signalRange = maxSignal - minSignal; + const signalNormalized = (clampedSignal - minSignal) / signalRange; + const opacity = minOpacity + (signalNormalized * (maxOpacity - minOpacity)); + + // Apply opacity to material + mesh.material.transparent = true; + mesh.material.opacity = opacity; + + // Also adjust emissive intensity for glow effect + const baseEmissiveIntensity = 0.1; + const maxEmissiveBoost = 0.3; + mesh.material.emissiveIntensity = baseEmissiveIntensity + (signalNormalized * maxEmissiveBoost); + } + + // Update emissive intensity based on signal quality assessment + updateAircraftEmissiveIntensity(mesh, signalData) { + let qualityMultiplier = 1.0; + + // Adjust based on signal quality description + switch (signalData.signalQuality) { + case 'Excellent': + qualityMultiplier = 1.5; + break; + case 'Good': + qualityMultiplier = 1.2; + break; + case 'Fair': + qualityMultiplier = 0.8; + break; + case 'Poor': + qualityMultiplier = 0.5; + break; + default: + qualityMultiplier = 1.0; + } + + // Apply quality multiplier to emissive intensity + const currentIntensity = mesh.material.emissiveIntensity || 0.1; + mesh.material.emissiveIntensity = Math.min(0.5, currentIntensity * qualityMultiplier); + } + + // Add visual indicator for data source type + addDataSourceIndicator(mesh, signalData) { + // Remove existing source indicator + const existingIndicator = mesh.getObjectByName('sourceIndicator'); + if (existingIndicator) { + mesh.remove(existingIndicator); + } + + // Only add indicator if we have multiple sources + if (signalData.sourceCount > 1) { + // Create small indicator showing multi-source data + const indicatorGeometry = new THREE.SphereGeometry(0.1, 4, 4); + const indicatorMaterial = new THREE.MeshBasicMaterial({ + color: signalData.sourceCount > 2 ? 0x00ff00 : 0xffaa00, // Green for 3+, orange for 2 + transparent: true, + opacity: 0.7 + }); + + const indicator = new THREE.Mesh(indicatorGeometry, indicatorMaterial); + indicator.name = 'sourceIndicator'; + indicator.position.set(1, 1, 0); // Position to side of aircraft + mesh.add(indicator); + } + } + + // Create enhanced trail geometry with gradient colors and speed-based effects + create3DTrailGeometry(points, aircraft) { + const geometry = new THREE.BufferGeometry(); + + // Set positions + const positions = new Float32Array(points.length * 3); + const colors = new Float32Array(points.length * 3); + + // Get signal quality for trail styling + const signalData = this.getAircraftSignalData(aircraft); + const baseTrailColor = this.getTrailColor(aircraft, signalData); + + // Calculate colors with gradient effect (newer = brighter, older = dimmer) + for (let i = 0; i < points.length; i++) { + // Position + positions[i * 3] = points[i].x; + positions[i * 3 + 1] = points[i].y; + positions[i * 3 + 2] = points[i].z; + + // Color with age-based fading + const ageRatio = i / (points.length - 1); // 0 = oldest, 1 = newest + const intensity = 0.3 + (ageRatio * 0.7); // Fade from 30% to 100% brightness + + colors[i * 3] = baseTrailColor.r * intensity; // Red + colors[i * 3 + 1] = baseTrailColor.g * intensity; // Green + colors[i * 3 + 2] = baseTrailColor.b * intensity; // Blue + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + + return geometry; + } + + // Get trail color based on aircraft type and signal quality + getTrailColor(aircraft, signalData) { + const visualType = this.getAircraftVisualType(aircraft); + + // Base colors for different aircraft types (more muted for trails) + let baseColor; + switch (visualType) { + case 'helicopter': + baseColor = { r: 0.0, g: 0.8, b: 0.8 }; // Cyan + break; + case 'heavy': + baseColor = { r: 0.0, g: 0.4, b: 0.8 }; // Blue + break; + case 'light': + baseColor = { r: 0.8, g: 0.8, b: 0.0 }; // Yellow + break; + case 'medium': + default: + baseColor = { r: 0.0, g: 0.6, b: 0.4 }; // Teal-green + break; + } + + // Adjust brightness based on signal quality + const qualityMultiplier = this.getSignalQualityMultiplier(signalData.signalQuality); + + return { + r: Math.min(1.0, baseColor.r * qualityMultiplier), + g: Math.min(1.0, baseColor.g * qualityMultiplier), + b: Math.min(1.0, baseColor.b * qualityMultiplier) + }; + } + + // Get brightness multiplier based on signal quality + getSignalQualityMultiplier(signalQuality) { + switch (signalQuality) { + case 'Excellent': return 1.2; + case 'Good': return 1.0; + case 'Fair': return 0.8; + case 'Poor': return 0.6; + default: return 0.9; + } + } initialize3DControls() { // Enable and initialize the Reset View button