diff --git a/README.md b/README.md index 218b00f..767af9f 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,141 @@ # cloud_cover -Downloads total cloud cover (CLCT) predictions from the [DWD open-data -server](https://opendata.dwd.de/weather/nwp/icon-d2/grib/) and renders -one PNG map per forecast hour, centred on the Brandenburg / Berlin area. +Cloud cover forecast maps for astrophotography planning, powered by DWD open data. -This is a Rust port of `../cloud_cover_prediction.py`. The output format -differs: instead of using PyNGL's contour renderer the Rust version does a -direct point-by-point orthographic projection of the ICON-D2 grid onto a -bitmap, which avoids every system-library dependency (no CDO, no PyNIO, no -PyNGL, no libssl, no libbz2 — the binary is fully self-contained). +![Example output — prediction for the Berlin/Brandenburg area](docs/example.png) + +## What is this? + +The German Weather Service (DWD) publishes free, high-resolution numerical weather +predictions through its [open-data server](https://opendata.dwd.de/). The +**ICON-D2** model covers Germany and surrounding areas with ~2 km grid spacing and +produces forecasts up to 48 hours ahead, updated every 3 hours. + +This tool downloads the **total cloud cover (CLCT)** field from ICON-D2 and renders +it as a series of map images — one per forecast hour — overlaid on an OpenStreetMap +base layer. The result is a quick visual answer to "will the sky be clear tonight?" + +## Quick start + +```sh +cargo build --release +./target/release/cloud_cover "2026-03-07T09:00:00Z" 24 +``` + +Output appears in `cache/2026-03-07_09UTC/` — one PNG per forecast hour plus an +animated GIF. ## Usage ``` -cloud_cover +cloud_cover [OPTIONS] ``` | Argument | Description | |---|---| -| `ISO_TIMESTAMP` | Model run time in ISO 8601, e.g. `2026-03-03T18:00:00Z` | +| `ISO_TIMESTAMP` | Model run time in ISO 8601, e.g. `2026-03-07T09:00:00Z` | | `HOURS` | Number of forecast steps to render (0–48) | -The timestamp must correspond to an ICON-D2 model run, which is issued -every 3 hours starting at 00 UTC (00, 03, 06, 09, 12, 15, 18, 21). +The timestamp must match an ICON-D2 model run: every 3 hours starting at 00 UTC +(00, 03, 06, 09, 12, 15, 18, 21). -### Example +### Options + +| Flag | Default | Description | +|---|---|---| +| `--center-lat` | 52.56 | Map centre latitude (°N) | +| `--center-lon` | 13.08 | Map centre longitude (°E) | +| `--zoom` | 10 | OSM tile zoom level (higher = more detail, smaller area) | +| `--no-basemap` | — | Skip OSM tiles; use a plain green background | + +### Examples ```sh -cargo run --release -- "2026-03-03T18:00:00Z" 12 +# Next 12 hours from the 18 UTC run, default viewport (Falkensee / Berlin) +./target/release/cloud_cover "2026-03-06T18:00:00Z" 12 + +# Wider area centred on central Germany at zoom 8 +./target/release/cloud_cover "2026-03-07T06:00:00Z" 24 \ + --center-lat 51.0 --center-lon 10.5 --zoom 8 + +# Quick run without map tiles +./target/release/cloud_cover "2026-03-07T09:00:00Z" 6 --no-basemap ``` -Output PNGs are written to `cache/_UTC/clct_000001.png` … -`clct_NNNNNN.png`. Downloaded GRIB2 files are cached in the same directory -so that re-running the same timestamp skips the network requests. +## Output + +Each run produces files in `cache/_UTC/`: + +- **`clct_000001.png` … `clct_NNNNNN.png`** — one 900 × 600 px map per forecast + hour, showing cloud cover blended over the OSM base layer with a colour legend +- **`clct_animation.gif`** — animated loop of all frames (generated when there are + multiple steps) +- **`clat.grib2`, `clon.grib2`, `clct_NNN.grib2`** — cached raw forecast data; + re-running the same timestamp skips the download + +Image titles show the forecast time converted to CET/CEST (Europe/Berlin). The first +frame is labelled "Conditions at …", subsequent frames "Prediction at … (+NNh)". + +Cloud cover is rendered as a continuous blend from the base map (clear sky) toward a +light blue-white tone (overcast), matching the style familiar from weather apps. ## How it works -1. **Download** — `reqwest` fetches the bzip2-compressed GRIB2 files from - the DWD server. Two coordinate files (`clat`, `clon`) give the latitude - and longitude of every point on the ICON-D2 icosahedral grid (~542 000 - points for Germany). One `clct` file per forecast step contains the - total cloud cover percentage at each point. +1. **Download** — Fetches bzip2-compressed GRIB2 files from the DWD open-data + server: two coordinate grids (`clat`, `clon`) describing the ~542,000 points of + the ICON-D2 icosahedral grid over Germany, plus one `clct` file per forecast step. + Downloads run in parallel and are cached to disk. -2. **Decompress** — `bzip2` decompresses the files in-memory. +2. **Parse** — A built-in minimal GRIB2 decoder extracts the data arrays. Only + simple packing (data representation template 0) is implemented, which is the + format DWD uses for these fields. Grid points flagged absent by the GRIB2 bitmap + are set to NaN. -3. **Parse** — A minimal built-in GRIB2 decoder extracts the data values. - Only simple packing (data representation template 0) is supported, which - is what DWD uses. Grid points that are flagged as absent by the section-6 - bitmap are set to `NaN` and skipped during rendering. +3. **Interpolate** — The irregular grid points are projected into pixel space via an + orthographic projection, then connected into a Delaunay triangulation. Each output + pixel is interpolated via barycentric coordinates within its enclosing triangle, + followed by a NaN-aware Gaussian blur to smooth triangle edges. -4. **Project & render** — Each grid point is projected onto the image plane - using an orthographic projection centred on Falkensee (52.56 °N, - 13.08 °E). Points are painted as 2 × 2 pixel squares, colour-coded by - cloud cover percentage. City markers and labels are drawn on top, and a - colour-scale legend is shown on the right. `plotters` writes the final - PNG. +4. **Render** — The base layer comes from Carto Voyager OSM tiles (fetched and + cached separately for the basemap and for labels). Cloud cover is blended on top, + then map labels are composited above the clouds. City markers, a title bar, and a + colour legend complete the frame. -## Cloud cover colour scale - -| Cloud cover | Colour | -|---|---| -| < 1 % | Land background (clear) | -| 1–2 % | Very light blue | -| 2–5 % | Light blue | -| 5–10 % | Medium blue | -| 10–20 % | Blue | -| 20–50 % | Dark blue | -| 50–100 % | Very dark blue | - -## Marked locations - -Falkensee · Pausin · Nauen · Hennigsdorf · Ketzin/Brückenkopf · Potsdam · -Berlin (Mitte) +5. **Animate** — All frames are colour-quantised to 256-colour palettes (in parallel) + and assembled into a looping GIF. ## Dependencies -All dependencies are pure Rust or vendored C code compiled into the binary. -No system shared libraries are required at runtime beyond libc. +All dependencies are pure Rust or vendored C compiled into the binary — no system +shared libraries are required at runtime beyond libc. | Crate | Role | |---|---| | `reqwest` (rustls-tls) | HTTP client with pure-Rust TLS | -| `bzip2` | bzip2 decompression (vendors libbzip2) | -| `plotters` + `plotters-bitmap` | PNG rendering with built-in bitmap font | +| `bzip2` | Bzip2 decompression (vendors libbzip2) | +| `plotters` + `plotters-bitmap` | PNG rendering | +| `font8x8` | Built-in 8×8 bitmap font for map labels | | `clap` | CLI argument parsing | -| `chrono` | Date/time handling | +| `chrono` + `chrono-tz` | Date/time handling and timezone conversion | | `anyhow` | Error propagation | +| `spade` | Delaunay triangulation for grid interpolation | +| `rayon` | Parallel downloads, rendering, and GIF quantisation | +| `image` | PNG decoding (loading frames for GIF assembly) | +| `gif` | GIF encoding | ## Building ```sh -cd cloud_cover_rs cargo build --release ``` -The compiled binary ends up at `target/release/cloud_cover`. +The binary is at `target/release/cloud_cover`. + +## Future ideas + +- Configurable display timezone (currently hardcoded to Europe/Berlin) +- Configurable city markers / observation sites +- Configurable data sources +- Vector map tile support for increased rendering resolution +- Separation of cache and output files & automatic cache management +- Automatic newest prediction fetching diff --git a/docs/example.png b/docs/example.png new file mode 100644 index 0000000..2303284 Binary files /dev/null and b/docs/example.png differ