Update README to reflect current project state

This commit is contained in:
Schuwi
2026-03-07 23:38:36 +01:00
parent 28b7e90af8
commit 56f779990d
2 changed files with 100 additions and 56 deletions

156
README.md
View File

@@ -1,97 +1,141 @@
# cloud_cover # cloud_cover
Downloads total cloud cover (CLCT) predictions from the [DWD open-data Cloud cover forecast maps for astrophotography planning, powered by 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.
This is a Rust port of `../cloud_cover_prediction.py`. The output format ![Example output — prediction for the Berlin/Brandenburg area](docs/example.png)
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 ## What is this?
bitmap, which avoids every system-library dependency (no CDO, no PyNIO, no
PyNGL, no libssl, no libbz2 — the binary is fully self-contained). 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 ## Usage
``` ```
cloud_cover <ISO_TIMESTAMP> <HOURS> cloud_cover [OPTIONS] <ISO_TIMESTAMP> <HOURS>
``` ```
| Argument | Description | | 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 (048) | | `HOURS` | Number of forecast steps to render (048) |
The timestamp must correspond to an ICON-D2 model run, which is issued The timestamp must match an ICON-D2 model run: every 3 hours starting at 00 UTC
every 3 hours starting at 00 UTC (00, 03, 06, 09, 12, 15, 18, 21). (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 ```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/<date>_<hour>UTC/clct_000001.png` ## Output
`clct_NNNNNN.png`. Downloaded GRIB2 files are cached in the same directory
so that re-running the same timestamp skips the network requests. Each run produces files in `cache/<date>_<hour>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 ## How it works
1. **Download**`reqwest` fetches the bzip2-compressed GRIB2 files from 1. **Download**Fetches bzip2-compressed GRIB2 files from the DWD open-data
the DWD server. Two coordinate files (`clat`, `clon`) give the latitude server: two coordinate grids (`clat`, `clon`) describing the ~542,000 points of
and longitude of every point on the ICON-D2 icosahedral grid (~542 000 the ICON-D2 icosahedral grid over Germany, plus one `clct` file per forecast step.
points for Germany). One `clct` file per forecast step contains the Downloads run in parallel and are cached to disk.
total cloud cover percentage at each point.
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. 3. **Interpolate** — The irregular grid points are projected into pixel space via an
Only simple packing (data representation template 0) is supported, which orthographic projection, then connected into a Delaunay triangulation. Each output
is what DWD uses. Grid points that are flagged as absent by the section-6 pixel is interpolated via barycentric coordinates within its enclosing triangle,
bitmap are set to `NaN` and skipped during rendering. followed by a NaN-aware Gaussian blur to smooth triangle edges.
4. **Project & render**Each grid point is projected onto the image plane 4. **Render**The base layer comes from Carto Voyager OSM tiles (fetched and
using an orthographic projection centred on Falkensee (52.56 °N, cached separately for the basemap and for labels). Cloud cover is blended on top,
13.08 °E). Points are painted as 2 × 2 pixel squares, colour-coded by then map labels are composited above the clouds. City markers, a title bar, and a
cloud cover percentage. City markers and labels are drawn on top, and a colour legend complete the frame.
colour-scale legend is shown on the right. `plotters` writes the final
PNG.
## Cloud cover colour scale 5. **Animate** — All frames are colour-quantised to 256-colour palettes (in parallel)
and assembled into a looping GIF.
| Cloud cover | Colour |
|---|---|
| < 1 % | Land background (clear) |
| 12 % | Very light blue |
| 25 % | Light blue |
| 510 % | Medium blue |
| 1020 % | Blue |
| 2050 % | Dark blue |
| 50100 % | Very dark blue |
## Marked locations
Falkensee · Pausin · Nauen · Hennigsdorf · Ketzin/Brückenkopf · Potsdam ·
Berlin (Mitte)
## Dependencies ## Dependencies
All dependencies are pure Rust or vendored C code compiled into the binary. All dependencies are pure Rust or vendored C compiled into the binary — no system
No system shared libraries are required at runtime beyond libc. shared libraries are required at runtime beyond libc.
| Crate | Role | | Crate | Role |
|---|---| |---|---|
| `reqwest` (rustls-tls) | HTTP client with pure-Rust TLS | | `reqwest` (rustls-tls) | HTTP client with pure-Rust TLS |
| `bzip2` | bzip2 decompression (vendors libbzip2) | | `bzip2` | Bzip2 decompression (vendors libbzip2) |
| `plotters` + `plotters-bitmap` | PNG rendering with built-in bitmap font | | `plotters` + `plotters-bitmap` | PNG rendering |
| `font8x8` | Built-in 8×8 bitmap font for map labels |
| `clap` | CLI argument parsing | | `clap` | CLI argument parsing |
| `chrono` | Date/time handling | | `chrono` + `chrono-tz` | Date/time handling and timezone conversion |
| `anyhow` | Error propagation | | `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 ## Building
```sh ```sh
cd cloud_cover_rs
cargo build --release 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

BIN
docs/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB