diff --git a/catalog.go b/catalog.go new file mode 100644 index 0000000000000000000000000000000000000000..a4aad6c5dac96127d6ffc8170cc276f988a46498 --- /dev/null +++ b/catalog.go @@ -0,0 +1,30 @@ +package radolan + +type spec struct { + px int // plain data dimensions + py int + + dx int // data (layer) dimensions + dy int + + rx float64 // resolution + ry float64 +} + +// local picture products do not provide dimensions in header +var catalog = map[string]spec{ + "OL": {200, 224, 200, 200, 2, 2}, // reflectivity (no clutter detection) + "OX": {200, 224, 200, 200, 1, 1}, // reflectivity (no clutter detection) + "PD": {200, 224, 200, 200, 1, 1}, // radial velocity + "PE": {200, 224, 200, 200, 2, 2}, // echotop + "PF": {200, 224, 200, 200, 1, 1}, // reflectivity (15 classes) + "PH": {200, 224, 200, 200, 1, 1}, // accumulated rainfall + "PL": {200, 224, 200, 200, 2, 2}, // reflectivity + "PM": {200, 224, 200, 200, 2, 2}, // max. reflectivity + "PR": {200, 224, 200, 200, 1, 1}, // radial velocity + "PU": {200, 2400, 200, 200, 1, 1}, // 3D radial velocity + "PV": {200, 224, 200, 200, 1, 1}, // radial velocity + "PX": {200, 224, 200, 200, 1, 1}, // reflectivity (6 classes) + "PY": {200, 224, 200, 200, 1, 1}, // accumulated rainfall + "PZ": {200, 2400, 200, 200, 2, 2}, // 3D reflectivity CAPPI +} diff --git a/data.go b/data.go index eddc7d629b34160d03a02e22409ad832b3785071..28e6c31ed2913a97b211747afdc50716548d37ca 100644 --- a/data.go +++ b/data.go @@ -29,7 +29,7 @@ func init() { // only comparing header characteristics. // This method requires header data to be already written. func (c *Composite) identifyEncoding() encoding { - values := c.Dx * c.Dy + values := c.Px * c.Py if c.level != nil { return runlength @@ -47,19 +47,35 @@ func (c *Composite) identifyEncoding() encoding { // parseData parses the composite data and writes the related fields. // This method requires header data to be already written. func (c *Composite) parseData(reader *bufio.Reader) error { - if c.Dx == 0 || c.Dy == 0 { + if c.Px == 0 || c.Py == 0 { return newError("parseData", "parsed header data required") } // create Data fields - c.Data = make([][]RVP6, c.Dy) - for i := range c.Data { - c.Data[i] = make([]RVP6, c.Dx) + c.PlainData = make([][]RVP6, c.Py) + for i := range c.PlainData { + c.PlainData[i] = make([]RVP6, c.Px) } return parse[c.identifyEncoding()](c, reader) } +// arrangeData slices plain data into its data layers or strips preceeding +// vertical projection +func (c *Composite) arrangeData() { + if c.Py%c.Dy == 0 { // multiple layers are linked downwards + c.DataZ = make([][][]RVP6, c.Py/c.Dy) + for i := range c.DataZ { + c.DataZ[i] = c.PlainData[c.Dy*i : c.Dy*(i+1)] // split layers + } + } else { // only use bottom most part of plain data + c.DataZ = [][][]RVP6{c.PlainData[c.Py-c.Dy:]} // strip elevation + } + + c.Dz = len(c.DataZ) + c.Data = c.DataZ[0] // alias +} + // parseUnknown performs no action and always returns an error. func (c *Composite) parseUnknown(rd *bufio.Reader) error { return newError("parseUnknown", "unknown encoding") diff --git a/header.go b/header.go index 8206e73fe25a51f9ef0abe0a784e515c076c2dbb..918681a1b48695b828bc02f913ece3cb6d615e35 100644 --- a/header.go +++ b/header.go @@ -89,14 +89,28 @@ func (c *Composite) parseHeader(reader *bufio.Reader) error { } } - // Parse Dimensions - Example: "GP 450x 450" or "BG460460" or "GP 1500x1400" - dim := section["GP"] - if bg, ok := section["BG"]; ok { - dim = bg[:len(bg)/2] + "x" + bg[len(bg)/2:] - } + // Parse Dimensions - Example: "GP 450x 450" or "BG460460" or "GP 1500x1400" (if defined) + if dim, ok := section["GP"]; ok { + if _, err := fmt.Sscanf(dim, "%dx%d", &c.Dy, &c.Dx); err != nil { + return newError("parseHeader", "could not parse dimensions (GP): "+err.Error()) + } + c.Px, c.Py = c.Dx, c.Dy // composite formats do not show elevation + + } else if dim, ok := section["BG"]; ok { + if _, err := fmt.Sscanf(dim, "%3d%3d", &c.Dy, &c.Dx); err != nil { + return newError("parseHeader", "could not parse dimensions (BG): "+err.Error()) + } + c.Px, c.Py = c.Dx, c.Dy // composite formats do not show elevation + + } else { // dimensions of local picture products not defined in header + v, ok := catalog[c.Product] // lookup in catalog + if !ok { + return newError("parseHeader", "no dimension information available") + } - if _, err := fmt.Sscanf(dim, "%dx%d", &c.Dy, &c.Dx); err != nil { - return newError("parseHeader", "could not parse dimensions: "+err.Error()) + c.Px, c.Py = v.px, v.py // plain data dimensions + c.Dx, c.Dy = v.dx, v.dy // data layer dimensions + c.Rx, c.Ry = v.rx, v.ry // data resolution } // Parse Precision - Example: "PR E-01" or "PR E+00" diff --git a/littleendian.go b/littleendian.go index c03e595941f88a75b246c36638a83fa9e1885e6f..7b884e9215b464a328439ddef74dbe36f761a4ff 100644 --- a/littleendian.go +++ b/littleendian.go @@ -7,16 +7,16 @@ import ( ) // parseLittleEndian parses the little endian encoded composite as described in [1] and [3]. -// Result are written into the previously created Data field of the composite. +// Result are written into the previously created PlainData field of the composite. func (c *Composite) parseLittleEndian(reader *bufio.Reader) error { - last := len(c.Data) - 1 - for i := range c.Data { + last := len(c.PlainData) - 1 + for i := range c.PlainData { line, err := c.readLineLittleEndian(reader) if err != nil { return err } - err = c.decodeLittleEndian(c.Data[last-i], line) // write vertically flipped + err = c.decodeLittleEndian(c.PlainData[last-i], line) // write vertically flipped if err != nil { return err } diff --git a/radolan.go b/radolan.go index 5262b5f8f141c36a93dff6a3a571bed303f11407..712ed4c25d721e12efc1f8f6be8fe3ef58371464 100644 --- a/radolan.go +++ b/radolan.go @@ -29,13 +29,16 @@ import ( "time" ) -// Radolan radar data is provided in so called composite formats. Each composite is a combined -// image consisting of mulitiple radar sweeps spread over the composite area. -// The composite c has a an internal resolution of c.Dx (horizontal) * c.Dy (vertical) records -// covering a real surface of c.Dx * c.Rx * c.Dy * c.Dy square kilometers. -// The pixel value at the position (x, y) is represented by c.Data[ y ][ x ] and is stored as -// raw rvp-6 value (NaN if the no-data flag is set). This rvp-6 value is used differently -// depending on the product type: +// Radolan radar data is provided as single local sweeps or so called composite +// formats. Each composite is a combined image consisting of mulitiple radar +// sweeps spread over the composite area. +// The 2D composite c has a an internal resolution of c.Dx (horizontal) * c.Dy +// (vertical) records covering a real surface of c.Dx * c.Rx * c.Dy * c.Dy +// square kilometers. +// The pixel value at the position (x, y) is represented by +// c.Data[ y ][ x ] and is stored as raw float value (called rvp-6) (NaN if the +// no-data flag is set). Some 3D radar products feature multiple layers in +// which the voxel at position (x, y, z) is accessible by c.DataZ[ z ][ y ][ x ]. // // The rvp-6 value is used differently depending on the product type: // @@ -69,10 +72,16 @@ type Composite struct { ForecastTime time.Time // data represents conditions predicted for this time Interval time.Duration // time duration until next forecast - Data [][]RVP6 // rvp-6 data for each point [y][x] + PlainData [][]RVP6 // rvp-6 data for parsed plain data element [y][x] + Px int // plain data width + Py int // plain data height + + DataZ [][][]RVP6 // rvp-6 data for each voxel [z][y][x] (composites use only one z-layer) + Data [][]RVP6 // rvp-6 data for each pixel [y][x] at layer 0 (alias for DataZ[0][x][y]) Dx int // data width Dy int // data height + Dz int // data layer Rx float64 // horizontal resolution in km/px Ry float64 // vertical resolution in km/px @@ -81,7 +90,7 @@ type Composite struct { dataLength int // length of binary section in bytes - precision int // multiplicator 10^precision for each raw value + precision int // multiplicator 10^precision for each raw value level []RVP6 // maps data value to corresponding rvp-6 value in runlength based formats offx float64 // horizontal projection offset @@ -102,6 +111,7 @@ func NewComposite(rd io.Reader) (comp *Composite, err error) { if err != nil { return } + comp.arrangeData() comp.calibrateProjection() @@ -153,10 +163,17 @@ func NewDummy(product string, dx, dy int) (comp *Composite) { // the given point. NaN is returned, if no data is available or the requested point is located // outside the scanned area. func (c *Composite) At(x, y int) RVP6 { - if x < 0 || y < 0 || x >= c.Dx || y >= c.Dy { + return c.AtZ(x, y, 0) +} + +// AtZ is shorthand for c.DataZ[z][y][x] and returns the radar video processor value (rvp-6) at +// the given point. NaN is returned, if no data is available or the requested point is located +// outside the scanned volume. +func (c *Composite) AtZ(x, y, z int) RVP6 { + if x < 0 || y < 0 || z < 0 || x >= c.Dx || y >= c.Dy || z >= c.Dz { return RVP6(math.NaN()) } - return c.Data[y][x] + return c.DataZ[z][y][x] } // newError returns an error indicating the failed function and reason diff --git a/runlength.go b/runlength.go index 6d6e654effae666430412d5902488bde4dee8e7a..59f1dccf83b82b8675eda18c6c87887c46c350b2 100644 --- a/runlength.go +++ b/runlength.go @@ -6,15 +6,15 @@ import ( ) // parseRunlength parses the runlength encoded composite and writes into the -// previously created Data field of the composite. +// previously created PlainData field of the composite. func (c *Composite) parseRunlength(reader *bufio.Reader) error { - for i := range c.Data { + for i := range c.PlainData { line, err := c.readLineRunlength(reader) if err != nil { return err } - err = c.decodeRunlength(c.Data[i], line) + err = c.decodeRunlength(c.PlainData[i], line) if err != nil { return err } diff --git a/singlebyte.go b/singlebyte.go index 567b06d2c158aaaa6cdc4a8b4d6770032d4c45ef..1570ca2fbff4ec331d566e99cbf94b72c1598edc 100644 --- a/singlebyte.go +++ b/singlebyte.go @@ -7,16 +7,16 @@ import ( ) // parseSingleByte parses the single byte encoded composite as described in [1] and writes -// into the previously created Data field of the composite. +// into the previously created PlainData field of the composite. func (c *Composite) parseSingleByte(reader *bufio.Reader) error { - last := len(c.Data) - 1 - for i := range c.Data { + last := len(c.PlainData) - 1 + for i := range c.PlainData { line, err := c.readLineSingleByte(reader) if err != nil { return err } - err = c.decodeSingleByte(c.Data[last-i], line) // write vertically flipped + err = c.decodeSingleByte(c.PlainData[last-i], line) // write vertically flipped if err != nil { return err }