forked from gsn/predictor
157 lines
3.6 KiB
Go
157 lines
3.6 KiB
Go
package downloader
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// IdxEntry represents a single parsed line from a GRIB .idx file.
|
|
// Example line: "15:1207405:d=2024010100:HGT:1000 mb:0 hour fcst:"
|
|
type IdxEntry struct {
|
|
Index int
|
|
Offset int64
|
|
Variable string // "HGT", "UGRD", "VGRD", etc.
|
|
LevelMB int // pressure level in mb (0 if not a pressure level)
|
|
Hour int // forecast hour
|
|
EndOffset int64 // byte after this message (from next entry's offset, or -1 if last)
|
|
}
|
|
|
|
// Length returns the byte length of this GRIB message, or -1 if unknown.
|
|
func (e *IdxEntry) Length() int64 {
|
|
if e.EndOffset <= 0 {
|
|
return -1
|
|
}
|
|
return e.EndOffset - e.Offset
|
|
}
|
|
|
|
// ParseIdx parses a .idx file body and returns all entries.
|
|
// Lines that can't be parsed 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
|
|
}
|
|
|
|
offset, err := strconv.ParseInt(parts[1], 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
variable := parts[3]
|
|
levelStr := parts[4]
|
|
hourStr := parts[5]
|
|
|
|
levelMB := parseLevelMB(levelStr)
|
|
hour := parseHour(hourStr)
|
|
|
|
entries = append(entries, IdxEntry{
|
|
Index: idx,
|
|
Offset: offset,
|
|
Variable: variable,
|
|
LevelMB: levelMB,
|
|
Hour: hour,
|
|
EndOffset: -1, // filled in below
|
|
})
|
|
}
|
|
|
|
// Fill in EndOffset from the next entry's Offset.
|
|
for i := 0; i < len(entries)-1; i++ {
|
|
entries[i].EndOffset = entries[i+1].Offset
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
// FilterIdx returns entries matching the given variables at pressure levels.
|
|
// Only entries with a recognized pressure level (levelMB > 0) are returned.
|
|
func FilterIdx(entries []IdxEntry, variables map[string]bool) []IdxEntry {
|
|
var filtered []IdxEntry
|
|
for _, e := range entries {
|
|
if !variables[e.Variable] {
|
|
continue
|
|
}
|
|
if e.LevelMB <= 0 {
|
|
continue
|
|
}
|
|
// Must have a known length (not the last entry) or be handled specially
|
|
if e.Length() <= 0 {
|
|
continue
|
|
}
|
|
filtered = append(filtered, e)
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// parseLevelMB parses a level string like "1000 mb" and returns the pressure in mb.
|
|
// Returns 0 if not a pressure level.
|
|
func parseLevelMB(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if !strings.HasSuffix(s, " mb") {
|
|
return 0
|
|
}
|
|
numStr := strings.TrimSuffix(s, " mb")
|
|
n, err := strconv.Atoi(numStr)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
// parseHour parses a forecast hour string like "0 hour fcst" or "anl".
|
|
// Returns -1 if it can't be parsed.
|
|
func parseHour(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if s == "anl" {
|
|
return 0
|
|
}
|
|
s = strings.TrimSuffix(s, " hour fcst")
|
|
n, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
return n
|
|
}
|
|
|
|
// GroupByRange groups idx entries into byte ranges suitable for HTTP Range downloads.
|
|
// Each range covers one contiguous GRIB message.
|
|
type ByteRange struct {
|
|
Start int64
|
|
End int64 // inclusive
|
|
Entry IdxEntry
|
|
}
|
|
|
|
// EntriesToRanges converts filtered idx entries to byte ranges.
|
|
func EntriesToRanges(entries []IdxEntry) []ByteRange {
|
|
ranges := make([]ByteRange, 0, len(entries))
|
|
for _, e := range entries {
|
|
if e.Length() <= 0 {
|
|
continue
|
|
}
|
|
ranges = append(ranges, ByteRange{
|
|
Start: e.Offset,
|
|
End: e.EndOffset - 1, // inclusive
|
|
Entry: e,
|
|
})
|
|
}
|
|
return ranges
|
|
}
|
|
|
|
// FormatRange returns an HTTP Range header value for a byte range.
|
|
func (r ByteRange) FormatRange() string {
|
|
return fmt.Sprintf("bytes=%d-%d", r.Start, r.End)
|
|
}
|