diff --git a/.gitignore b/.gitignore index a443bd4..4df8d49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target -input -rv.png +*.png +config.toml +data/ diff --git a/Cargo.lock b/Cargo.lock index 023a266..62e817c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -32,6 +41,12 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -71,6 +86,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.12" @@ -112,6 +133,119 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "geo" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1" +dependencies = [ + "float_next_after", + "geo-types", + "geographiclib-rs", + "i_overlay", + "log", + "num-traits", + "proj", + "robust", + "rstar", + "serde", +] + +[[package]] +name = "geo-types" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" +dependencies = [ + "approx", + "num-traits", + "rstar", + "serde", +] + +[[package]] +name = "geographiclib-rs" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f611040a2bb37eaa29a78a128d1e92a378a03e0b6e66ae27398d42b1ba9a7841" +dependencies = [ + "libm", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "i_float" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343" +dependencies = [ + "serde", +] + +[[package]] +name = "i_key_sort" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" + +[[package]] +name = "i_overlay" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", +] + +[[package]] +name = "i_shape" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce" +dependencies = [ + "i_float", + "serde", +] + +[[package]] +name = "i_tree" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" + [[package]] name = "image" version = "0.25.6" @@ -124,12 +258,28 @@ dependencies = [ "png", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -143,9 +293,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.32.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ "pkg-config", "vcpkg", @@ -166,6 +316,18 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -183,6 +345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -215,10 +378,11 @@ dependencies = [ [[package]] name = "proj" -version = "0.30.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e0c01de214d7ea50ee6519969be9efdc6f0dc5ce6c64fbd4f054ea26d43ca4" +checksum = "3939f58cd2f8e5f3bba7fb76b3854956d39b1b76cec4fe5f65481d18f9c92d22" dependencies = [ + "geo-types", "libc", "num-traits", "proj-sys", @@ -227,9 +391,9 @@ dependencies = [ [[package]] name = "proj-sys" -version = "0.26.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a208129995443a4c475464c33308274be1578993b0ad266c9139188ed0dc7e91" +checksum = "533a4ed2ab59f7605ecea26db7ed76572d30aed9d2a6a90738bc7f7e7b5a11d8" dependencies = [ "cmake", "flate2", @@ -257,6 +421,23 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "robust" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" + +[[package]] +name = "rstar" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421400d13ccfd26dfa5858199c30a5d76f9c54e0dba7575273025b43c5175dbb" +dependencies = [ + "heapless", + "num-traits", + "smallvec", +] + [[package]] name = "rustix" version = "1.0.7" @@ -270,6 +451,35 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.3.0" @@ -282,6 +492,18 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "syn" version = "2.0.101" @@ -324,6 +546,47 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -340,8 +603,11 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" name = "wetter" version = "0.1.0" dependencies = [ + "geo", "image", "proj", + "serde", + "toml", ] [[package]] @@ -417,6 +683,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + [[package]] name = "xattr" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index e1b9644..3a90122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +geo = { version = "0.30.0", default-features = false, features = ["use-proj", "use-serde"] } image = { version = "0.25.6", default-features = false, features = ["png"] } -proj = { version = "0.30.0", default-features = false } +proj = "0.29.0" +serde = { version = "1.0.219", features = ["derive"] } +toml = "0.8.22" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4851aa6 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,22 @@ +use geo::Point; + +use serde::Deserialize; + +use std::collections::HashMap; + +use toml::de::Error; + +#[derive(Deserialize)] +pub struct Config { + pois: HashMap, +} + +impl Config { + pub fn from_toml(s: &str) -> Result { + toml::from_str(s) + } + + pub fn pois(&self) -> &HashMap { + &self.pois + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs index 95eee43..ca37c23 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -2,3 +2,4 @@ mod parser; mod rv; pub use rv::Rv; +pub use rv::DATA_LEN as RV_DATA_LEN; diff --git a/src/data/parser.rs b/src/data/parser.rs index d91ba9f..f1c7a8a 100644 --- a/src/data/parser.rs +++ b/src/data/parser.rs @@ -1,9 +1,9 @@ +use std::fmt::{self, Display, Formatter}; use std::io::{Error as IoError, Read, Seek}; use std::num::ParseIntError; use std::slice; use std::str::{self, FromStr, Utf8Error}; -#[allow(dead_code)] #[derive(Debug)] pub enum ParseError { IoError(IoError), @@ -12,6 +12,19 @@ pub enum ParseError { Utf8Error(Utf8Error), } +impl Display for ParseError { + fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + match self { + ParseError::IoError(err) => err.fmt(fmt), + ParseError::ParseIntError(err) => err.fmt(fmt), + ParseError::Unexpected(actual, expected) => { + fmt.write_fmt(format_args!("Expected '{}', got '{}'", expected, actual)) + } + ParseError::Utf8Error(err) => err.fmt(fmt), + } + } +} + pub struct Parser { r: R, } diff --git a/src/data/rv.rs b/src/data/rv.rs index 992c05d..27b7fd2 100644 --- a/src/data/rv.rs +++ b/src/data/rv.rs @@ -3,9 +3,11 @@ compile_error!("Only little-endian architectures are supported."); use crate::data::parser::{ParseError, Parser}; +use std::cmp::Ordering; +use std::fmt::{self, Display, Formatter}; use std::io::{Read, Seek}; -const DATA_LEN: usize = 1100 * 1200; +pub const DATA_LEN: usize = 1100 * 1200; #[derive(Clone, Copy, Debug)] pub struct VersionMismatch { @@ -13,7 +15,16 @@ pub struct VersionMismatch { pub expected: u8, } -#[derive(Clone, Copy, Debug)] +impl Display for VersionMismatch { + fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.write_fmt(format_args!( + "Expected data version {}, got {}", + self.expected, self.actual + )) + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct Datetime { pub year: u16, pub month: u8, @@ -27,6 +38,7 @@ pub struct Rv { timestamp: Datetime, version: u8, precision: i8, + interval: u16, forecast: u16, data: Vec, } @@ -68,7 +80,7 @@ impl Rv { parser.expect("INT")?; - let _interval = parser.parse_int::()?; + let interval = parser.parse_int::<_, 4>()?; parser.expect("GP")?; @@ -102,6 +114,7 @@ impl Rv { }, version, precision, + interval, forecast, data, }) @@ -122,6 +135,10 @@ impl Rv { 10_f64.powi(self.precision as i32) } + pub fn interval(&self) -> u16 { + self.interval + } + pub fn instant(&self) -> (Datetime, u16) { (self.timestamp, self.forecast) } @@ -130,3 +147,25 @@ impl Rv { &self.data[..] } } + +impl Eq for Rv {} + +impl Ord for Rv { + fn cmp(&self, other: &Self) -> Ordering { + self.timestamp + .cmp(&other.timestamp) + .then(self.forecast.cmp(&other.forecast)) + } +} + +impl PartialEq for Rv { + fn eq(&self, other: &Self) -> bool { + self.timestamp == other.timestamp && self.forecast == other.forecast + } +} + +impl PartialOrd for Rv { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/main.rs b/src/main.rs index 588a336..fa235ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,21 @@ +mod config; mod data; -use data::Rv; +use config::Config; + +use data::{Rv, RV_DATA_LEN}; use image::imageops; use image::RgbaImage; use proj::Proj; -use std::fs::File; +use std::cmp::Reverse; +use std::collections::BinaryHeap; +use std::env; +use std::fs::{self, File}; use std::io::BufReader; +use std::str; const PROJ_WGS84_DE1200: &'static str = "+proj=stere +lat_0=90 +lat_ts=60 +lon_0=10 +a=6378137 +b=6356752.3142451802 +no_defs +x_0=543196.83521776402 +y_0=3622588.8619310018"; @@ -21,17 +28,13 @@ enum Intensity { Unknown = 0xd0d0d080, } -fn main() { - let rv = Rv::parse(BufReader::new(File::open("input").unwrap())).unwrap(); - rv.expect_version(5).unwrap(); - - let mut data = Vec::with_capacity(1100 * 1200 * 4); - - for v in rv.data() { - let intensity = if *v == 0x29c4 { +impl Intensity { + fn from_rv_value(v: u16, scale: f64, interval: u16) -> Self { + if v == 0x29c4 { Intensity::Unknown } else { - let mm_per_h = *v as f64 * rv.scale() * 12.0; // 12 * 5 min intervals per hour + let mm_per_h = v as f64 * scale * (60.0 / interval as f64); + if mm_per_h < 0.1 { Intensity::None } else if mm_per_h < 5.0 { @@ -41,23 +44,85 @@ fn main() { } else { Intensity::Heavy } - }; + } + } +} - data.extend_from_slice(&(intensity as u32).to_be_bytes()); +fn main() -> Result<(), String> { + let config = + Config::from_toml(&fs::read_to_string("config.toml").map_err(|err| err.to_string())?) + .map_err(|err| err.to_string())?; + + let mut heap = BinaryHeap::new(); + + for path in env::args().skip(1) { + let rv = Rv::parse(BufReader::new( + File::open(&path).map_err(|err| err.to_string())?, + )) + .map_err(|err| err.to_string())?; + rv.expect_version(5).map_err(|err| err.to_string())?; + + heap.push(Reverse(rv)); } - let mut img = RgbaImage::from_raw(1100, 1200, data).unwrap(); - imageops::flip_vertical_in_place(&mut img); - img.save("rv.png").unwrap(); + let Reverse(current) = heap.pop().ok_or("At least one file must be given")?; - let wgs84_to_de1200 = Proj::new(PROJ_WGS84_DE1200).unwrap(); - println!( - "{:#?}", - wgs84_to_de1200 - .project( - (1.463301510_f64.to_radians(), 55.86208711_f64.to_radians()), - false - ) - .unwrap() - ); + let (timestamp, offset) = current.instant(); + + if offset != 0 { + eprintln!( + "First file is expected to contain current situation, got forecast for +{} minutes", + offset + ); + } + + let mut img_data = Vec::with_capacity(RV_DATA_LEN * 4); + + for v in current.data() { + let intensity = Intensity::from_rv_value(*v, current.scale(), current.interval()); + + img_data.extend_from_slice(&(intensity as u32).to_be_bytes()); + } + + let mut img = RgbaImage::from_raw(1100, 1200, img_data).unwrap(); + imageops::flip_vertical_in_place(&mut img); + img.save(format!( + "rv_{}-{}-{}_{}:{}.png", + timestamp.year, timestamp.month, timestamp.day, timestamp.hour, timestamp.minute + )) + .map_err(|err| err.to_string())?; + + let mut data = Vec::with_capacity(RV_DATA_LEN * heap.len()); + + while heap.len() > 0 { + let Reverse(forecast) = heap.pop().unwrap(); + data.extend_from_slice(forecast.data()); + } + + let wgs84_to_de1200 = Proj::new(PROJ_WGS84_DE1200).map_err(|err| err.to_string())?; + + for (name, poi) in config.pois() { + let (x, y) = (wgs84_to_de1200 + .project(poi.to_radians(), false) + .map_err(|err| err.to_string())? + / 1000.0) + .x_y(); + + let (x, y) = (x.round() as usize, (1200 + y.round() as i64) as usize); + if !(0..1100).contains(&x) || !(0..1200).contains(&y) { + eprintln!("POI \"{}\" is out of range", name); + continue; + } + + let v = current.data()[y * 1100 + x]; + if v == 0x29c4 { + eprintln!("No value for POI \"{}\"", name); + continue; + } + + let mm_per_h = v as f64 * current.scale() * (60.0 / current.interval() as f64); + println!("intensity{{poi=\"{}\"}} {}", name, mm_per_h); + } + + Ok(()) } diff --git a/update.py b/update.py new file mode 100755 index 0000000..1c86cbe --- /dev/null +++ b/update.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import subprocess +import tarfile +from datetime import UTC, datetime, timedelta +from io import BytesIO +from pathlib import Path +from sys import exit, stderr, stdout + +import requests + +DATA_DIR = Path.cwd() / "data" +DATA_DIR.mkdir(exist_ok=True) + +DOWNLOAD_URL = ( + "https://opendata.dwd.de/weather/radar/composite/rv/DE1200_RV%y%m%d%H%M.tar.bz2" +) + +now = datetime.now(UTC) +now = now.replace(minute=now.minute // 5 * 5, second=0, microsecond=0) + +for dt in (now, now - timedelta(minutes=5)): + url = dt.strftime(DOWNLOAD_URL) + print(f"Attempting to download {url}", file=stderr) + + try: + r = requests.get(url) + r.raise_for_status() + break + except requests.HTTPError: + print(f"Not Found: {url}") +else: + exit() + +with tarfile.open(fileobj=BytesIO(r.content)) as tar: + tar.extractall(DATA_DIR, filter="data") + files = list(map(lambda n: DATA_DIR / n, tar.getnames())) + +subprocess.run( + [str(Path(__name__).resolve().parent / "target/release/wetter")] + files, + stdout=stdout, + stderr=stderr, + check=True, +)