188 lines
5.6 KiB
Rust
188 lines
5.6 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::mpsc::{channel, Receiver};
|
|
|
|
use super::location::Locations;
|
|
|
|
/// ft
|
|
const MAX_ALTITUDE: u32 = 5_000;
|
|
/// s
|
|
const STATE_TIMEOUT: u64 = 180;
|
|
|
|
const IGNORED_CATEGORIES: &[u8] = &[
|
|
// Small (15,500..75,000) lbs
|
|
2,
|
|
// Large (75,000..300,000 lbs)
|
|
3,
|
|
// High Vortex Large
|
|
4,
|
|
// Heavy (> 300,000 lbs)
|
|
5,
|
|
];
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Event {
|
|
pub action: Action,
|
|
pub info: Info,
|
|
pub location: Arc<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum Action {
|
|
Appeared,
|
|
Disappeared,
|
|
Moved,
|
|
Ignored,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Info {
|
|
pub hex: String,
|
|
pub flight: Option<String>,
|
|
pub altitude_m: Option<f64>,
|
|
}
|
|
|
|
struct State {
|
|
info: Info,
|
|
position: Option<adsb_deku::cpr::Position>,
|
|
location: Option<Arc<String>>,
|
|
last: Instant,
|
|
}
|
|
|
|
impl State {
|
|
pub fn new(hex: String) -> Self {
|
|
State {
|
|
info: Info {
|
|
hex,
|
|
flight: None,
|
|
altitude_m: None,
|
|
},
|
|
position: None,
|
|
location: None,
|
|
last: Instant::now(),
|
|
}
|
|
}
|
|
|
|
pub fn update(&mut self, entry: &beast::aircrafts::Entry, locations: &Locations) -> Option<Event> {
|
|
self.last = Instant::now();
|
|
|
|
if let Some(flight) = entry.flight() {
|
|
self.info.flight = Some(flight.to_string());
|
|
}
|
|
if let Some(altitude_m) = entry.altitude_m() {
|
|
self.info.altitude_m = Some(altitude_m);
|
|
}
|
|
|
|
let pos = entry.position()?;
|
|
let coord = geo::Coordinate { x: pos.longitude, y: pos.latitude };
|
|
if let Some(old_pos) = self.position.replace(pos.clone()) {
|
|
if old_pos.longitude != pos.longitude || old_pos.latitude != pos.latitude {
|
|
let location = locations.find(&coord);
|
|
if location != self.location {
|
|
if let Some(location) = location {
|
|
self.location = Some(location.clone());
|
|
Some(Event {
|
|
action: Action::Moved,
|
|
location,
|
|
info: self.info.clone(),
|
|
})
|
|
} else {
|
|
// move to no location
|
|
self.location = None;
|
|
None
|
|
}
|
|
} else {
|
|
// pos moved, but no new location
|
|
None
|
|
}
|
|
} else {
|
|
// pos not moved
|
|
None
|
|
}
|
|
} else {
|
|
self.location = locations.find(&coord);
|
|
Some(Event {
|
|
action: Action::Appeared,
|
|
info: self.info.clone(),
|
|
location: self.location.clone()
|
|
.unwrap_or_else(|| Arc::new("irgendwo".to_owned())),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn run(host: &'static str, port: u16, locations: Locations) -> Receiver<Event> {
|
|
let (tx, rx) = channel(1);
|
|
|
|
tokio::spawn(async move {
|
|
let source = beast::aircrafts::Aircrafts::new();
|
|
source.connect(host, port);
|
|
|
|
let mut states = HashMap::new();
|
|
// ignore anything above MAX_ALTITUDE permanently
|
|
let mut ignored = HashSet::new();
|
|
loop {
|
|
let mut events = vec![];
|
|
for (icao_address, entry) in source.read().iter() {
|
|
let hex = format!("{}", icao_address);
|
|
let entry = entry.read().unwrap();
|
|
|
|
if entry.altitude.is_none() {
|
|
continue;
|
|
}
|
|
|
|
if entry.altitude.map(|altitude| altitude > MAX_ALTITUDE).unwrap_or(false) ||
|
|
entry.category.as_ref().map(|category| IGNORED_CATEGORIES.contains(&category.1)).unwrap_or(false)
|
|
{
|
|
if !ignored.contains(&hex) {
|
|
ignored.insert(hex.clone());
|
|
if let Some(_) = states.remove(&hex) {
|
|
events.push(Event {
|
|
action: Action::Ignored,
|
|
info: Info {
|
|
hex,
|
|
flight: entry.flight().map(|s| s.to_string()),
|
|
altitude_m: entry.altitude_m(),
|
|
},
|
|
location: Arc::new("ignoriert".to_owned()),
|
|
});
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if ignored.contains(&hex) {
|
|
continue;
|
|
}
|
|
|
|
if let Some(event) = states.entry(hex.clone())
|
|
.or_insert_with(|| State::new(hex.clone()))
|
|
.update(&entry, &locations) {
|
|
events.push(event);
|
|
}
|
|
}
|
|
|
|
states.retain(|_, state| {
|
|
if state.last + Duration::from_secs(STATE_TIMEOUT) < Instant::now() {
|
|
events.push(Event {
|
|
action: Action::Disappeared,
|
|
info: state.info
|
|
.clone(),
|
|
location: Arc::new("weg".to_owned()),
|
|
});
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
for event in events {
|
|
tx.send(event).await.unwrap();
|
|
}
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
}
|
|
});
|
|
|
|
rx
|
|
}
|