use super::adsb::ME;
use super::bds::bds05::AirbornePosition;
use super::bds::bds06::SurfacePosition;
use super::{TimedMessage, DF, ICAO};
use crate::data::airports::one_airport;
use deku::prelude::*;
use libm::fabs;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
const EARTH_RADIUS_KM: f64 = 6371.0;
const CPR_GLOBAL_MAX_TIME_DIFF_S: f64 = 30.0;
const CPR_LOCAL_MAX_AGE_S: f64 = 180.0;
const ACTIVE_AIRCRAFT_WINDOW_S: f64 = 300.0;
const SPEED_VALIDATION_MAX_GAP_S: f64 = 1800.0;
const MAX_AIRCRAFT_SPEED_KMH: f64 = 1200.0;
const MAX_POSITION_JUMP_KM: f64 = 500.0;
const MAX_REFERENCE_DISTANCE_KM: f64 = 1.0;
const AIRPORT_PROXIMITY_KM: f64 = 10.0;
fn haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let d_lat = (lat2 - lat1).to_radians();
let d_lon = (lon2 - lon1).to_radians();
let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin()
+ lat1.to_radians().cos()
* lat2.to_radians().cos()
* (d_lon / 2.0).sin()
* (d_lon / 2.0).sin();
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_KM * c }
fn dist_haversine(pos1: &Position, pos2: &Position) -> f64 {
haversine(pos1.latitude, pos1.longitude, pos2.latitude, pos2.longitude)
}
fn is_near_airport(pos: &Position, max_distance_km: f64) -> bool {
use crate::data::airports::AIRPORTS;
AIRPORTS.iter().any(|airport| {
haversine(pos.latitude, pos.longitude, airport.lat, airport.lon)
< max_distance_km
})
}
fn find_nearest_airport(
pos: &Position,
max_distance_km: f64,
) -> Option<String> {
use crate::data::airports::AIRPORTS;
AIRPORTS
.iter()
.filter_map(|airport| {
let distance = haversine(
pos.latitude,
pos.longitude,
airport.lat,
airport.lon,
);
if distance < max_distance_km {
Some((distance, airport.icao.clone()))
} else {
None
}
})
.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
.map(|(_, icao)| icao)
}
pub fn update_global_reference(
aircraft: &BTreeMap<ICAO, AircraftState>,
reference: &mut Option<Position>,
current_timestamp: f64,
) -> bool {
let lowest = aircraft
.values()
.filter(|state| {
current_timestamp - state.timestamp < ACTIVE_AIRCRAFT_WINDOW_S
})
.filter_map(|state| state.pos.map(|pos| (state.last_altitude, pos)))
.min_by(|a, b| match (a.0, b.0) {
(None, None) => std::cmp::Ordering::Equal, (None, Some(_)) => std::cmp::Ordering::Less, (Some(_), None) => std::cmp::Ordering::Greater, (Some(alt_a), Some(alt_b)) => alt_a.partial_cmp(&alt_b).unwrap(),
});
if let Some((_, pos)) = lowest {
*reference = Some(pos);
return true;
}
false
}
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, DekuRead, Copy, Clone,
)]
#[repr(u8)]
#[deku(id_type = "u8", bits = "1")]
#[serde(rename_all = "snake_case")]
pub enum CPRFormat {
Even = 0,
Odd = 1,
}
impl fmt::Display for CPRFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Even => "even",
Self::Odd => "odd",
}
)
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy)]
pub struct Position {
pub latitude: f64,
pub longitude: f64,
}
impl FromStr for Position {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let regex_list = [Regex::new(s).unwrap()];
if let Some(airport) = one_airport(®ex_list) {
return Ok(Position {
latitude: airport.lat,
longitude: airport.lon,
});
}
let parts: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
if parts.len() != 2 {
return Err("Invalid number of coordinates".to_string());
}
let latitude: f64 = parts[0]
.parse()
.map_err(|e| format!("Latitude parse error: {e}"))?;
let longitude: f64 = parts[1]
.parse()
.map_err(|e| format!("Longitude parse error: {e}"))?;
Ok(Position {
latitude,
longitude,
})
}
}
#[derive(Default)]
pub struct AircraftState {
timestamp: f64,
pos: Option<Position>,
last_altitude: Option<i32>,
pub airport: Option<String>,
odd_ts: f64,
odd_msg: Option<AirbornePosition>,
even_ts: f64,
even_msg: Option<AirbornePosition>,
}
const NZ: f64 = 15.0;
const CPR_MAX: f64 = 131_072.0;
#[rustfmt::skip]
fn nl(lat: f64) -> u64 {
let mut lat = lat;
if lat < 0.0 { lat = -lat; }
if lat < 29.911_356_86 {
if lat < 10.470_471_30 { return 59; }
if lat < 14.828_174_37 { return 58; }
if lat < 18.186_263_57 { return 57; }
if lat < 21.029_394_93 { return 56; }
if lat < 23.545_044_87 { return 55; }
if lat < 25.829_247_07 { return 54; }
if lat < 27.938_987_10 { return 53; }
return 52;
}
if lat < 44.194_549_51 {
if lat < 31.772_097_08 { return 51; }
if lat < 33.539_934_36 { return 50; }
if lat < 35.228_995_98 { return 49; }
if lat < 36.850_251_08 { return 48; }
if lat < 38.412_418_92 { return 47; }
if lat < 39.922_566_84 { return 46; }
if lat < 41.386_518_32 { return 45; }
if lat < 42.809_140_12 { return 44; }
return 43;
}
if lat < 59.954_592_77 {
if lat < 45.546_267_23 { return 42; }
if lat < 46.867_332_52 { return 41; }
if lat < 48.160_391_28 { return 40; }
if lat < 49.427_764_39 { return 39; }
if lat < 50.671_501_66 { return 38; }
if lat < 51.893_424_69 { return 37; }
if lat < 53.095_161_53 { return 36; }
if lat < 54.278_174_72 { return 35; }
if lat < 55.443_784_44 { return 34; }
if lat < 56.593_187_56 { return 33; }
if lat < 57.727_473_54 { return 32; }
if lat < 58.847_637_76 { return 31; }
return 30;
}
if lat < 61.049_177_74 { return 29; }
if lat < 62.132_166_59 { return 28; }
if lat < 63.204_274_79 { return 27; }
if lat < 64.266_165_23 { return 26; }
if lat < 65.318_453_10 { return 25; }
if lat < 66.361_710_08 { return 24; }
if lat < 67.396_467_74 { return 23; }
if lat < 68.423_220_22 { return 22; }
if lat < 69.442_426_31 { return 21; }
if lat < 70.454_510_75 { return 20; }
if lat < 71.459_864_73 { return 19; }
if lat < 72.458_845_45 { return 18; }
if lat < 73.451_774_42 { return 17; }
if lat < 74.438_934_16 { return 16; }
if lat < 75.420_562_57 { return 15; }
if lat < 76.396_843_91 { return 14; }
if lat < 77.367_894_61 { return 13; }
if lat < 78.333_740_83 { return 12; }
if lat < 79.294_282_25 { return 11; }
if lat < 80.249_232_13 { return 10; }
if lat < 81.198_013_49 { return 9; }
if lat < 82.139_569_81 { return 8; }
if lat < 83.071_994_45 { return 7; }
if lat < 83.991_735_63 { return 6; }
if lat < 84.891_661_91 { return 5; }
if lat < 85.755_416_21 { return 4; }
if lat < 86.535_369_98 { return 3; }
if lat < 87.000_000_00 { return 2; }
1
}
const D_LAT_EVEN: f64 = 360.0 / (4.0 * NZ);
const D_LAT_ODD: f64 = 360.0 / (4.0 * NZ - 1.0);
fn modulo(a: f64, b: f64) -> f64 {
a - b * libm::floor(a / b)
}
pub fn airborne_position(
oldest: &AirbornePosition,
latest: &AirbornePosition,
) -> Option<Position> {
let (even_frame, odd_frame) = match (oldest, latest) {
(
even @ AirbornePosition {
parity: CPRFormat::Even,
..
},
odd @ AirbornePosition {
parity: CPRFormat::Odd,
..
},
)
| (
odd @ AirbornePosition {
parity: CPRFormat::Odd,
..
},
even @ AirbornePosition {
parity: CPRFormat::Even,
..
},
) => (even, odd),
_ => return None,
};
let cpr_lat_even = f64::from(even_frame.lat_cpr) / CPR_MAX;
let cpr_lon_even = f64::from(even_frame.lon_cpr) / CPR_MAX;
let cpr_lat_odd = f64::from(odd_frame.lat_cpr) / CPR_MAX;
let cpr_lon_odd = f64::from(odd_frame.lon_cpr) / CPR_MAX;
let j = libm::floor(59.0 * cpr_lat_even - 60.0 * cpr_lat_odd + 0.5);
let mut lat_even = D_LAT_EVEN * (modulo(j, 60.) + cpr_lat_even);
let mut lat_odd = D_LAT_ODD * (modulo(j, 59.) + cpr_lat_odd);
if lat_even >= 270.0 {
lat_even -= 360.0;
}
if lat_odd >= 270.0 {
lat_odd -= 360.0;
}
if !(-90. ..=90.).contains(&lat_even) || !(-90. ..=90.).contains(&lat_odd) {
return None;
}
if nl(lat_even) != nl(lat_odd) {
return None;
}
let lat = if latest == even_frame {
lat_even
} else {
lat_odd
};
let cpr_format = &latest.parity;
let (p, c) = if cpr_format == &CPRFormat::Even {
(0, cpr_lon_even)
} else {
(1, cpr_lon_odd)
};
let ni = std::cmp::max(nl(lat) - p, 1) as f64;
let m = libm::floor(
cpr_lon_even * (nl(lat) - 1) as f64 - cpr_lon_odd * nl(lat) as f64
+ 0.5,
);
let r = modulo(m, ni);
let mut lon = (360.0 / ni) * (r + c);
if lon >= 180.0 {
lon -= 360.0;
}
Some(Position {
latitude: lat,
longitude: lon,
})
}
pub fn airborne_position_with_reference(
msg: &AirbornePosition,
latitude_ref: f64,
longitude_ref: f64,
) -> Option<Position> {
let cpr_lat = f64::from(msg.lat_cpr) / CPR_MAX;
let cpr_lon = f64::from(msg.lon_cpr) / CPR_MAX;
let d_lat = if msg.parity == CPRFormat::Even {
360. / 60.
} else {
360. / 59.
};
let j = libm::floor(0.5 + latitude_ref / d_lat - cpr_lat);
let lat = d_lat * (j + cpr_lat);
if !(-90. ..=90.).contains(&lat) {
return None;
}
if fabs(lat - latitude_ref) > d_lat / 2. {
return None;
}
let ni = if msg.parity == CPRFormat::Even {
nl(lat)
} else {
nl(lat) - 1
};
let d_lon = if ni > 0 { 360. / ni as f64 } else { 360. };
let m = libm::floor(0.5 + longitude_ref / d_lon - cpr_lon);
let lon = d_lon * (m + cpr_lon);
if fabs(lon - longitude_ref) > d_lon / 2. {
return None;
}
Some(Position {
latitude: lat,
longitude: lon,
})
}
pub fn surface_position_with_reference(
msg: &SurfacePosition,
latitude_ref: f64,
longitude_ref: f64,
) -> Option<Position> {
let cpr_lat = f64::from(msg.lat_cpr) / CPR_MAX;
let cpr_lon = f64::from(msg.lon_cpr) / CPR_MAX;
let d_lat = if msg.parity == CPRFormat::Even {
90. / 60.
} else {
90. / 59.
};
let j = libm::floor(0.5 + latitude_ref / d_lat - cpr_lat);
let lat = d_lat * (j + cpr_lat);
if !(-90. ..=90.).contains(&lat) {
return None;
}
if fabs(lat - latitude_ref) > d_lat / 2. {
return None;
}
let ni = if msg.parity == CPRFormat::Even {
nl(lat)
} else {
nl(lat) - 1
};
let d_lon = if ni > 0 { 90. / ni as f64 } else { 90. };
let m = libm::floor(0.5 + longitude_ref / d_lon - cpr_lon);
let lon = d_lon * (m + cpr_lon);
if fabs(lon - longitude_ref) > d_lon / 2. {
return None;
}
Some(Position {
latitude: lat,
longitude: lon,
})
}
pub type UpdateIf = Option<Box<dyn Fn(&AirbornePosition) -> bool>>;
pub fn decode_position(
message: &mut ME,
timestamp: f64,
icao24: &ICAO,
aircraft: &mut BTreeMap<ICAO, AircraftState>,
reference: &mut Option<Position>,
update_reference: &UpdateIf,
) {
let latest = aircraft.entry(*icao24).or_insert(AircraftState {
timestamp,
pos: None,
last_altitude: None,
airport: None,
odd_ts: timestamp,
odd_msg: None,
even_ts: timestamp,
even_msg: None,
});
match message {
ME::BDS05 {
tc: _,
inner: airborne,
} => {
let mut pos: Option<Position> = None;
let latest_timestamp = match airborne.parity {
CPRFormat::Even => latest.odd_ts,
CPRFormat::Odd => latest.even_ts,
};
let latest_msg = match airborne.parity {
CPRFormat::Even => latest.odd_msg,
CPRFormat::Odd => latest.even_msg,
};
if (timestamp - latest_timestamp) < 0. {
return;
}
if (timestamp - latest_timestamp).abs() < CPR_GLOBAL_MAX_TIME_DIFF_S
{
pos = match latest_msg {
Some(oldest) => airborne_position(&oldest, airborne),
None => None,
};
}
if pos.is_none()
& ((timestamp - latest.timestamp) < CPR_LOCAL_MAX_AGE_S)
{
if let Some(latest_pos) = latest.pos {
pos = airborne_position_with_reference(
airborne,
latest_pos.latitude,
latest_pos.longitude,
)
}
}
if let Some(new_pos) = pos {
if let Some(latest_pos) = latest.pos {
let distance = dist_haversine(&new_pos, &latest_pos);
let time_diff_seconds =
(timestamp - latest.timestamp).abs();
if time_diff_seconds < SPEED_VALIDATION_MAX_GAP_S {
let time_diff_hours = time_diff_seconds / 3600.0;
if time_diff_hours > 0.0 {
let speed_kmh = distance / time_diff_hours;
if speed_kmh > MAX_AIRCRAFT_SPEED_KMH {
pos = None;
}
}
}
if pos.is_some()
&& distance > MAX_POSITION_JUMP_KM
&& time_diff_seconds < CPR_GLOBAL_MAX_TIME_DIFF_S
{
pos = None
}
}
}
if let Some(pos) = pos {
airborne.latitude = Some(pos.latitude);
airborne.longitude = Some(pos.longitude);
latest.pos = Some(pos);
latest.timestamp = timestamp;
latest.last_altitude = airborne.alt;
latest.airport = None;
if let Some(update_reference) = update_reference {
if update_reference(airborne) {
*reference = Some(Position {
latitude: pos.latitude,
longitude: pos.longitude,
})
}
}
}
match airborne.parity {
CPRFormat::Even => {
latest.even_msg = Some(*airborne);
latest.even_ts = timestamp
}
CPRFormat::Odd => {
latest.odd_msg = Some(*airborne);
latest.odd_ts = timestamp
}
}
}
ME::BDS06 {
tc: _,
inner: surface,
} => {
let mut pos = None;
if let Some(latest_pos) = latest.pos {
let surface_pos = surface_position_with_reference(
surface,
latest_pos.latitude,
latest_pos.longitude,
);
if surface_pos.is_some()
&& dist_haversine(&latest_pos, &surface_pos.unwrap())
< MAX_REFERENCE_DISTANCE_KM
{
pos = surface_pos;
}
}
if let Some(reference) = reference {
if pos.is_none() {
let candidate_pos = surface_position_with_reference(
surface,
reference.latitude,
reference.longitude,
);
if let Some(candidate) = candidate_pos {
if latest.pos.is_none() {
if is_near_airport(&candidate, AIRPORT_PROXIMITY_KM)
{
pos = Some(candidate);
}
} else {
pos = candidate_pos;
}
} else {
pos = candidate_pos;
}
}
}
if let Some(pos) = pos {
surface.latitude = Some(pos.latitude);
surface.longitude = Some(pos.longitude);
latest.pos = Some(pos);
latest.timestamp = timestamp;
latest.last_altitude = None;
if latest.airport.is_none() {
latest.airport =
find_nearest_airport(&pos, AIRPORT_PROXIMITY_KM);
}
}
}
_ => (),
}
}
pub fn decode_positions(
res: &mut [TimedMessage],
reference: Option<Position>,
update_reference: &UpdateIf,
) {
let mut aircraft: BTreeMap<ICAO, AircraftState> = BTreeMap::new();
let mut reference = reference;
let _: Vec<()> = res
.iter_mut()
.map(|msg| {
if let Some(message) = &mut msg.message {
match &mut message.df {
DF::ExtendedSquitterADSB(adsb) => decode_position(
&mut adsb.message,
msg.timestamp,
&adsb.icao24,
&mut aircraft,
&mut reference,
update_reference,
),
DF::ExtendedSquitterTisB { cf, .. } => decode_position(
&mut cf.me,
msg.timestamp,
&cf.aa,
&mut aircraft,
&mut reference,
update_reference,
),
_ => {}
}
}
})
.collect();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
use approx::assert_relative_eq;
use hexlit::hex;
#[test]
fn decode_airporne_position() {
let b1 = hex!("8D40058B58C901375147EFD09357");
let b2 = hex!("8D40058B58C904A87F402D3B8C59");
let (_, msg1) = Message::from_bytes((&b1, 0)).unwrap();
let (_, msg2) = Message::from_bytes((&b2, 0)).unwrap();
let (msg1, msg2) = match (msg1.df, msg2.df) {
(ExtendedSquitterADSB(msg1), ExtendedSquitterADSB(msg2)) => {
match (msg1.message, msg2.message) {
(
ME::BDS05 { inner: m1, .. },
ME::BDS05 { inner: m2, .. },
) => (m1, m2),
_ => unreachable!(),
}
}
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = airborne_position(&msg1, &msg2).unwrap();
assert_relative_eq!(latitude, 49.81755, max_relative = 1e-3);
assert_relative_eq!(longitude, 6.08442, max_relative = 1e-3);
let b3 = hex!("8d4d224f58bf07c2d41a9a353d70");
let b4 = hex!("8d4d224f58bf003b221b34aa5b8d");
let (_, msg1) = Message::from_bytes((&b3, 0)).unwrap();
let (_, msg2) = Message::from_bytes((&b4, 0)).unwrap();
let (msg1, msg2) = match (msg1.df, msg2.df) {
(ExtendedSquitterADSB(msg1), ExtendedSquitterADSB(msg2)) => {
match (msg1.message, msg2.message) {
(
ME::BDS05 { inner: m1, .. },
ME::BDS05 { inner: m2, .. },
) => (m1, m2),
_ => unreachable!(),
}
}
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = airborne_position(&msg1, &msg2).unwrap();
assert_relative_eq!(latitude, 42.346, max_relative = 1e-3);
assert_relative_eq!(longitude, 0.4347, max_relative = 1e-3);
}
#[test]
fn decode_airporne_position_with_reference() {
let bytes = hex!("8D40058B58C901375147EFD09357");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
let msg = match msg.df {
ExtendedSquitterADSB(msg) => match msg.message {
ME::BDS05 { inner: me, .. } => me,
_ => unreachable!(),
},
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = airborne_position_with_reference(&msg, 49.0, 6.0).unwrap();
assert_relative_eq!(latitude, 49.82410, max_relative = 1e-3);
assert_relative_eq!(longitude, 6.06785, max_relative = 1e-3);
let bytes = hex!("8D40058B58C904A87F402D3B8C59");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
let msg = match msg.df {
ExtendedSquitterADSB(msg) => match msg.message {
ME::BDS05 { inner: me, .. } => me,
_ => unreachable!(),
},
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = airborne_position_with_reference(&msg, 49.0, 6.0).unwrap();
assert_relative_eq!(latitude, 49.81755, max_relative = 1e-3);
assert_relative_eq!(longitude, 6.08442, max_relative = 1e-3);
}
#[test]
fn decode_airborne_position_with_reference_numerical_challenge() {
let lat_ref = 30.508474576271183; let lon_ref = 7.2 * 5.0 + 3e-15;
let bytes = hex!("8d06a15358bf17ff7d4a84b47b95");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
let msg = match msg.df {
ExtendedSquitterADSB(msg) => match msg.message {
ME::BDS05 { inner: me, .. } => me,
_ => unreachable!(),
},
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = airborne_position_with_reference(&msg, lat_ref, lon_ref).unwrap();
assert_relative_eq!(latitude, 30.50540, max_relative = 1e-3);
assert_relative_eq!(longitude, 33.44787, max_relative = 1e-3);
}
#[test]
fn decode_surface_position_with_reference() {
let bytes = hex!("8c4841753aab238733c8cd4020b1");
let (_, msg) = Message::from_bytes((&bytes, 0)).unwrap();
let msg = match msg.df {
ExtendedSquitterADSB(msg) => match msg.message {
ME::BDS06 { inner: me, .. } => me,
_ => unreachable!(),
},
_ => unreachable!(),
};
let Position {
latitude,
longitude,
} = surface_position_with_reference(&msg, 51.99, 4.375).unwrap();
assert_relative_eq!(latitude, 52.32061, max_relative = 1e-3);
assert_relative_eq!(longitude, 4.73473, max_relative = 1e-3);
}
#[test]
fn test_regression_50km_threshold_check() {
let icao24 = ICAO::from_str("3949e8").unwrap();
let mut aircraft = BTreeMap::new();
let mut reference = None;
let msgs1 = vec![
(hex!("8d3949e858ab05a2c11a30c334bf"), 100.0), (hex!("8d3949e858ab0211c74e97f9a5f3"), 103.0), (hex!("8d3949e858ab0211b54ecc0c36dc"), 106.0), (hex!("8d3949e890ab05a26b1b2e2b2da0"), 109.0), ];
for (msg, ts) in msgs1 {
let (_, mut m) = Message::from_bytes((&msg, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = m.df {
decode_position(
&mut adsb.message,
ts,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
}
}
let msgs2 = vec![
(hex!("8f3949e86cb503a343e9c6ecf4cd"), 115.0), (hex!("8f3949e86cb5073d9daa5f6f9d4c"), 116.0), ];
for (msg, ts) in msgs2 {
let (_, mut m) = Message::from_bytes((&msg, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = m.df {
decode_position(
&mut adsb.message,
ts,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
}
}
let state = aircraft.get(&icao24).unwrap();
if let Some(last_pos) = state.pos {
assert!(
last_pos.latitude > 30.0 && last_pos.latitude < 90.0,
"Position should be valid"
);
}
}
#[test]
fn test_regression_position_oscillations_filtered() {
let icao24 = ICAO::from_str("3949e8").unwrap();
let mut aircraft = BTreeMap::new();
let mut reference = None;
let initial_msgs = vec![
(hex!("8d3949e858ab05a2c11a30c334bf"), 100.0), (hex!("8d3949e858ab0211c74e97f9a5f3"), 103.0), ];
for (msg, ts) in initial_msgs {
let (_, mut m) = Message::from_bytes((&msg, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = m.df {
decode_position(
&mut adsb.message,
ts,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
}
}
let state = aircraft.get(&icao24).unwrap();
let p0 = state.pos.expect("Should have initial position");
let jump_msgs = vec![
(hex!("8f3949e86cb503a343e9c6ecf4cd"), 110.0), (hex!("8f3949e86cb5073d9daa5f6f9d4c"), 111.0), ];
for (msg, ts) in jump_msgs {
let (_, mut m) = Message::from_bytes((&msg, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = m.df {
decode_position(
&mut adsb.message,
ts,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
}
}
let state_after = aircraft.get(&icao24).unwrap();
if let Some(p1) = state_after.pos {
let distance =
haversine(p0.latitude, p0.longitude, p1.latitude, p1.longitude);
let time_diff = 8.0; let implied_speed_kmh = (distance / time_diff) * 3600.0;
eprintln!(
"Position jump: {:.0} km in {}s = {:.0} km/h",
distance, time_diff, implied_speed_kmh
);
assert!(distance < 200.0 || time_diff > 600.0,
"Large position jumps should be filtered (got {:.0} km in {:.0}s = {:.0} km/h - physically impossible!)",
distance, time_diff, implied_speed_kmh);
} else {
eprintln!("✓ Position was correctly rejected (pos=None)");
}
}
#[test]
fn test_regression_position_decode_after_large_gap() {
let icao24 = ICAO::from_str("39c424").unwrap();
let mut aircraft = BTreeMap::new();
let mut reference = None;
let before_gap = vec![
(hex!("8d39c42469b974b6a05ab914104f"), 1754260360.77), (hex!("8d39c42478b9713304a5e41f6bc1"), 1754260361.70), (hex!("8d39c42478b974b6325a6e2724f8"), 1754260362.74), ];
let mut positions_before = 0;
for (frame, timestamp) in before_gap {
let (_, mut msg) = Message::from_bytes((&frame, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = msg.df {
decode_position(
&mut adsb.message,
timestamp,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
if let ME::BDS05 {
inner: ref airborne,
..
} = adsb.message
{
if airborne.latitude.is_some() {
positions_before += 1;
}
}
}
}
assert!(
positions_before >= 1,
"Should decode at least one position before gap"
);
let state_before = aircraft.get(&icao24).unwrap();
let pos_before =
state_before.pos.expect("Should have position before gap");
eprintln!(
"Position before gap: {:.4}°N, {:.4}°E",
pos_before.latitude, pos_before.longitude
);
let after_gap = vec![
(hex!("8f39c42490c3746e522aed52d00d"), 1754270452.34), (hex!("8f39c42490c3746e722ad4e90929"), 1754270452.83), (hex!("8f39c42490c370ea0050448bbfd0"), 1754270453.87), (hex!("8f39c42490c3746ebc2a9937916b"), 1754270454.31), (hex!("8f39c42490c380ea3650192a742d"), 1754270454.80), ];
let mut positions_after = 0;
for (frame, timestamp) in after_gap {
let (_, mut msg) = Message::from_bytes((&frame, 0)).unwrap();
if let DF::ExtendedSquitterADSB(ref mut adsb) = msg.df {
decode_position(
&mut adsb.message,
timestamp,
&icao24,
&mut aircraft,
&mut reference,
&None,
);
if let ME::BDS05 {
inner: ref airborne,
..
} = adsb.message
{
if let Some(latitude) = airborne.latitude {
positions_after += 1;
eprintln!(
"Decoded position after gap: {:.4}°N, {:.4}°E",
latitude,
airborne.longitude.unwrap()
);
}
}
}
}
assert!(
positions_after >= 2,
"Should decode at least 2 positions after gap, got {}",
positions_after
);
let state_after = aircraft.get(&icao24).unwrap();
let pos_after =
state_after.pos.expect("Should have position after gap");
eprintln!(
"Position after gap: {:.4}°N, {:.4}°E",
pos_after.latitude, pos_after.longitude
);
let lon_change = (pos_before.longitude - pos_after.longitude).abs();
assert!(
lon_change > 20.0,
"Longitude should change significantly after gap (expected ~26°, got {:.1}°)",
lon_change
);
assert!(
pos_after.latitude > 43.0 && pos_after.latitude < 44.0,
"Latitude should be in Bulgaria/Romania region, got {:.4}°N",
pos_after.latitude
);
assert!(
pos_after.longitude > 26.0 && pos_after.longitude < 27.0,
"Longitude should be in Bulgaria/Romania region, got {:.4}°E",
pos_after.longitude
);
}
}