All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
121 lines
3.2 KiB
Go
121 lines
3.2 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"`
|
|
}
|
|
|
|
type geometryMetadata struct {
|
|
Type string `json:"type"`
|
|
Geometries []json.RawMessage `json:"geometries"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// IsEmptyGeometryCollection reports whether raw is SPC's no-polygon placeholder
|
|
// geometry shape: a GeometryCollection with no child geometries.
|
|
func IsEmptyGeometryCollection(raw json.RawMessage) bool {
|
|
var meta geometryMetadata
|
|
if err := json.Unmarshal(raw, &meta); err != nil {
|
|
return false
|
|
}
|
|
return meta.Type == "GeometryCollection" && len(meta.Geometries) == 0
|
|
}
|
|
|
|
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
|
|
}
|