Update README to reflect current project state
This commit is contained in:
156
README.md
156
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).
|
||||

|
||||
|
||||
## 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 <ISO_TIMESTAMP> <HOURS>
|
||||
cloud_cover [OPTIONS] <ISO_TIMESTAMP> <HOURS>
|
||||
```
|
||||
|
||||
| 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/<date>_<hour>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/<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
|
||||
|
||||
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
|
||||
|
||||
BIN
docs/example.png
Normal file
BIN
docs/example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Reference in New Issue
Block a user