129 lines
3.1 KiB
Go
129 lines
3.1 KiB
Go
// Package grib contains the GRIB-cube download skeleton shared by every
|
|
// NOAA source (GFS, GEFS, future families). It exposes the .idx parser,
|
|
// HTTP helpers, and a parallel download loop; source-specific URL
|
|
// templating is injected by the caller.
|
|
package grib
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// IdxEntry is one parsed line from a NOAA GRIB .idx file.
|
|
//
|
|
// Example line: "15:1207405:d=2024010100:HGT:1000 mb:0 hour fcst:"
|
|
type IdxEntry struct {
|
|
Index int
|
|
Offset int64
|
|
Variable string
|
|
LevelMB int // 0 when the level is not isobaric
|
|
Hour int // forecast hour; 0 for analysis ("anl"); -1 if unparseable
|
|
EndOffset int64 // computed from the next entry's Offset; -1 for the final entry
|
|
}
|
|
|
|
// Length returns the byte length of this GRIB message, or -1 if unknown
|
|
// (the final entry in an idx file).
|
|
func (e *IdxEntry) Length() int64 {
|
|
if e.EndOffset <= 0 {
|
|
return -1
|
|
}
|
|
return e.EndOffset - e.Offset
|
|
}
|
|
|
|
// ParseIdx parses a .idx file body. Unparseable lines are silently skipped.
|
|
func ParseIdx(body []byte) []IdxEntry {
|
|
lines := strings.Split(string(body), "\n")
|
|
var entries []IdxEntry
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.Split(line, ":")
|
|
if len(parts) < 7 {
|
|
continue
|
|
}
|
|
idx, err := strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
off, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
entries = append(entries, IdxEntry{
|
|
Index: idx,
|
|
Offset: off,
|
|
Variable: parts[3],
|
|
LevelMB: parseLevelMB(parts[4]),
|
|
Hour: parseHour(parts[5]),
|
|
EndOffset: -1,
|
|
})
|
|
}
|
|
for i := 0; i < len(entries)-1; i++ {
|
|
entries[i].EndOffset = entries[i+1].Offset
|
|
}
|
|
return entries
|
|
}
|
|
|
|
// FilterIdx returns entries matching one of the wanted variables at a known
|
|
// pressure level with a computable byte length.
|
|
func FilterIdx(entries []IdxEntry, wanted map[string]bool) []IdxEntry {
|
|
var out []IdxEntry
|
|
for _, e := range entries {
|
|
if !wanted[e.Variable] || e.LevelMB <= 0 || e.Length() <= 0 {
|
|
continue
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseLevelMB(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if !strings.HasSuffix(s, " mb") {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSuffix(s, " mb"))
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
func parseHour(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if s == "anl" {
|
|
return 0
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSuffix(s, " hour fcst"))
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
return n
|
|
}
|
|
|
|
// ByteRange is one HTTP range download corresponding to one GRIB message.
|
|
type ByteRange struct {
|
|
Start int64
|
|
End int64 // inclusive
|
|
Entry IdxEntry
|
|
}
|
|
|
|
// EntriesToRanges converts idx entries to inclusive HTTP byte ranges.
|
|
func EntriesToRanges(entries []IdxEntry) []ByteRange {
|
|
out := make([]ByteRange, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.Length() <= 0 {
|
|
continue
|
|
}
|
|
out = append(out, ByteRange{Start: e.Offset, End: e.EndOffset - 1, Entry: e})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FormatRange returns an HTTP Range header value for the byte range.
|
|
func (r ByteRange) FormatRange() string {
|
|
return fmt.Sprintf("bytes=%d-%d", r.Start, r.End)
|
|
}
|