1pub mod error;
2
3use crate::error::{Error, Result};
4use dwd_api_sys::models::{StationOverview, StationOverviewExtendedGetStationIdsParameterInner};
5use regex::Regex;
6use std::{
7 collections::HashMap,
8 str::{self, FromStr},
9 sync::LazyLock,
10};
11
12const STATIONS_LINK: &str = "https://www.dwd.de/DE/leistungen/klimadatendeutschland/statliste/statlex_rich.txt?view=nasPublication";
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct Station {
16 pub stat_name: String,
17 pub stat_id: u32,
18 pub ke: String,
19 pub stat: String,
20 pub latitude: f64,
21 pub longitude: f64,
22 pub hs: u32,
23 pub hfg_nfg: Option<u32>,
24 pub bl: String,
25 }
28
29impl AsRef<Station> for Station {
30 fn as_ref(&self) -> &Station {
31 self
32 }
33}
34
35impl FromStr for Station {
36 type Err = Error;
37
38 fn from_str(s: &str) -> Result<Self> {
39 parse_station(s).ok_or_else(|| Error::InvalidStationFormat(s.to_string()))
40 }
41}
42
43fn parse_station(line: &str) -> Option<Station> {
44 static RE: LazyLock<Regex> = LazyLock::new(|| {
45 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")
46 });
47 RE.is_match(line).then(|| {
48 let caps = RE.captures(line).unwrap();
49 Station {
50 stat_name: caps["STAT_NAME"].to_string(),
51 stat_id: caps["STAT_ID"].parse().expect("invalid station id"),
52 ke: caps["KE"].to_string(),
53 stat: caps["STAT"].to_string(),
54 latitude: caps["BR_HIGH"].parse().expect("invalid br high"),
55 longitude: caps["LA_HIGH"].parse().expect("invalid la high"),
56 hs: caps["HS"].parse().expect("invalid hs"),
57 hfg_nfg: caps
58 .name("HFG_NFG")
59 .map(|m| m.as_str().parse().expect("invalid hfg/nfg")),
60 bl: caps["BL"].to_string(),
61 }
64 })
65}
66
67pub async fn fetch_stations() -> Result<Vec<Station>> {
68 Ok(reqwest::get(STATIONS_LINK)
69 .await?
70 .text()
71 .await?
72 .lines()
73 .map(|line| line.parse())
74 .filter_map(Result::ok)
75 .collect())
76}
77
78pub fn closest_station(
79 stations: &[Station],
80 latitude: f64,
81 longitude: f64,
82) -> Option<(&Station, f64)> {
83 stations
84 .iter()
85 .map(|s| (s, haversine_distance(latitude, longitude, s)))
86 .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
87}
88
89pub async fn fetch_overview(
90 stations: &[impl AsRef<Station>],
91) -> Result<HashMap<String, StationOverview>> {
92 let mut config = dwd_api_sys::apis::configuration::Configuration::default();
93 config.user_agent =
99 Some("Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0".to_owned());
100
101 let station_ids = Some(
102 stations
103 .iter()
104 .map(|s| {
105 StationOverviewExtendedGetStationIdsParameterInner::String(
106 s.as_ref().stat.to_owned(),
107 )
108 })
109 .collect(),
110 );
111
112 let data = dwd_api_sys::apis::default_api::station_overview_extended_get(&config, station_ids)
113 .await
114 .map_err(|e| Error::StationOverviewExtendedGetError(e))?;
115 Ok(data)
116}
117
118pub fn haversine_distance(latitude: f64, longitude: f64, station: &Station) -> f64 {
119 let station_latitude = station.latitude;
120 let station_longitude = station.longitude;
121
122 let to_radians = |deg: f64| deg * std::f64::consts::PI / 180.0;
123 let lat1 = to_radians(latitude);
124 let lon1 = to_radians(longitude);
125 let lat2 = to_radians(station_latitude);
126 let lon2 = to_radians(station_longitude);
127
128 let dlat = lat2 - lat1;
129 let dlon = lon2 - lon1;
130
131 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
132 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
133
134 6_371.0 * c }