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 }