summaryrefslogtreecommitdiff
path: root/doc/bom-radar-spec.md
blob: 3123a1623f0dcf19e7606f74e076952e92526e0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
---title: "BOM Radar Display — Sydney (IDR713)"date-created: "2026-06-09"version: "0.1"type: "spec"author:  - "st33v"  - "claude-opus-4-8"model:  - "claude-opus-4-8"tldr: "Extend the existing bomsynoptic app to fetch, composite and serve Sydney radar (IDR713) from BOM anonymous FTP. Layer model: static transparencies (cached) plus a rolling buffer of the last six 6-minute rain-echo frames, served as a loopable sequence alongside the synoptic chart."status: "draft"spec-kind: "feature"target-repo: "bomsynoptic"target-machine: "cremonde"dependencies: []design-chat: "https://claude.ai/chat/..."phase: "1"phases-planned: 1implementing-agent: "claude-code"reader-targets:  - "claude-code"  - "st33v"tags:  - "bom"  - "radar"  - "ftp"---# BOM Radar Display — Sydney (IDR713)## 1. PurposeThe `bomsynoptic` app currently fetches and serves the BOM MSLP synopticchart on a six-hourly cadence. This spec adds a second product class:**rain radar**, beginning with the **128 km Sydney (Terrey Hills)** radar,product code **`IDR713`**.Unlike the synoptic chart — a single self-contained image — a radar view is a*composite* of several PNG layers that must be stacked in the correct order,and the rain-echo layer refreshes roughly every six minutes. The deliverableis the current radar image plus a short loop of the last half-dozen frames,served in the same manner as the existing synoptic chart.Brisbane (Mt Stapylton) and other radars are explicitly **out of scope** forthis phase; the design must make duplication to a new product code trivial(§9).## 2. How BOM radar works (background for the implementer)BOM does not publish a single ready-made radar image. It publishes a set of**transparent PNG layers**, all sharing identical pixel dimensions, intendedto be alpha-composited in a fixed order. Two distinct cadences apply:- **Static layers** (basemap, topography, range rings, place labels, feature  overlays, legend) change only rarely — when BOM revises the basemap. Fetch  once, cache, refresh occasionally (e.g. daily).- **Dynamic layers** (the rain-echo frames) are emitted **every ~6 minutes**.  Each frame is a separate timestamped file. Several recent frames are present  on the server at any moment; we take the most recent *N*.All files live under BOM's **anonymous FTP** service. Availability isbest-effort — BOM explicitly does not guarantee it — so resilience matters(§8).## 3. FTP sourceAnonymous FTP, no credentials beyond user `anonymous` (empty / any password).| Layer class | Directory ||---|---|| Rain-echo frames (dynamic) | `ftp://ftp.bom.gov.au/anon/gen/radar/` || Transparencies (static)    | `ftp://ftp.bom.gov.au/anon/gen/radar_transparencies/` |### 3.1 Dynamic framesFilename pattern in `/anon/gen/radar/`:```IDR713.T.<YYYYMMDDHHMM>.png```- The timestamp is **UTC**.- Because the timestamp is fixed-width and big-endian, a plain lexical sort of  the matching filenames is also a chronological sort.- Selection: list the directory, filter to the `IDR713.T.` prefix, sort, take  the last **6** (configurable as `FRAME_COUNT`, default 6).### 3.2 Static transparenciesFiles in `/anon/gen/radar_transparencies/`, all prefixed `IDR713.`:| File | Role ||---|---|| `IDR713.background.png`   | base map: land/sea, coastline || `IDR713.topography.png`   | terrain shading || `IDR713.range.png`        | range rings || `IDR713.locations.png`    | town / city labels || `IDR713.roads.png`        | roads (optional overlay) || `IDR713.rail.png`         | rail (optional overlay) || `IDR713.waterways.png`    | rivers (optional overlay) || `IDR713.catchments.png`   | catchment boundaries (optional) || `IDR713.wthrDistricts.png`| weather district boundaries (optional) || `IDR713.legend.0.png`     | colour-scale legend |Not every transparency exists for every radar. The implementer should fetchthe directory listing once and treat any missing optional layer as absentrather than failing.## 4. Compositing modelAlpha-composite **bottom → top** in this order. Optional feature overlays sitbetween topography and the radar echoes or above them per taste; theconstraint that matters is that **labels and range rings render on top of therain** so they stay legible.```1. IDR713.background.png        ← bottom (opaque base)2. IDR713.topography.png3. (optional) catchments / waterways / roads / rail / wthrDistricts4. IDR713.T.<timestamp>.png     ← the rain echoes (per frame)5. IDR713.range.png6. IDR713.locations.png         ← top (labels)```The **legend** (`IDR713.legend.0.png`) is a separate strip. Either compositeit into a fixed corner/margin of the output, or serve it as a sibling assetthe front-end positions beside the loop. Recommend compositing it in so theserved image is self-describing.Implementation note: build the **static base** (layers 1–3 + the on-topoverlays 5–6 are split around the echo, so cache them as two precompositedplates — a *lower plate* under the echo and an *upper plate* over it). Foreach frame: `lower_plate → echo → upper_plate`. This reduces per-frame work totwo composites instead of seven.Suggested tooling: Pillow (`PIL.Image.alpha_composite`) keeps the dependencyfootprint small and matches the likely existing stack; ImageMagick is anacceptable alternative if already present.## 5. Fetch cadence & rolling buffer- **Echo frames:** poll every **6 minutes** (`POLL_INTERVAL`, default 360 s).  On each poll, fetch only frames not already held locally (compare against  the buffer). Maintain a rolling buffer of the last `FRAME_COUNT` (6) frames;  evict older raw frames and their composites.- **Transparencies:** refresh on startup and then daily (`TRANSPARENCY_TTL`,  default 24 h). Survive a failed refresh by keeping the cached plates.- Do not hammer the server: the 6-minute poll matches BOM's emission rate.  Skip a poll cleanly if the previous one is still running.Note the contrast with the existing synoptic job: synoptic is six-**hourly**;radar is six-**minute**. The scheduler must accommodate both — reuse theexisting scheduling mechanism if it generalises, otherwise run radar on itsown timer.## 6. OutputsProduce two things per cycle:1. **Latest still** — the most recent frame composited over the plates.   Stable filename (e.g. `idr713-latest.png`) so the served URL never   changes.2. **Loop** — the last 6 composited frames. Serve either as:   - an **animated APNG/GIF** (`idr713-loop.png` / `.gif`) — simplest for the     front-end, single asset, frame timing baked in; or   - a **numbered sequence** (`idr713.0.png … idr713.5.png`) plus a small JSON     manifest of frame timestamps, leaving animation to client-side JS.Recommend the **sequence + manifest** route: it mirrors how BOM's own viewerworks, lets the front-end control frame rate and pausing, and avoidsre-encoding a whole animation every six minutes. The manifest also lets the UIprint the real observation time (converted from UTC to the user's / Sydneylocal time) under each frame.## 7. Storage layout (suggested)```<data-root>/radar/idr713/  transparencies/        # cached static PNGs, refreshed daily  frames/                # raw echo PNGs, rolling buffer of 6  out/    idr713-latest.png    idr713.0.png … idr713.5.png    manifest.json        # [{seq, utc, local, src_filename}, …]```Mirror whatever convention the synoptic side already uses for its serveddirectory; the above is a fallback if there's no established pattern.## 8. Resilience- **Missing / partial frames:** if a poll returns fewer than 6 frames, serve  what exists; never blank the display.- **FTP unreachable:** retain and keep serving the last good composites; log  and retry next cycle. BOM does not guarantee availability.- **Stale data:** stamp the manifest with the newest frame's UTC time so the  UI can grey out / warn if the latest echo is older than, say, 20 minutes.- **Corrupt download:** validate each PNG opens before it enters the buffer;  discard and retry otherwise.- **Dimension mismatch:** if a transparency and the echo frames disagree on  size (can happen across a BOM basemap revision), refetch transparencies  before compositing.## 9. Generalising to other radarsParameterise on the product code. A single config entry should be enough toadd Brisbane (Mt Stapylton) later:```yamlradars:  - id: "IDR713"   # Sydney (Terrey Hills) 128 km    enabled: true  - id: "IDR663"   # Brisbane (Mt Stapylton) 128 km — confirm code before enabling    enabled: false```Everything else — directories, filename patterns, layer set, compositingorder — is identical across radars, differing only by the `IDRnnn` stem.**Do not hardcode `IDR713`.** (The Brisbane code above is a placeholder;st33v will confirm the exact product before that radar is switched on.)## 10. Copyright / terms of useBOM anonymous-FTP products are licensed for personal or in-organisation use,**not** commercial use, and republishing is meant to go through RegisteredUser services. Since `st33v.com` is a public-facing surface, this is worth adeliberate decision rather than an oversight. Options: attribute BOM clearlyand treat the personal/non-commercial display as within bounds, gate the radarbehind the internal (WireGuard) surface only, or register. **Flag for st33v —do not silently publish.**## 11. Out of scope (this phase)- Brisbane and all radars other than IDR713 (config-ready, disabled).- Doppler wind / velocity products and the rainfall-accumulation products.- Historical / archived frames beyond the rolling 6-frame buffer.- Any front-end beyond serving the still, the sequence, and the manifest in  the same idiom as the existing synoptic chart.## 12. Open questions for Claude Code1. Does the existing scheduler generalise to a 6-minute timer, or does radar   need its own?2. What's the established served-assets directory and URL pattern on the   synoptic side — reuse it.3. Pillow already a dependency, or introduce it?4. Loop-as-sequence (recommended) vs animated single asset — confirm against   the existing front-end's capabilities.