dwd-api 0.2.0

A high level Rust wrapper for the DWD (Deutscher Wetterdienst) weather data API
Documentation
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,
    // pub beginn: NaiveDate,
    // pub ende: NaiveDate,
}

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(),
            // beginn: caps["BEGINN"].parse().expect("invalid beginn"),
            // ende: caps["ENDE"].parse().expect("invalid ende"),
        }
    })
}

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.base_path = "https://dwd.api.proxy.bund.dev/v30".to_owned();
    // config.base_path = "https://dwd.api.bund.dev/v30".to_owned();
    // config.base_path = "https://app-prod-ws.warnwetter.de/v30".to_owned();
    // config.base_path = "https://opendata.dwd.de/".to_owned();

    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 // Earth's radius in kilometers
}