This commit is contained in:
Astro 2021-10-28 03:36:18 +02:00
commit 7da29c1070
7 changed files with 1580 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1200
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "heliwatch"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
geo = "0.18"

13
fetch_locations.sh Executable file
View File

@ -0,0 +1,13 @@
#!/usr/bin/env sh
overpass() {
curl -X POST -d "data=[out:json];$1" http://overpass-api.de/api/interpreter
}
bbox="50.8,13,51.3,14.5"
Q=""
for level in 7 8 9 10 11 ; do
Q=$Q'way["admin_level"="'$level'"]('$bbox'); relation["admin_level"="'$level'"]('$bbox');'
done
echo overpass "($Q); out body; >; out skel;" \> locations.json

198
src/adsb.rs Normal file
View File

@ -0,0 +1,198 @@
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use serde::Deserialize;
use tokio::sync::mpsc::{channel, Receiver};
use super::location::Locations;
/// ft
const MAX_ALTITUDE: u32 = 50000; //1800;
/// s
const STATE_TIMEOUT: u64 = 180;
#[derive(Deserialize, Debug, Clone)]
pub struct Info {
hex: String,
flight: String,
lat: f64,
lon: f64,
/// ft
altitude: u32,
track: u32,
/// kts
speed: u32,
}
impl Info {
pub fn get_flight(&self) -> Option<String> {
let mut i = self.flight.len();
while i > 0 && self.flight.chars().nth(i - 1).unwrap().is_whitespace() {
i -= 1;
}
if i > 0 {
Some(self.flight[0..i].to_string())
} else {
None
}
}
pub fn get_altitude_m(&self) -> f64 {
self.altitude as f64 * 0.3048
}
pub fn get_speed_kph(&self) -> f64 {
self.speed as f64 * 1.852
}
}
#[derive(Debug, Clone)]
pub struct Event {
pub action: Action,
pub info: Info,
pub location: Arc<String>,
}
#[derive(Debug, Clone)]
pub enum Action {
Appeared,
Disappeared,
Moved,
}
struct State {
info: Option<Info>,
location: Option<Arc<String>>,
last: Instant,
}
impl State {
pub fn new() -> Self {
State {
info: None,
location: None,
last: Instant::now(),
}
}
pub fn update(&mut self, info: Info, locations: &Locations) -> Option<Event> {
self.last = Instant::now();
let coord = geo::Coordinate { x: info.lon, y: info.lat };
if let Some(old_info) = self.info.replace(info) {
let info = self.info.as_ref().unwrap();
if old_info.lon != info.lon || old_info.lat != info.lat {
let location = locations.find(&coord);
if location != self.location {
if let Some(location) = location {
self.location = Some(location.clone());
Some(Event {
action: Action::Moved,
info: self.info.clone().unwrap(),
location,
})
} else {
if self.location.is_some() {
println!("{}: move to nowhere", info.flight);
}
// 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().unwrap(),
location: self.location.clone()
.unwrap_or_else(|| Arc::new("irgendwo".to_owned())),
})
}
}
}
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
async fn fetch(url: &str) -> Result<Vec<Info>> {
let result = reqwest::get(url)
.await?
.json()
.await?;
Ok(result)
}
pub async fn run(url: &'static str, locations: Locations) -> Receiver<Event> {
let (tx, rx) = channel(1);
tokio::spawn(async move {
let mut states = HashMap::new();
// ignore anything above MAX_ALTITUDE permanently
let mut ignored = HashSet::new();
// cache that lives longer than dump1090's
let mut flights = HashMap::new();
loop {
let infos = match fetch(url).await {
Ok(infos) => infos,
Err(e) => {
eprintln!("{}", e);
continue;
}
};
for mut info in infos {
if info.altitude > MAX_ALTITUDE {
if !ignored.contains(&info.hex) {
states.remove(&info.hex);
ignored.insert(info.hex);
}
continue;
}
if ignored.contains(&info.hex) {
continue;
}
if let Some(flight) = info.get_flight() {
flights.insert(info.hex.clone(), flight);
} else if let Some(flight) = flights.get(&info.hex) {
info.flight = flight.to_string();
} else {
continue;
};
if let Some(event) = states.entry(info.hex.clone())
.or_insert(State::new())
.update(info, &locations) {
tx.send(event).await.unwrap();
}
}
let mut events = vec![];
states.retain(|_, state| {
if state.last + Duration::from_secs(STATE_TIMEOUT) < Instant::now() {
events.push(Event {
action: Action::Disappeared,
info: state.info.clone().unwrap(),
location: Arc::new("weg".to_owned()),
});
false
} else {
true
}
});
for event in events {
tx.send(event).await.unwrap();
}
std::thread::sleep(std::time::Duration::from_secs(1));
}
});
rx
}

143
src/location.rs Normal file
View File

@ -0,0 +1,143 @@
use std::collections::HashMap;
use std::fs::File;
use std::sync::Arc;
use geo::prelude::{Area, Contains};
type Polygon = geo::Polygon<f64>;
struct Location {
name: Arc<String>,
polys: Vec<Arc<Polygon>>,
area: f64,
}
impl Location {
pub fn new(name: &str, polys: Vec<Arc<Polygon>>) -> Self {
let area = polys.iter()
.map(|poly| poly.unsigned_area())
.sum();
Location {
name: Arc::new(name.to_owned()),
polys,
area,
}
}
pub fn contains(&self, coord: &geo::Coordinate<f64>) -> bool {
self.polys.iter().any(|poly| poly.contains(coord))
}
}
pub struct Locations {
locations: Vec<Location>,
}
impl Locations {
pub fn load(file: &str) -> Self {
println!("Loading {}...", file);
let json: serde_json::Value = serde_json::from_reader(File::open(file).unwrap())
.unwrap();
let obj = json.as_object().expect("json obj");
let els = obj.get("elements").and_then(|v| v.as_array()).expect("els");
println!("{} elements", els.len());
let mut nodes = HashMap::new();
for el in els {
let el = el.as_object().expect("el");
match el.get("type").and_then(|v| v.as_str()) {
Some("node") => {
let id = el.get("id").and_then(|v| v.as_u64()).expect("id");
let lon = el.get("lon").expect("lon")
.as_f64().expect("lon f64");
let lat = el.get("lat").expect("lat")
.as_f64().expect("lat f64");
let coord = geo::Coordinate { x: lon, y: lat };
nodes.insert(id, coord);
}
_ => {}
}
}
println!("{} nodes", nodes.len());
let mut locations = vec![];
let mut ways = HashMap::new();
for el in els {
let el = el.as_object().expect("el");
match el.get("type").and_then(|v| v.as_str()) {
Some("way") => {
let id = el.get("id").and_then(|v| v.as_u64()).expect("id");
let way_nodes = el.get("nodes").and_then(|v| v.as_array()).expect("nodes")
.iter()
.map(|way_node| {
let way_node = way_node.as_u64()
.expect("way_node");
nodes.get(&way_node).expect("way_node node")
})
.cloned()
.collect::<Vec<_>>();
let poly = Arc::new(Polygon::new(
geo::LineString(way_nodes),
vec![]
));
ways.insert(id, poly.clone());
if let Some(tags) = el.get("tags").and_then(|v| v.as_object()) {
if let Some(name) = tags.get("name").and_then(|v| v.as_str()) {
locations.push(Location::new(name, vec![poly]));
}
}
}
_ => {}
}
}
println!("{} ways", ways.len());
for el in els {
let el = el.as_object().expect("el");
match el.get("type").and_then(|v| v.as_str()) {
Some("relation") => {
let polys = el.get("members").and_then(|v| v.as_array()).expect("members")
.iter()
.filter_map(|member| {
let member = member.as_object().unwrap();
let member_type = member.get("type").and_then(|v| v.as_str()).unwrap();
if member_type == "way" {
let member_ref = member.get("ref").and_then(|v| v.as_u64()).unwrap();
let way = ways.get(&member_ref).expect("member way");
Some(way.clone())
} else {
None
}
})
.collect::<Vec<_>>();
if let Some(tags) = el.get("tags").and_then(|v| v.as_object()) {
if let Some(name) = tags.get("name").and_then(|v| v.as_str()) {
locations.push(Location::new(name, polys));
}
}
}
_ => {}
}
}
locations.sort_by(|a, b| a.area.partial_cmp(&b.area).unwrap());
println!("{} locations", locations.len());
Locations {
locations,
}
}
pub fn find(&self, coord: &geo::Coordinate<f64>) -> Option<Arc<String>> {
for l in &self.locations {
if l.contains(coord) {
return Some(l.name.clone());
}
}
None
}
}

14
src/main.rs Normal file
View File

@ -0,0 +1,14 @@
mod adsb;
mod location;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let locations = location::Locations::load("locations.json");
let mut events = adsb::run("https://adsb.hq.c3d2.de/data.json", locations).await;
while let Some(event) = events.recv().await {
println!("event: {:?}", event);
}
Ok(())
}