Implement aircraft position validation to fix obviously wrong trails
Add comprehensive position validation to filter out impossible aircraft movements and improve trail accuracy as requested in issue #16. **Position Validation Features:** - Geographic coordinate bounds checking (lat: -90 to 90, lon: -180 to 180) - Altitude validation (range: -500ft to 60,000ft) - Speed validation (max: 2000 knots ≈ Mach 3) - Distance jump validation (max: 500 nautical miles) - Time consistency validation (reject out-of-order timestamps) - Speed consistency warnings (reported vs. implied speed) **Integration Points:** - updateHistories(): Validates before adding to position history (trails) - mergeAircraftData(): Validates before updating aircraft position state - Comprehensive logging with ICAO identification for debugging **Validation Logic:** - Rejects obviously wrong positions that would create impossible flight paths - Warns about high but possible speeds (>800 knots) for monitoring - Maintains detailed logs: [POSITION_VALIDATION] ICAO ABCDEF: REJECTED/WARNING - Performance optimized with early returns and minimal overhead **Result:** - Users see only realistic aircraft trails and movements - Obviously wrong ADS-B data (teleporting, impossible speeds) filtered out - Debug logs provide visibility into validation decisions - Clean flight tracking without zigzag patterns or position jumps Fixes #16 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ec7d9e02af
commit
9242852c01
1 changed files with 207 additions and 24 deletions
|
|
@ -22,6 +22,7 @@ package merger
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -33,8 +34,31 @@ import (
|
|||
const (
|
||||
// MaxDistance represents an infinite distance for initialization
|
||||
MaxDistance = float64(999999)
|
||||
|
||||
// Position validation constants
|
||||
MaxSpeedKnots = 2000.0 // Maximum plausible aircraft speed (roughly Mach 3 at cruise altitude)
|
||||
MaxDistanceNautMiles = 500.0 // Maximum position jump distance in nautical miles
|
||||
MaxAltitudeFeet = 60000 // Maximum altitude in feet (commercial ceiling ~FL600)
|
||||
MinAltitudeFeet = -500 // Minimum altitude (below sea level but allow for dead sea, etc.)
|
||||
|
||||
// Earth coordinate bounds
|
||||
MinLatitude = -90.0
|
||||
MaxLatitude = 90.0
|
||||
MinLongitude = -180.0
|
||||
MaxLongitude = 180.0
|
||||
|
||||
// Conversion factors
|
||||
KnotsToKmh = 1.852
|
||||
NmToKm = 1.852
|
||||
)
|
||||
|
||||
// ValidationResult represents the result of position validation checks.
|
||||
type ValidationResult struct {
|
||||
Valid bool // Whether the position passed all validation checks
|
||||
Errors []string // List of validation failures for debugging
|
||||
Warnings []string // List of potential issues (not blocking)
|
||||
}
|
||||
|
||||
// Source represents a data source (dump1090 receiver or similar ADS-B source).
|
||||
// It contains both static configuration and dynamic status information used
|
||||
// for data fusion decisions and source monitoring.
|
||||
|
|
@ -417,8 +441,19 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
|||
// - sourceID: Identifier of source providing new data
|
||||
// - timestamp: Timestamp of new data
|
||||
func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, sourceID string, timestamp time.Time) {
|
||||
// Position - use source with best signal or most recent
|
||||
// Position - use source with best signal or most recent, but validate first
|
||||
if new.Latitude != 0 && new.Longitude != 0 {
|
||||
// Always validate position before considering update
|
||||
validation := m.validatePosition(new, state, timestamp)
|
||||
|
||||
if !validation.Valid {
|
||||
// Log validation errors and skip position update
|
||||
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
|
||||
for _, err := range validation.Errors {
|
||||
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED position update - %s", icaoHex, err)
|
||||
}
|
||||
} else {
|
||||
// Position is valid, proceed with normal logic
|
||||
updatePosition := false
|
||||
|
||||
if state.Latitude == 0 {
|
||||
|
|
@ -442,6 +477,13 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
}
|
||||
}
|
||||
|
||||
// Log warnings even if position is valid
|
||||
for _, warning := range validation.Warnings {
|
||||
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
|
||||
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
|
||||
}
|
||||
}
|
||||
|
||||
// Altitude - use most recent
|
||||
if new.Altitude != 0 {
|
||||
state.Altitude = new.Altitude
|
||||
|
|
@ -530,14 +572,31 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
|||
// - signal: Signal strength measurement
|
||||
// - timestamp: When this data was received
|
||||
func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) {
|
||||
// Position history
|
||||
// Position history with validation
|
||||
if aircraft.Latitude != 0 && aircraft.Longitude != 0 {
|
||||
// Validate position before adding to history
|
||||
validation := m.validatePosition(aircraft, state, timestamp)
|
||||
|
||||
if validation.Valid {
|
||||
state.PositionHistory = append(state.PositionHistory, PositionPoint{
|
||||
Time: timestamp,
|
||||
Latitude: aircraft.Latitude,
|
||||
Longitude: aircraft.Longitude,
|
||||
Source: sourceID,
|
||||
})
|
||||
} else {
|
||||
// Log validation errors for debugging
|
||||
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
||||
for _, err := range validation.Errors {
|
||||
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED - %s", icaoHex, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Log warnings even for valid positions
|
||||
for _, warning := range validation.Warnings {
|
||||
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
||||
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
|
||||
}
|
||||
}
|
||||
|
||||
// Signal history
|
||||
|
|
@ -809,6 +868,130 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64)
|
|||
return distance, bearing
|
||||
}
|
||||
|
||||
// validatePosition performs comprehensive validation of aircraft position data to filter out
|
||||
// obviously incorrect flight paths and implausible position updates.
|
||||
//
|
||||
// This function implements multiple validation checks to improve data quality:
|
||||
//
|
||||
// 1. **Coordinate Validation**: Ensures latitude/longitude are within Earth's bounds
|
||||
// 2. **Altitude Validation**: Rejects impossible altitudes (negative or > FL600)
|
||||
// 3. **Speed Validation**: Calculates implied speed and rejects >Mach 3 movements
|
||||
// 4. **Distance Validation**: Rejects position jumps >500nm without time justification
|
||||
// 5. **Time Validation**: Ensures timestamps are chronologically consistent
|
||||
//
|
||||
// Parameters:
|
||||
// - aircraft: New aircraft position data to validate
|
||||
// - state: Current aircraft state with position history
|
||||
// - timestamp: Timestamp of the new position data
|
||||
//
|
||||
// Returns:
|
||||
// - ValidationResult with valid flag and detailed error/warning messages
|
||||
func (m *Merger) validatePosition(aircraft *modes.Aircraft, state *AircraftState, timestamp time.Time) *ValidationResult {
|
||||
result := &ValidationResult{
|
||||
Valid: true,
|
||||
Errors: make([]string, 0),
|
||||
Warnings: make([]string, 0),
|
||||
}
|
||||
|
||||
// Skip validation if no position data
|
||||
if aircraft.Latitude == 0 && aircraft.Longitude == 0 {
|
||||
return result // No position to validate
|
||||
}
|
||||
|
||||
// 1. Geographic coordinate validation
|
||||
if aircraft.Latitude < MinLatitude || aircraft.Latitude > MaxLatitude {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Invalid latitude: %.6f (must be between %.1f and %.1f)",
|
||||
aircraft.Latitude, MinLatitude, MaxLatitude))
|
||||
}
|
||||
|
||||
if aircraft.Longitude < MinLongitude || aircraft.Longitude > MaxLongitude {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Invalid longitude: %.6f (must be between %.1f and %.1f)",
|
||||
aircraft.Longitude, MinLongitude, MaxLongitude))
|
||||
}
|
||||
|
||||
// 2. Altitude validation
|
||||
if aircraft.Altitude != 0 { // Only validate non-zero altitudes
|
||||
if aircraft.Altitude < MinAltitudeFeet {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (below minimum %d)",
|
||||
aircraft.Altitude, MinAltitudeFeet))
|
||||
}
|
||||
|
||||
if aircraft.Altitude > MaxAltitudeFeet {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (above maximum %d)",
|
||||
aircraft.Altitude, MaxAltitudeFeet))
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Speed and distance validation (requires position history)
|
||||
if len(state.PositionHistory) > 0 && state.Latitude != 0 && state.Longitude != 0 {
|
||||
lastPos := state.PositionHistory[len(state.PositionHistory)-1]
|
||||
|
||||
// Calculate distance between positions
|
||||
distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude,
|
||||
aircraft.Latitude, aircraft.Longitude)
|
||||
|
||||
// Calculate time difference
|
||||
timeDiff := timestamp.Sub(lastPos.Time).Seconds()
|
||||
|
||||
if timeDiff > 0 {
|
||||
// Calculate implied speed in knots
|
||||
distanceNm := distance / NmToKm
|
||||
speedKnots := (distanceNm / timeDiff) * 3600 // Convert to knots per hour
|
||||
|
||||
// Distance validation: reject jumps >500nm
|
||||
if distanceNm > MaxDistanceNautMiles {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible position jump: %.1f nm in %.1f seconds (max allowed: %.1f nm)",
|
||||
distanceNm, timeDiff, MaxDistanceNautMiles))
|
||||
}
|
||||
|
||||
// Speed validation: reject >2000 knots (roughly Mach 3)
|
||||
if speedKnots > MaxSpeedKnots {
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible speed: %.0f knots (max allowed: %.0f knots)",
|
||||
speedKnots, MaxSpeedKnots))
|
||||
}
|
||||
|
||||
// Warning for high but possible speeds (>800 knots)
|
||||
if speedKnots > 800 && speedKnots <= MaxSpeedKnots {
|
||||
result.Warnings = append(result.Warnings, fmt.Sprintf("High speed detected: %.0f knots", speedKnots))
|
||||
}
|
||||
} else if timeDiff < 0 {
|
||||
// 4. Time validation: reject out-of-order timestamps
|
||||
result.Valid = false
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("Out-of-order timestamp: %.1f seconds in the past", -timeDiff))
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Aircraft-specific validations based on reported speed vs. position
|
||||
if aircraft.GroundSpeed > 0 && len(state.PositionHistory) > 0 {
|
||||
// Check if reported ground speed is consistent with position changes
|
||||
lastPos := state.PositionHistory[len(state.PositionHistory)-1]
|
||||
distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude,
|
||||
aircraft.Latitude, aircraft.Longitude)
|
||||
timeDiff := timestamp.Sub(lastPos.Time).Seconds()
|
||||
|
||||
if timeDiff > 0 {
|
||||
distanceNm := distance / NmToKm
|
||||
impliedSpeed := (distanceNm / timeDiff) * 3600
|
||||
reportedSpeed := float64(aircraft.GroundSpeed)
|
||||
|
||||
// Warning if speeds differ significantly (>100 knots difference)
|
||||
if math.Abs(impliedSpeed-reportedSpeed) > 100 && reportedSpeed > 50 {
|
||||
result.Warnings = append(result.Warnings,
|
||||
fmt.Sprintf("Speed inconsistency: reported %d knots, implied %.0f knots",
|
||||
aircraft.GroundSpeed, impliedSpeed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Close closes the merger and releases resources
|
||||
func (m *Merger) Close() error {
|
||||
m.mu.Lock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue