This commit is contained in:
commit
7da29c1070
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(())
|
||||
}
|
Loading…
Reference in New Issue