feat: sample precipitation at POIs, add download script

This commit is contained in:
Luca 2025-06-07 02:20:26 +02:00
parent 60ea19f2c4
commit e977380817
9 changed files with 502 additions and 39 deletions

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
/target
input
rv.png
*.png
config.toml
data/

287
Cargo.lock generated
View File

@ -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"

View File

@ -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"

22
src/config.rs Normal file
View File

@ -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<String, Point>,
}
impl Config {
pub fn from_toml(s: &str) -> Result<Self, Error> {
toml::from_str(s)
}
pub fn pois(&self) -> &HashMap<String, Point> {
&self.pois
}
}

View File

@ -2,3 +2,4 @@ mod parser;
mod rv;
pub use rv::Rv;
pub use rv::DATA_LEN as RV_DATA_LEN;

View File

@ -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: Read + Seek> {
r: R,
}

View File

@ -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<u16>,
}
@ -68,7 +80,7 @@ impl Rv {
parser.expect("INT")?;
let _interval = parser.parse_int::<u16, 4>()?;
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<Ordering> {
Some(self.cmp(other))
}
}

View File

@ -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(())
}

44
update.py Executable file
View File

@ -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,
)