pub mod error;
use crate::error::{Error, Result};
use dwd_api_sys::models::{StationOverview, StationOverviewExtendedGetStationIdsParameterInner};
use regex::Regex;
use std::{
collections::HashMap,
str::{self, FromStr},
sync::LazyLock,
};
const STATIONS_LINK: &str = "https://www.dwd.de/DE/leistungen/klimadatendeutschland/statliste/statlex_rich.txt?view=nasPublication";
#[derive(Debug, Clone, PartialEq)]
pub struct Station {
pub stat_name: String,
pub stat_id: u32,
pub ke: String,
pub stat: String,
pub latitude: f64,
pub longitude: f64,
pub hs: u32,
pub hfg_nfg: Option<u32>,
pub bl: String,
}
impl AsRef<Station> for Station {
fn as_ref(&self) -> &Station {
self
}
}
impl FromStr for Station {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
parse_station(s).ok_or_else(|| Error::InvalidStationFormat(s.to_string()))
}
}
fn parse_station(line: &str) -> Option<Station> {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?<STAT_NAME>.+?)\s+(?<STAT_ID>[0-9]+)\s+(?<KE>[A-Z][A-Z])\s+(?<STAT>[0-9A-Z]+)\s+(?<BR_HIGH>[0-9|\.]+)\s+(?<LA_HIGH>[0-9\.]+)\s+(?<HS>[0-9]+)\s+(?:(?<HFG_NFG>[0-9]+)\s+)?(?<BL>[A-Z][A-Z])\s+(?<BEGINN>[0-9\.]+)\s+(?<ENDE>[0-9\.]+)\s*$").expect("invalid station regex")
});
RE.is_match(line).then(|| {
let caps = RE.captures(line).unwrap();
Station {
stat_name: caps["STAT_NAME"].to_string(),
stat_id: caps["STAT_ID"].parse().expect("invalid station id"),
ke: caps["KE"].to_string(),
stat: caps["STAT"].to_string(),
latitude: caps["BR_HIGH"].parse().expect("invalid br high"),
longitude: caps["LA_HIGH"].parse().expect("invalid la high"),
hs: caps["HS"].parse().expect("invalid hs"),
hfg_nfg: caps
.name("HFG_NFG")
.map(|m| m.as_str().parse().expect("invalid hfg/nfg")),
bl: caps["BL"].to_string(),
}
})
}
pub async fn fetch_stations() -> Result<Vec<Station>> {
Ok(reqwest::get(STATIONS_LINK)
.await?
.text()
.await?
.lines()
.map(|line| line.parse())
.filter_map(Result::ok)
.collect())
}
pub fn closest_station(
stations: &[Station],
latitude: f64,
longitude: f64,
) -> Option<(&Station, f64)> {
stations
.iter()
.map(|s| (s, haversine_distance(latitude, longitude, s)))
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
}
pub async fn fetch_overview(
stations: &[impl AsRef<Station>],
) -> Result<HashMap<String, StationOverview>> {
let mut config = dwd_api_sys::apis::configuration::Configuration::default();
config.user_agent =
Some("Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0".to_owned());
let station_ids = Some(
stations
.iter()
.map(|s| {
StationOverviewExtendedGetStationIdsParameterInner::String(
s.as_ref().stat.to_owned(),
)
})
.collect(),
);
let data = dwd_api_sys::apis::default_api::station_overview_extended_get(&config, station_ids)
.await
.map_err(|e| Error::StationOverviewExtendedGetError(e))?;
Ok(data)
}
pub fn haversine_distance(latitude: f64, longitude: f64, station: &Station) -> f64 {
let station_latitude = station.latitude;
let station_longitude = station.longitude;
let to_radians = |deg: f64| deg * std::f64::consts::PI / 180.0;
let lat1 = to_radians(latitude);
let lon1 = to_radians(longitude);
let lat2 = to_radians(station_latitude);
let lon2 = to_radians(station_longitude);
let dlat = lat2 - lat1;
let dlon = lon2 - lon1;
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
6_371.0 * c }