Files
weatherfeeder/internal/providers/spc/geojson.go

106 lines
2.7 KiB
Go

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
}