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, } #[derive(Debug, Clone, PartialEq)] pub enum Action { Appeared, Disappeared, Moved, Ignored, } #[derive(Debug, Clone)] pub struct Info { pub hex: String, pub flight: Option, pub altitude_m: Option, } struct State { info: Info, position: Option, location: Option>, 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 { 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 { 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 }