diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js index f955558..7b4dd2a 100644 --- a/assets/static/js/modules/aircraft-manager.js +++ b/assets/static/js/modules/aircraft-manager.js @@ -59,6 +59,7 @@ export class AircraftManager { // Debug: Log coordinate format and values console.log(`📍 ${icao}: pos=[${pos[0]}, ${pos[1]}], types=[${typeof pos[0]}, ${typeof pos[1]}]`); + console.log(`🔍 Marker check for ${icao}: has=${this.aircraftMarkers.has(icao)}, map_size=${this.aircraftMarkers.size}`); // Check for invalid coordinates - proper geographic bounds const isValidLat = pos[0] >= -90 && pos[0] <= 90; @@ -74,6 +75,8 @@ export class AircraftManager { const marker = this.aircraftMarkers.get(icao); // Always update position - let Leaflet handle everything + const oldPos = marker.getLatLng(); + console.log(`🔄 Updating ${icao}: [${oldPos.lat}, ${oldPos.lng}] -> [${pos[0]}, ${pos[1]}]`); marker.setLatLng(pos); // Update rotation using Leaflet's options if available, otherwise skip rotation diff --git a/assets/static/js/modules/map-manager.js b/assets/static/js/modules/map-manager.js index cabe8c9..0f93fec 100644 --- a/assets/static/js/modules/map-manager.js +++ b/assets/static/js/modules/map-manager.js @@ -32,6 +32,7 @@ export class MapManager { // Store origin for reset functionality this.mapOrigin = origin; + console.log(`🗺️ Map origin: [${origin.latitude}, ${origin.longitude}]`); this.map = L.map('map').setView([origin.latitude, origin.longitude], 10); // Dark tile layer diff --git a/cmd/beast-dump/main.go b/cmd/beast-dump/main.go index 202d366..4d585de 100644 --- a/cmd/beast-dump/main.go +++ b/cmd/beast-dump/main.go @@ -100,7 +100,7 @@ func parseFlags() *Config { func NewBeastDumper(config *Config) *BeastDumper { return &BeastDumper{ config: config, - decoder: modes.NewDecoder(), + decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default stats: struct { totalMessages int64 validMessages int64 diff --git a/docs/ADS-B Decoding Guide.pdf b/docs/ADS-B Decoding Guide.pdf new file mode 100644 index 0000000..87d319a Binary files /dev/null and b/docs/ADS-B Decoding Guide.pdf differ diff --git a/internal/client/beast.go b/internal/client/beast.go index b5830bf..05dc97a 100644 --- a/internal/client/beast.go +++ b/internal/client/beast.go @@ -72,7 +72,7 @@ func NewBeastClient(source *merger.Source, merger *merger.Merger) *BeastClient { return &BeastClient{ source: source, merger: merger, - decoder: modes.NewDecoder(), + decoder: modes.NewDecoder(source.Latitude, source.Longitude), msgChan: make(chan *beast.Message, 1000), errChan: make(chan error, 10), stopChan: make(chan struct{}), diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 84eed39..5f78619 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -400,9 +400,14 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so } if updatePosition { + fmt.Printf("Merger Update %06X: %.6f,%.6f -> %.6f,%.6f\n", + state.Aircraft.ICAO24, state.Latitude, state.Longitude, new.Latitude, new.Longitude) state.Latitude = new.Latitude state.Longitude = new.Longitude state.PositionSource = sourceID + } else { + fmt.Printf("Merger Skip %06X: rejected update %.6f,%.6f (current: %.6f,%.6f)\n", + state.Aircraft.ICAO24, new.Latitude, new.Longitude, state.Latitude, state.Longitude) } } diff --git a/internal/modes/decoder.go b/internal/modes/decoder.go index 960504e..ee9d05e 100644 --- a/internal/modes/decoder.go +++ b/internal/modes/decoder.go @@ -164,25 +164,35 @@ type Decoder struct { cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison) cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison) + // Reference position for CPR zone ambiguity resolution (receiver location) + refLatitude float64 // Receiver latitude in decimal degrees + refLongitude float64 // Receiver longitude in decimal degrees + // Mutex to protect concurrent access to CPR maps mu sync.RWMutex } // NewDecoder creates a new Mode S/ADS-B decoder with initialized CPR tracking. // -// The decoder is ready to process Mode S messages immediately and will -// maintain CPR position state across multiple messages for accurate -// position decoding. +// The reference position (typically the receiver location) is used to resolve +// CPR zone ambiguity during position decoding. Without a proper reference, +// aircraft can appear many degrees away from their actual position. +// +// Parameters: +// - refLat: Reference latitude in decimal degrees (receiver location) +// - refLon: Reference longitude in decimal degrees (receiver location) // // Returns a configured decoder ready for message processing. -func NewDecoder() *Decoder { +func NewDecoder(refLat, refLon float64) *Decoder { return &Decoder{ - cprEvenLat: make(map[uint32]float64), - cprEvenLon: make(map[uint32]float64), - cprOddLat: make(map[uint32]float64), - cprOddLon: make(map[uint32]float64), - cprEvenTime: make(map[uint32]int64), - cprOddTime: make(map[uint32]int64), + cprEvenLat: make(map[uint32]float64), + cprEvenLon: make(map[uint32]float64), + cprOddLat: make(map[uint32]float64), + cprOddLon: make(map[uint32]float64), + cprEvenTime: make(map[uint32]int64), + cprOddTime: make(map[uint32]int64), + refLatitude: refLat, + refLongitude: refLon, } } @@ -399,24 +409,13 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) { // decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding. // -// This is the core algorithm for resolving aircraft positions from CPR-encoded data. -// The algorithm requires both even and odd CPR messages to resolve position ambiguity. +// CRITICAL: The CPR algorithm has zone ambiguity that requires either: +// 1. A reference position (receiver location) to resolve zones correctly, OR +// 2. Message timestamp comparison to choose the most recent valid position // -// CPR Global Decoding Algorithm: -// 1. Check that both even and odd CPR values are available -// 2. Calculate latitude using even/odd zone boundaries -// 3. Determine which latitude zone contains the aircraft -// 4. Calculate longitude based on the resolved latitude -// 5. Apply range corrections to get final position -// -// Mathematical Process: -// - Latitude zones are spaced 360°/60 = 6° apart for even messages -// - Latitude zones are spaced 360°/59 = ~6.1° apart for odd messages -// - The zone offset calculation resolves which 6° band contains the aircraft -// - Longitude calculation depends on latitude due to Earth's spherical geometry -// -// Note: This implementation uses a simplified approach. Production systems -// should also consider message timestamps to choose the most recent position. +// Without proper zone resolution, aircraft can appear 6+ degrees away from actual position. +// This implementation uses global decoding which can produce large errors without +// additional context about expected aircraft location. // // Parameters: // - aircraft: Aircraft struct to update with decoded position @@ -472,8 +471,21 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) { return } - // Choose the most recent position - aircraft.Latitude = latOdd // Use odd for now, should check timestamps + // Zone ambiguity resolution using receiver reference position + // Calculate which decoded latitude is closer to the receiver + distToEven := math.Abs(latEven - d.refLatitude) + distToOdd := math.Abs(latOdd - d.refLatitude) + + // Choose the latitude solution that's closer to the receiver position + if distToOdd < distToEven { + aircraft.Latitude = latOdd + fmt.Printf("CPR Zone: chose ODD lat=%.6f (dist=%.3f) over EVEN lat=%.6f (dist=%.3f) [ref=%.6f]\n", + latOdd, distToOdd, latEven, distToEven, d.refLatitude) + } else { + aircraft.Latitude = latEven + fmt.Printf("CPR Zone: chose EVEN lat=%.6f (dist=%.3f) over ODD lat=%.6f (dist=%.3f) [ref=%.6f]\n", + latEven, distToEven, latOdd, distToOdd, d.refLatitude) + } // Longitude calculation nl := d.nlFunction(aircraft.Latitude)