radar

package
v0.41.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package radar answers "is it likely to rain or snow where this truck will be, when it gets there?" from NOAA RRFS forecast composite reflectivity. A build job (rivolt-infra apps/platform/maps/radar) turns each forecast hour into one small grid file and serves it inside the cluster; this package reads those files, projects a point onto the grid, and reports the strongest echo around it at the hours either side of an arrival time. Nothing here talks to NOAA, and nothing about a user leaves the server.

Package radar serves live weather radar tiles from NOAA/NWS (single-owner-era "live radar" feature).

The source is the National Weather Service's open GeoServer (opengeo.ncep.noaa.gov): the MRMS (Multi-Radar/Multi-Sensor) base reflectivity mosaic, quality controlled, 1 km, a frame about every two minutes and about two hours of history, for CONUS, Alaska, Hawaii, the Caribbean and Guam. NWS content is public domain, free for any lawful purpose provided we do not claim it as our own, imply an endorsement or affiliation, or modify it and present it as official (weather.gov/disclaimer; the service's GetCapabilities says Fees none, AccessConstraints none).

Users' browsers and phones never call NOAA: they ask this server for tiles, and NOAA sees only this server, with no user, no viewport and no account. A tile is cached by frame time (shared by every user, so the upstream load follows the number of distinct tiles in use and the frame rate, not the number of users) in process and, where the install has Redis, across replicas, with one fetch per tile per frame across pods.

Index

Constants

View Source
const (
	TileSize      = 256
	MaxZoom       = 10 // deepest zoom the route admits; the client asks up to ClientMaxZoom and the map overzooms past it
	ClientMinZoom = 3  // below this a continent-sized radar picture is useless and the tile count is large
	ClientMaxZoom = 9  // the mosaic is 1 km: finer zooms only resample it

)

Tile and cache parameters.

View Source
const (
	// MaxCycleAge is how old a cycle may be (by its nominal time) before its hours are treated as absent. Extended cycles come every 6 h and
	// appear about 3 h after their nominal time, so 9 h keeps one cycle usable through the whole gap until the next.
	MaxCycleAge = 9 * time.Hour
	// DefaultCacheBytes caps the decoded grids a pod holds (about 22 hours of CONUS).
	DefaultCacheBytes = 48 << 20
)
View Source
const DefaultBaseURL = "https://opengeo.ncep.noaa.gov/geoserver"

DefaultBaseURL is the NWS GeoServer.

View Source
const NoData = 255

NoData is the reserved byte of the dBZ plane: no value there. It is never read as "dry".

View Source
const RainDBZ = 20.0

RainDBZ is the reflectivity at which a cell counts as "rain likely". Below it, echoes are drizzle-or-less or clutter.

Variables

View Source
var ErrUnavailable = errors.New("radar: feed unavailable")

ErrUnavailable means the radar feed could not be read or is stale: the caller answers 503 and the client hides the layer for now.

Functions

func ParseTilePath

func ParseTilePath(p string) (z, x, y int, ok bool)

ParseTilePath parses "/z/x/y.png" strictly: no leading zeros, no extra segments or characters, x and y inside the zoom's grid, z up to MaxZoom.

func ResampleRows

func ResampleRows(src *image.NRGBA, z, y int) *image.NRGBA

ResampleRows turns an EPSG:4326 picture of a tile's lon/lat box into the Web Mercator tile: columns map one to one (longitude is linear in both); each output row takes the source row at its own latitude. src spans the tile's box top to bottom.

Types

type Config

type Config struct {
	BaseURL string // the GeoServer; empty is DefaultBaseURL
	HTTP    *http.Client
	Redis   redis.Cmdable // nil: process-local caching only
	Prefix  string        // Redis key prefix (the install's)
	Now     func() time.Time
	// MaxUpstream bounds the concurrent requests to NOAA from this pod (default 6).
	MaxUpstream int
	// UserAgent identifies this server to NOAA (default names Rivolt and a contact).
	UserAgent string
}

Config wires a Service.

type Echo

type Echo struct {
	DBZ  float64
	Snow bool
	OK   bool
}

Echo is what a point sees at one forecast hour: the strongest dBZ in the 3x3 cells around it, whether any of them says snow, and whether any cell had a value at all.

type Frames

type Frames struct {
	Latest time.Time   `json:"-"`
	Recent []time.Time `json:"-"`
}

Frames is what a client needs to show "radar as of": the newest frame and the recent ones (oldest first).

func ParseFrames

func ParseFrames(capabilities []byte, now time.Time) (Frames, error)

ParseFrames reads the time dimension of a WMS GetCapabilities document and returns the newest frame and a thinned recent list. It is an error when there is no frame or the newest is older than staleAfter at now.

