Fix aircraft markers not updating positions in real-time

Root cause: The merger was blocking position updates from the same source
after the first position was established, designed for multi-source scenarios
but preventing single-source position updates.

Changes:
- Refactor JavaScript into modular architecture (WebSocketManager, AircraftManager, MapManager, UIManager)
- Add CPR coordinate validation to prevent invalid latitude/longitude values
- Fix merger to allow position updates from same source for moving aircraft
- Add comprehensive coordinate bounds checking in CPR decoder
- Update HTML to use new modular JavaScript with cache busting
- Add WebSocket debug logging to track data flow

Technical details:
- CPR decoder now validates coordinates within ±90° latitude, ±180° longitude
- Merger allows updates when currentBest == sourceID (same source continuous updates)
- JavaScript modules provide better separation of concerns and debugging
- WebSocket properly transmits updated aircraft coordinates to frontend

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 14:04:17 +02:00
commit 1de3e092ae
13 changed files with 2222 additions and 33 deletions

View file

@ -130,6 +130,11 @@ class SkyView {
document.getElementById('toggle-trails').addEventListener('click', () => this.toggleTrails());
document.getElementById('toggle-range').addEventListener('click', () => this.toggleRangeCircles());
document.getElementById('toggle-sources').addEventListener('click', () => this.toggleSources());
// If we already have aircraft data waiting, update markers now that map is ready
if (this.aircraftData.size > 0) {
this.updateMapMarkers();
}
}
async initializeCoverageMap() {
@ -251,6 +256,11 @@ class SkyView {
// Map Updates
updateMapMarkers() {
// Check if map is initialized
if (!this.map) {
return;
}
// Clear stale aircraft markers
const currentICAOs = new Set(this.aircraftData.keys());
for (const [icao, marker] of this.aircraftMarkers) {
@ -261,9 +271,10 @@ class SkyView {
}
}
// Update aircraft markers
// Update aircraft markers - only for aircraft with valid positions
// Note: Aircraft without positions are still shown in the table view
for (const [icao, aircraft] of this.aircraftData) {
if (aircraft.Latitude && aircraft.Longitude) {
if (aircraft.Latitude && aircraft.Longitude && aircraft.Latitude !== 0 && aircraft.Longitude !== 0) {
this.updateAircraftMarker(icao, aircraft);
}
}
@ -486,10 +497,10 @@ class SkyView {
const distance = this.calculateDistance(aircraft);
const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A';
const sources = aircraft.Sources ? Object.keys(aircraft.Sources).map(id => {
const sources = aircraft.sources ? Object.keys(aircraft.sources).map(id => {
const source = this.sourcesData.get(id);
const srcData = aircraft.Sources[id];
return `<span class="source-badge" title="Signal: ${srcData.SignalLevel?.toFixed(1)} dBFS">
const srcData = aircraft.sources[id];
return `<span class="source-badge" title="Signal: ${srcData.signal_level?.toFixed(1)} dBFS">
${source?.name || id}
</span>`;
}).join('') : 'N/A';
@ -567,7 +578,7 @@ class SkyView {
createSourcePopupContent(source) {
const aircraftCount = Array.from(this.aircraftData.values())
.filter(aircraft => aircraft.Sources && aircraft.Sources[source.id]).length;
.filter(aircraft => aircraft.sources && aircraft.sources[source.id]).length;
return `
<div class="source-popup">
@ -599,6 +610,9 @@ class SkyView {
// Table Management
updateAircraftTable() {
// Note: This table shows ALL aircraft we're tracking, including those without
// position data. Aircraft without positions will show "No position" in the
// location column but still provide useful info like callsign, altitude, etc.
const tbody = document.getElementById('aircraft-tbody');
tbody.innerHTML = '';
@ -618,7 +632,7 @@ class SkyView {
if (sourceFilter) {
filteredData = filteredData.filter(aircraft =>
aircraft.Sources && aircraft.Sources[sourceFilter]
aircraft.sources && aircraft.sources[sourceFilter]
);
}
@ -642,8 +656,8 @@ class SkyView {
const icao = aircraft.ICAO24 || 'N/A';
const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0;
const distance = this.calculateDistance(aircraft);
const sources = aircraft.Sources ? Object.keys(aircraft.Sources).length : 0;
const bestSignal = this.getBestSignalFromSources(aircraft.Sources);
const sources = aircraft.sources ? Object.keys(aircraft.sources).length : 0;
const bestSignal = this.getBestSignalFromSources(aircraft.sources);
const row = document.createElement('tr');
row.innerHTML = `
@ -677,8 +691,8 @@ class SkyView {
if (!sources) return null;
let bestSignal = -999;
for (const [id, data] of Object.entries(sources)) {
if (data.SignalLevel > bestSignal) {
bestSignal = data.SignalLevel;
if (data.signal_level > bestSignal) {
bestSignal = data.signal_level;
}
}
return bestSignal === -999 ? null : bestSignal;
@ -725,7 +739,7 @@ class SkyView {
case 'squawk':
return (a.Squawk || '').localeCompare(b.Squawk || '');
case 'signal':
return (this.getBestSignalFromSources(b.Sources) || -999) - (this.getBestSignalFromSources(a.Sources) || -999);
return (this.getBestSignalFromSources(b.sources) || -999) - (this.getBestSignalFromSources(a.sources) || -999);
case 'age':
return (a.Age || 0) - (b.Age || 0);
default:
@ -1127,9 +1141,9 @@ class SkyView {
// Use closest source as reference point
let minDistance = Infinity;
for (const [id, srcData] of Object.entries(aircraft.Sources || {})) {
if (srcData.Distance && srcData.Distance < minDistance) {
minDistance = srcData.Distance;
for (const [id, srcData] of Object.entries(aircraft.sources || {})) {
if (srcData.distance && srcData.distance < minDistance) {
minDistance = srcData.distance;
}
}