feat: Implement comprehensive signal quality visualization and enhanced trail effects

Adds advanced visual feedback system for 3D radar aircraft with real-time signal quality indicators:

Signal Quality Visualization:
- Opacity mapping based on signal strength (-60 to -5 dBFS range)
- Emissive glow intensity reflects signal quality (Excellent/Good/Fair/Poor)
- Multi-source data indicators (small spheres for aircraft with multiple receivers)
- Dynamic signal strength analysis from aircraft sources data
- Real-time updates with changing signal conditions

Enhanced Trail System:
- Gradient coloring with age-based fading (30% to 100% intensity)
- Aircraft type-based trail colors (cyan=helicopter, blue=heavy, yellow=light, teal=medium)
- Signal quality affects trail brightness and visibility
- Custom BufferGeometry with per-vertex colors
- Memory-efficient geometry disposal and rebuilding

Technical Improvements:
- Modular signal data extraction from aircraft sources
- Linear signal strength to opacity mapping
- Quality multiplier system for visual feedback
- Proper Three.js BufferAttribute management
- Dynamic visual updates synchronized with aircraft data

Visual Results:
- Strong signals = bright, opaque aircraft with vivid trails
- Weak signals = dim, transparent aircraft with faded trails
- Multi-source aircraft display small indicator spheres
- Trail colors match aircraft types for consistency
- Smooth gradient effects enhance flight path visualization

Addresses core signal quality and trail enhancement requirements from issue #42.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-09-01 21:29:53 +02:00
commit 275a346e85

View file

@ -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
@ -1340,6 +1347,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
const resetButton = document.getElementById('radar3d-reset');