Skip to main content

dwd_api/
lib.rs

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    // pub beginn: NaiveDate,
26    // pub ende: NaiveDate,
27}
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            // beginn: caps["BEGINN"].parse().expect("invalid beginn"),
62            // ende: caps["ENDE"].parse().expect("invalid ende"),
63        }
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.base_path = "https://dwd.api.proxy.bund.dev/v30".to_owned();
94    // config.base_path = "https://dwd.api.bund.dev/v30".to_owned();
95    // config.base_path = "https://app-prod-ws.warnwetter.de/v30".to_owned();
96    // config.base_path = "https://opendata.dwd.de/".to_owned();
97
98    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 // Earth's radius in kilometers
135}