type Grid

type Grid struct {
	Header Header
	// contains filtered or unexported fields
}

Grid is one forecast hour: the dBZ plane as the file has it, and the snow flags as a bit set (a missing snow flag reads as no snow claim).

func Decode

func Decode(r io.Reader) (*Grid, error)

Decode reads a gzip grid file.

func (*Grid) At

func (g *Grid) At(lat, lon float64) Echo

At is the echo around a point, or OK=false outside the grid or where every one of the 3x3 cells has no data.

func (*Grid) Bytes

func (g *Grid) Bytes() int

Bytes is the memory a decoded grid holds, for the cache's size cap.

type Header struct {
	V         int       `json:"v"`
	Domain    string    `json:"domain"`
	Cycle     string    `json:"cycle"`
	FH        int       `json:"fh"`
	CycleTime time.Time `json:"cycle_time"`
	ValidTime time.Time `json:"valid_time"`
	NX        int       `json:"nx"`
	NY        int       `json:"ny"`
	X0        float64   `json:"x0"` // projected metres of the grid's top-left corner
	Y0        float64   `json:"y0"`
	DX        float64   `json:"dx"` // metres per column
	DY        float64   `json:"dy"` // metres per row (negative: row 0 is the top)
	DBZOffset float64   `json:"dbz_offset"`
	DBZStep   float64   `json:"dbz_step"`
	NoData    int       `json:"nodata"`
	Planes    []string  `json:"planes"`
	Proj      LCC       `json:"proj"`
}

Header is the JSON header of a grid file (see radar_build.py).

type LCC

type LCC struct {
	Type    string  `json:"type"`
	Lat0    float64 `json:"lat0"`
	Lon0    float64 `json:"lon0"`
	Lat1    float64 `json:"lat1"`
	Lat2    float64 `json:"lat2"`
	RadiusM float64 `json:"radius_m"`
}

LCC is the Lambert conformal conic projection of the RRFS CONUS grid on a sphere (the parameters come from the file's header).

type Level

type Level string

Level is the coarse reading shown to people. Amounts are never claimed.

const (
	Dry  Level = "dry"
	Rain Level = "rain"
	Snow Level = "snow"
)

type Reading

type Reading struct {
	Level Level
	DBZ   float64
}

Reading is what the forecast says at one point and time.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service fetches, reprojects if it must, and caches radar tiles.

func New

func New(cfg Config) *Service

New returns a Service.

func (*Service) Frames

func (s *Service) Frames(ctx context.Context) (Frames, error)

Frames returns the newest frame and the recent ones, from the CONUS mosaic's time dimension (the others follow within seconds and are asked for by nearest time). Cached for a minute in process and across pods.

func (*Service) Handler

func (s *Service) Handler() http.Handler

Handler is the radar routes, mounted with its prefix stripped (/api/maps/radar):

GET /frame            -> {"frame":"2026-11-09T00:24:11Z","frame_unix":1794266651,"frames_unix":[...oldest first...],"min_zoom":3,"max_zoom":9}
GET /{z}/{x}/{y}.png  -> a 256x256 PNG, transparent where there is no radar (outside the US mosaics, or no echo).
                         Query t=<frame_unix> asks for one of the recent frames (anything else is the newest).

Both answer the frame time in X-Radar-Frame (RFC 3339, UTC). An unreachable or stale feed is 503 with Retry-After: the client hides the layer for a while; it is never a transparent 200, which would read as "no rain". Nothing about the caller is sent upstream or logged.

func (*Service) Tile

func (s *Service) Tile(ctx context.Context, z, x, y int, frameUnix int64) ([]byte, time.Time, error)

Tile returns the radar tile (z, x, y, Web Mercator) for the frame at frameUnix (0: the newest), the frame actually used, and the PNG. A tile outside every mosaic is a transparent PNG. A frame that is not one of the recent frames is replaced by the newest, so the time parameter is not an open relay into NOAA's history.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store fetches forecast hours from the in-cluster radar service and keeps decoded grids in a size-capped LRU.

func NewStore

func NewStore(baseURL string, hc *http.Client) *Store

NewStore returns a store reading from baseURL (for example http://radar.radar.svc), or nil when baseURL is empty (radar off).

func (*Store) At

func (s *Store) At(ctx context.Context, lat, lon float64, t time.Time) (Reading, bool)

At answers for a point at time t, or false when the forecast cannot say: radar is off, the point is outside the grid or the horizon, the newest usable cycle is too old, or the service did not answer. Absence is never reported as dry.

func (*Store) CachedBytes

func (s *Store) CachedBytes() int

CachedBytes is the decoded size currently held, for a gauge.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL