From e14a7db4ffe3c4e0f15f6b37a980501a8d74d26b Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 13 Aug 2026 13:04:10 +0200 Subject: Initial commit: prognosis, the Go implementation An hour-by-hour forecast for the terminal, with official IMGW warnings for Polish locations. Replaces the Python version, whose cache file format it keeps so the two can coexist until this reaches parity. Open-Meteo provides the forecast, geocoding and pollen; GUGiK turns coordinates into a TERYT powiat code; IMGW supplies the warnings, filtered to that powiat rather than the whole country. Only the two lookups that never change are cached. Forecasts never are. Silence is never allowed to read as all-clear: "no warnings in force" and "the check failed" are reported as distinct states. Place names are resolved without guessing. A name matching several places is refused with a numbered list carrying each candidate's region and coordinates, and -pick N chooses one and remembers it. A stray positional beside -l is an error, so an unquoted "Wiry, PL" cannot silently resolve to somewhere else. The cache is written one entry per line with sorted keys, and treated as disposable but not worthless: an entry that will not parse is skipped and the rest kept, and a file that will not parse at all is moved to cache.json.bad rather than overwritten. No third-party dependencies. `make ci` is the gate: gofmt clean, vet, tests. --- internal/render/scale.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 internal/render/scale.go (limited to 'internal/render/scale.go') diff --git a/internal/render/scale.go b/internal/render/scale.go new file mode 100644 index 0000000..a2eb2dc --- /dev/null +++ b/internal/render/scale.go @@ -0,0 +1,92 @@ +package render + +import "math" + +// Temperature bands. The two ends are IMGW's own warning criteria, so a red +// temperature means the met office would issue a warning about it rather than +// that it looked hot to whoever wrote this: +// +// Tmin <= -15 silny mroz, stopien 1 -> bright blue +// Tmax >= 30 upal, stopien 1 -> red +// Tmax > 35 the higher heat level -> bright red +// +// The splits between (0, 10, 20) are round numbers, not thresholds from any +// source; they only subdivide the range nobody warns about. +var tempBands = []struct { + below float64 + code string +}{ + {0, Blue}, {10, Cyan}, {20, Green}, {30, Yellow}, +} + +// Deg rounds to whole degrees. Rounding through int conversion avoids "-0", +// which is arithmetically fine and visually wrong. +func Deg(t float64) int { + return int(math.Round(t)) +} + +// TempStyle is the ANSI code for a temperature in Celsius. +// +// The warning edges are written out rather than folded into tempBands so they +// match IMGW's criteria exactly, inclusive and exclusive included. +// +// The value is rounded first, so the colour always matches the number printed +// beside it: otherwise -14.6 prints as "-15" in a different colour than a true +// -15 and looks like a rendering bug. +func TempStyle(celsius float64) string { + t := float64(Deg(celsius)) + switch { + case t <= -15: + return BrightBlue + case t > 35: + return BrightRed + case t >= 30: + return Red + } + for _, b := range tempBands { + if t < b.below { + return b.code + } + } + return Yellow +} + +// Pollen bands in grains/m3, from Polish clinical sources. Each entry is an +// upper bound (exclusive) and a band key; a nil bound means "everything above". +// +// grass -- alergen.info.pl symptom table: 20 = first nasal symptoms in 25% +// of sufferers, 50 = symptoms in all tested, 65 = intensified in +// over 75%, 120 = dyspnoea after 30 minutes of exposure. +// birch -- mp.pl: 80 provokes symptoms in over 95% of allergics. +// mugwort -- mp.pl: over 70 counts as high, intensified symptoms. +// +// Birch and mugwort have a single published anchor each, so they get a two-way +// split rather than four bands: their "low" is weaker evidence than grass's. +// Alder, olive and ragweed have no Polish threshold that could be sourced and +// are deliberately left unbanded rather than banded on a guess. +var pollenBands = map[string][]struct { + below float64 + band string +}{ + "grass": {{20, "low"}, {50, "medium"}, {65, "high"}, {math.Inf(1), "very high"}}, + "birch": {{80, "low"}, {math.Inf(1), "high"}}, + "mugwort": {{70, "low"}, {math.Inf(1), "high"}}, +} + +// PollenBand is the qualitative level for a count, or "" where no threshold +// exists for that species. +func PollenBand(species string, value float64) string { + if value < 1 { + return "none" + } + bands, ok := pollenBands[species] + if !ok { + return "" + } + for _, b := range bands { + if value < b.below { + return b.band + } + } + return "" +} -- cgit v1.3