160 lines
4.5 KiB
Go
160 lines
4.5 KiB
Go
package radarfetch
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
type Bounds struct {
|
|
West float64 `json:"west"`
|
|
South float64 `json:"south"`
|
|
East float64 `json:"east"`
|
|
North float64 `json:"north"`
|
|
}
|
|
|
|
type Sources struct {
|
|
NmcHTML string `json:"nmc_html"`
|
|
NmcImg string `json:"nmc_img"`
|
|
CmaBin string `json:"cma_bin"`
|
|
}
|
|
|
|
type Files struct {
|
|
HTML string `json:"html"`
|
|
PNG string `json:"png"`
|
|
BIN string `json:"bin"`
|
|
Metadata string `json:"metadata"`
|
|
CMAPNG string `json:"cma_png"`
|
|
}
|
|
|
|
type Sizes struct {
|
|
PNG int64 `json:"png"`
|
|
BIN int64 `json:"bin"`
|
|
}
|
|
|
|
type Metadata struct {
|
|
TimestampLocal string `json:"timestamp_local"`
|
|
Date int `json:"date"`
|
|
Hour int `json:"hour"`
|
|
Minute int `json:"minute"`
|
|
Z int `json:"z"`
|
|
Y int `json:"y"`
|
|
X int `json:"x"`
|
|
Bounds Bounds `json:"bounds"`
|
|
ResDeg float64 `json:"res_deg"`
|
|
Sources Sources `json:"sources"`
|
|
Files Files `json:"files"`
|
|
Sizes Sizes `json:"sizes"`
|
|
CreatedAt string `json:"created_at"`
|
|
// Cloud clusters detected from single-frame CREF (>=40 dBZ)
|
|
// Optional; may be empty when detection fails.
|
|
Clusters []Cluster `json:"clusters,omitempty"`
|
|
// Optional notes about sampling strategy or thresholds used
|
|
AnalysisNote string `json:"analysis_note,omitempty"`
|
|
// Wind query planning parameters and candidates
|
|
QueryParams QueryParams `json:"query_params,omitempty"`
|
|
QueryCandidates []QueryCandidate `json:"query_candidates,omitempty"`
|
|
}
|
|
|
|
// Cluster represents a connected echo region above threshold.
|
|
type Cluster struct {
|
|
ID int `json:"id"`
|
|
AreaPx int `json:"area_px"`
|
|
MaxDBZ float64 `json:"max_dbz"`
|
|
AvgDBZ float64 `json:"avg_dbz"`
|
|
// Pixel-space centroid (row, col) using dBZ-weighted center
|
|
Row int `json:"row"`
|
|
Col int `json:"col"`
|
|
// Centroid lon/lat of pixel center
|
|
Lon float64 `json:"lon"`
|
|
Lat float64 `json:"lat"`
|
|
// Bounding box in pixel coords (inclusive)
|
|
MinRow int `json:"min_row"`
|
|
MinCol int `json:"min_col"`
|
|
MaxRow int `json:"max_row"`
|
|
MaxCol int `json:"max_col"`
|
|
// Recommended sample points for downstream wind queries
|
|
Samples []Sample `json:"samples"`
|
|
// Optional path to a small PNG rendering of this cluster (copied to latest)
|
|
PNG string `json:"png,omitempty"`
|
|
// Eligibility for downstream wind query
|
|
EligibleForQuery bool `json:"eligible_for_query,omitempty"`
|
|
SkipReason string `json:"skip_reason,omitempty"`
|
|
}
|
|
|
|
type Sample struct {
|
|
Row int `json:"row"`
|
|
Col int `json:"col"`
|
|
Lon float64 `json:"lon"`
|
|
Lat float64 `json:"lat"`
|
|
// role: center | ray_n | ray_s | ray_e | ray_w
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
// Parameters controlling wind query candidate selection.
|
|
type QueryParams struct {
|
|
MinAreaPx int `json:"min_area_px"`
|
|
StrongDBZOverride float64 `json:"strong_dbz_override"`
|
|
MaxSamplesPerCluster int `json:"max_samples_per_cluster"`
|
|
MaxCandidatesTotal int `json:"max_candidates_total"`
|
|
}
|
|
|
|
// A single candidate point to query external wind API.
|
|
type QueryCandidate struct {
|
|
ClusterID int `json:"cluster_id"`
|
|
Role string `json:"role"`
|
|
Lon float64 `json:"lon"`
|
|
Lat float64 `json:"lat"`
|
|
}
|
|
|
|
func WriteMetadata(path string, m *Metadata) error {
|
|
b, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, b, 0o644)
|
|
}
|
|
|
|
func UpdateLatest(root string, curDir string, m *Metadata) error {
|
|
latest := filepath.Join(root, "latest")
|
|
if err := os.MkdirAll(latest, 0o755); err != nil {
|
|
return err
|
|
}
|
|
// Write latest.json
|
|
b, _ := json.MarshalIndent(struct {
|
|
Dir string `json:"dir"`
|
|
Meta *Metadata `json:"meta"`
|
|
}{Dir: curDir, Meta: m}, "", " ")
|
|
_ = os.WriteFile(filepath.Join(latest, "latest.json"), b, 0o644)
|
|
|
|
copyFile := func(name string) {
|
|
dst := filepath.Join(latest, name)
|
|
src := filepath.Join(curDir, name)
|
|
data, e2 := os.ReadFile(src)
|
|
if e2 == nil {
|
|
// ensure parent dir exists for nested paths like "clusters/..."
|
|
_ = os.MkdirAll(filepath.Dir(dst), 0o755)
|
|
_ = os.WriteFile(dst, data, 0o644)
|
|
}
|
|
}
|
|
copyFile("nmc_chinaall.png")
|
|
copyFile("nmc_huanan.png")
|
|
copyFile("nmc_nanning.png")
|
|
copyFile("metadata.json")
|
|
copyFile(fmt.Sprintf("%d-%d-%d.bin", m.Z, m.Y, m.X))
|
|
if m.Files.CMAPNG != "" {
|
|
copyFile(filepath.Base(m.Files.CMAPNG))
|
|
}
|
|
// copy cluster PNGs if present
|
|
if len(m.Clusters) > 0 {
|
|
for _, cl := range m.Clusters {
|
|
if cl.PNG == "" {
|
|
continue
|
|
}
|
|
copyFile(cl.PNG)
|
|
}
|
|
}
|
|
return nil
|
|
}
|