Add SPC provider parsing helpers
This commit is contained in:
105
internal/providers/spc/geojson.go
Normal file
105
internal/providers/spc/geojson.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package spc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GeoJSONFeatureCollection is the minimal SPC outlook FeatureCollection shape
|
||||
// needed by weatherfeeder.
|
||||
type GeoJSONFeatureCollection struct {
|
||||
Type string `json:"type"`
|
||||
Features []GeoJSONFeature `json:"features"`
|
||||
}
|
||||
|
||||
// GeoJSONFeature preserves typed SPC properties and compact raw geometry.
|
||||
type GeoJSONFeature struct {
|
||||
Type string `json:"type"`
|
||||
Properties GeoJSONProperties `json:"properties"`
|
||||
Geometry json.RawMessage `json:"geometry"`
|
||||
}
|
||||
|
||||
// GeoJSONProperties contains the SPC fields used by canonical mapping.
|
||||
type GeoJSONProperties struct {
|
||||
ValidISO string `json:"VALID_ISO"`
|
||||
ExpireISO string `json:"EXPIRE_ISO"`
|
||||
IssueISO string `json:"ISSUE_ISO"`
|
||||
Forecaster string `json:"FORECASTER"`
|
||||
Label string `json:"LABEL"`
|
||||
Label2 string `json:"LABEL2"`
|
||||
DN *int `json:"DN"`
|
||||
}
|
||||
|
||||
// DecodeGeoJSON decodes an SPC GeoJSON outlook product and compacts feature
|
||||
// geometry JSON for stable downstream storage.
|
||||
func DecodeGeoJSON(raw []byte) (GeoJSONFeatureCollection, error) {
|
||||
var collection GeoJSONFeatureCollection
|
||||
if err := json.Unmarshal(raw, &collection); err != nil {
|
||||
return GeoJSONFeatureCollection{}, fmt.Errorf("decode geojson: %w", err)
|
||||
}
|
||||
for i := range collection.Features {
|
||||
geom, err := compactJSON(collection.Features[i].Geometry)
|
||||
if err != nil {
|
||||
return GeoJSONFeatureCollection{}, fmt.Errorf("features[%d].geometry: %w", i, err)
|
||||
}
|
||||
collection.Features[i].Geometry = geom
|
||||
}
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
func (p *GeoJSONProperties) UnmarshalJSON(raw []byte) error {
|
||||
type alias GeoJSONProperties
|
||||
var aux struct {
|
||||
alias
|
||||
DN any `json:"DN"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
*p = GeoJSONProperties(aux.alias)
|
||||
dn, err := parseSeverityRank(aux.DN)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.DN = dn
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSeverityRank(value any) (*int, error) {
|
||||
switch v := value.(type) {
|
||||
case nil:
|
||||
return nil, nil
|
||||
case float64:
|
||||
rank := int(v)
|
||||
if float64(rank) != v {
|
||||
return nil, fmt.Errorf("DN must be an integer, got %v", v)
|
||||
}
|
||||
return &rank, nil
|
||||
case string:
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
rank, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DN must be an integer, got %q", v)
|
||||
}
|
||||
return &rank, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("DN must be an integer or string, got %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func compactJSON(raw json.RawMessage) (json.RawMessage, error) {
|
||||
if len(raw) == 0 {
|
||||
return nil, fmt.Errorf("missing")
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := json.Compact(&buf, raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.RawMessage(buf.Bytes()), nil
|
||||
}
|
||||
Reference in New Issue
Block a user