fix: Implement dynamic age calculation for aircraft tracking #37

Merged
olemd merged 1 commit from fix/aircraft-age-calculation into main 2025-09-01 10:34:11 +02:00
5 changed files with 145 additions and 3 deletions

View file

@ -469,10 +469,40 @@ class SkyView {
this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera); this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera);
} }
updateOpenPopupAges() {
// Find any open aircraft popups and update their age displays
if (!this.aircraftManager) return;
this.aircraftManager.aircraftMarkers.forEach((marker, icao) => {
if (marker.isPopupOpen()) {
const aircraft = this.aircraftManager.aircraftData.get(icao);
if (aircraft) {
// Refresh the popup content with current age
marker.setPopupContent(this.aircraftManager.createPopupContent(aircraft));
// Re-enhance callsign display for the updated popup
const popupElement = marker.getPopup().getElement();
if (popupElement) {
this.aircraftManager.enhanceCallsignDisplay(popupElement);
}
}
}
});
}
startPeriodicTasks() { startPeriodicTasks() {
// Update clocks every second // Update clocks every second
setInterval(() => this.uiManager.updateClocks(), 1000); setInterval(() => this.uiManager.updateClocks(), 1000);
// Update aircraft ages and refresh displays every second
setInterval(() => {
// Update aircraft table to show current ages
this.uiManager.updateAircraftTable();
// Update any open aircraft popups with current ages
this.updateOpenPopupAges();
}, 1000);
// Update charts every 10 seconds // Update charts every 10 seconds
setInterval(() => this.updateCharts(), 10000); setInterval(() => this.updateCharts(), 10000);

View file

@ -491,7 +491,7 @@ export class AircraftManager {
<strong>Messages:</strong> ${aircraft.TotalMessages || 0} <strong>Messages:</strong> ${aircraft.TotalMessages || 0}
</div> </div>
<div class="detail-row"> <div class="detail-row">
<strong>Age:</strong> ${aircraft.Age ? aircraft.Age.toFixed(1) : '0'}s <strong>Age:</strong> ${this.calculateAge(aircraft).toFixed(1)}s
</div> </div>
</div> </div>
</div> </div>
@ -513,6 +513,16 @@ export class AircraftManager {
return minDistance === Infinity ? null : minDistance; return minDistance === Infinity ? null : minDistance;
} }
calculateAge(aircraft) {
if (!aircraft.last_update) return 0;
const lastUpdate = new Date(aircraft.last_update);
const now = new Date();
const ageMs = now.getTime() - lastUpdate.getTime();
return Math.max(0, ageMs / 1000); // Return age in seconds, minimum 0
}
// Enhance callsign display in popup after it's created // Enhance callsign display in popup after it's created
async enhanceCallsignDisplay(popupElement) { async enhanceCallsignDisplay(popupElement) {
if (!this.callsignManager) return; if (!this.callsignManager) return;

View file

@ -129,7 +129,7 @@ export class UIManager {
<td>${aircraft.Track || '-'}°</td> <td>${aircraft.Track || '-'}°</td>
<td>${sources}</td> <td>${sources}</td>
<td><span class="${this.getSignalClass(bestSignal)}">${bestSignal ? bestSignal.toFixed(1) : '-'}</span></td> <td><span class="${this.getSignalClass(bestSignal)}">${bestSignal ? bestSignal.toFixed(1) : '-'}</span></td>
<td>${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s</td> <td>${this.calculateAge(aircraft).toFixed(0)}s</td>
`; `;
row.addEventListener('click', () => { row.addEventListener('click', () => {
@ -161,6 +161,16 @@ export class UIManager {
return 'commercial'; return 'commercial';
} }
calculateAge(aircraft) {
if (!aircraft.last_update) return 0;
const lastUpdate = new Date(aircraft.last_update);
const now = new Date();
const ageMs = now.getTime() - lastUpdate.getTime();
return Math.max(0, ageMs / 1000); // Return age in seconds, minimum 0
}
getBestSignalFromSources(sources) { getBestSignalFromSources(sources) {
if (!sources) return null; if (!sources) return null;
let bestSignal = -999; let bestSignal = -999;

View file

@ -202,7 +202,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
SpeedHistory: a.SpeedHistory, SpeedHistory: a.SpeedHistory,
Distance: a.Distance, Distance: a.Distance,
Bearing: a.Bearing, Bearing: a.Bearing,
Age: a.Age, Age: time.Since(a.LastUpdate).Seconds(),
MLATSources: a.MLATSources, MLATSources: a.MLATSources,
PositionSource: a.PositionSource, PositionSource: a.PositionSource,
UpdateRate: a.UpdateRate, UpdateRate: a.UpdateRate,

View file

@ -0,0 +1,92 @@
package merger
import (
"encoding/json"
"testing"
"time"
"skyview/internal/modes"
)
func TestAircraftState_MarshalJSON_AgeCalculation(t *testing.T) {
// Create a test aircraft state with a known LastUpdate time
pastTime := time.Now().Add(-30 * time.Second) // 30 seconds ago
state := &AircraftState{
Aircraft: &modes.Aircraft{
ICAO24: 0x123456,
Callsign: "TEST123",
},
LastUpdate: pastTime,
}
// Marshal to JSON
jsonData, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal AircraftState: %v", err)
}
// Parse the JSON to check the age field
var result map[string]interface{}
if err := json.Unmarshal(jsonData, &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that age is not 0 and is approximately 30 seconds
age, ok := result["age"].(float64)
if !ok {
t.Fatal("Age field not found or not a number")
}
if age < 25 || age > 35 {
t.Errorf("Expected age to be around 30 seconds, got %f", age)
}
if age == 0 {
t.Error("Age should not be 0 - this indicates the bug is still present")
}
t.Logf("Aircraft age calculated correctly: %f seconds", age)
}
func TestAircraftState_MarshalJSON_ZeroAge(t *testing.T) {
// Test with very recent time to ensure age is very small but not exactly 0
recentTime := time.Now()
state := &AircraftState{
Aircraft: &modes.Aircraft{
ICAO24: 0x123456,
Callsign: "TEST123",
},
LastUpdate: recentTime,
}
// Marshal to JSON
jsonData, err := json.Marshal(state)
if err != nil {
t.Fatalf("Failed to marshal AircraftState: %v", err)
}
// Parse the JSON to check the age field
var result map[string]interface{}
if err := json.Unmarshal(jsonData, &result); err != nil {
t.Fatalf("Failed to unmarshal JSON: %v", err)
}
// Check that age is very small but should not be exactly 0 (unless extremely fast)
age, ok := result["age"].(float64)
if !ok {
t.Fatal("Age field not found or not a number")
}
if age < 0 {
t.Errorf("Age should not be negative, got %f", age)
}
// Age should be very small (less than 1 second) but might not be exactly 0
if age > 1.0 {
t.Errorf("Age should be very small for recent update, got %f", age)
}
t.Logf("Recent aircraft age calculated correctly: %f seconds", age)
}