2025-09-21 03:51:25 +08:00

243 lines
7.7 KiB
Go

package radarfetch
import (
"fmt"
"os"
"path/filepath"
"time"
)
// StartJob launches a background ticker to download NMC images and CMA bin using Huanan time.
type Options struct {
OutRoot string
TZOffset int
Interval time.Duration
NMCChinaURL string
NMCHuananURL string
NMCNanningURL string
CMABase string
Z, Y, X int
}
// RunOnce executes a single download-render-save cycle.
func RunOnce(opts Options) error {
if opts.OutRoot == "" {
opts.OutRoot = "./radar_data"
}
if opts.TZOffset == 0 {
opts.TZOffset = 8
}
if opts.NMCChinaURL == "" {
opts.NMCChinaURL = "https://www.nmc.cn/publish/radar/chinaall.html"
}
if opts.NMCHuananURL == "" {
opts.NMCHuananURL = "https://www.nmc.cn/publish/radar/huanan.html"
}
if opts.NMCNanningURL == "" {
opts.NMCNanningURL = "https://www.nmc.cn/publish/radar/guang-xi/nan-ning.htm"
}
if opts.CMABase == "" {
opts.CMABase = "https://image.data.cma.cn"
}
if opts.Z == 0 && opts.Y == 0 && opts.X == 0 {
opts.Z, opts.Y, opts.X = 7, 40, 102
}
fmt.Printf("[radar] start run: out=%s z/y/x=%d/%d/%d tz=%d\n", opts.OutRoot, opts.Z, opts.Y, opts.X, opts.TZOffset)
err := runDownload(opts.OutRoot, opts.TZOffset, opts.NMCChinaURL, opts.NMCHuananURL, opts.NMCNanningURL, opts.CMABase, opts.Z, opts.Y, opts.X)
if err != nil {
fmt.Printf("[radar] run failed: %v\n", err)
return err
}
fmt.Println("[radar] run done")
return nil
}
// Run starts the periodic downloader (blocking ticker loop).
func Run(opts Options) {
if opts.OutRoot == "" {
opts.OutRoot = "./radar_data"
}
if opts.TZOffset == 0 {
opts.TZOffset = 8
}
if opts.Interval <= 0 {
opts.Interval = 10 * time.Minute
}
if opts.NMCChinaURL == "" {
opts.NMCChinaURL = "https://www.nmc.cn/publish/radar/chinaall.html"
}
if opts.NMCHuananURL == "" {
opts.NMCHuananURL = "https://www.nmc.cn/publish/radar/huanan.html"
}
if opts.NMCNanningURL == "" {
opts.NMCNanningURL = "https://www.nmc.cn/publish/radar/guang-xi/nan-ning.htm"
}
if opts.CMABase == "" {
opts.CMABase = "https://image.data.cma.cn"
}
if opts.Z == 0 && opts.Y == 0 && opts.X == 0 {
opts.Z, opts.Y, opts.X = 7, 40, 102
}
_ = RunOnce(opts)
t := time.NewTicker(opts.Interval)
for range t.C {
_ = RunOnce(opts)
}
}
func runDownload(outRoot string, tzOffset int, chinaURL, huananURL, nanningURL, cmaBase string, z, y, x int) error {
// 1) Fetch NMC pages and parse image/time (time from Huanan)
fmt.Println("[radar] fetch NMC China page ...")
chinaHTML, err := GetWithUA(chinaURL, DefaultUA, 15*time.Second, nil)
if err != nil {
return fmt.Errorf("fetch NMC China: %w", err)
}
chinaImg, _, ok := ExtractFirstImageAndTime(chinaHTML)
if !ok {
return fmt.Errorf("parse China page: data-img not found")
}
fmt.Println("[radar] fetch NMC Huanan page ...")
huananHTML, err := GetWithUA(huananURL, DefaultUA, 15*time.Second, nil)
if err != nil {
return fmt.Errorf("fetch NMC Huanan: %w", err)
}
huananImg, huananTime, ok := ExtractFirstImageAndTime(huananHTML)
if !ok {
return fmt.Errorf("parse Huanan page: data-img not found")
}
date, hour, minute, tsLocal := ParseNmcTime(huananTime, tzOffset)
fmt.Println("[radar] fetch NMC Nanning page ...")
nanningHTML, err := GetWithUA(nanningURL, DefaultUA, 15*time.Second, nil)
if err != nil {
return fmt.Errorf("fetch NMC Nanning: %w", err)
}
nanningImg, _, ok := ExtractFirstImageAndTime(nanningHTML)
if !ok {
return fmt.Errorf("parse Nanning page: data-img not found")
}
// Prepare out directory
outDir := filepath.Join(outRoot, fmt.Sprintf("%04d%02d%02d", date/10000, (date/100)%100, date%100), fmt.Sprintf("%02d", hour), fmt.Sprintf("%02d", minute))
if err := os.MkdirAll(outDir, 0o755); err != nil {
return fmt.Errorf("mkdir outDir: %w", err)
}
// Download three images (with Referer)
imgHeaders := map[string]string{
"Referer": "https://www.nmc.cn/",
"Accept": "image/avif,image/webp,image/apng,image/*,*/*;q=0.8",
}
fmt.Println("[radar] download China/Huanan/Nanning images ...")
if b, err := GetWithUA(chinaImg, DefaultUA, 20*time.Second, imgHeaders); err == nil {
_ = os.WriteFile(filepath.Join(outDir, "nmc_chinaall.png"), b, 0o644)
}
if b, err := GetWithUA(huananImg, DefaultUA, 20*time.Second, imgHeaders); err == nil {
_ = os.WriteFile(filepath.Join(outDir, "nmc_huanan.png"), b, 0o644)
}
if b, err := GetWithUA(nanningImg, DefaultUA, 20*time.Second, imgHeaders); err == nil {
_ = os.WriteFile(filepath.Join(outDir, "nmc_nanning.png"), b, 0o644)
}
// 2) Fetch CMA BIN with Huanan time
binURL := BuildCMAURL(cmaBase, date, hour, minute, z, y, x)
binHeaders := map[string]string{
"Referer": "https://data.cma.cn/",
"Origin": "https://data.cma.cn",
"User-Agent": DefaultUA,
"Accept": "*/*",
}
fmt.Printf("[radar] download CMA bin z/y/x=%d/%d/%d ...\n", z, y, x)
binBytes, err := GetWithUA(binURL, DefaultUA, 30*time.Second, binHeaders)
if err != nil {
return fmt.Errorf("fetch BIN: %w", err)
}
binPath := filepath.Join(outDir, fmt.Sprintf("%d-%d-%d.bin", z, y, x))
if err := os.WriteFile(binPath, binBytes, 0o644); err != nil {
return fmt.Errorf("save BIN: %w", err)
}
// Render BIN -> PNG
cmaPNG := filepath.Join(outDir, fmt.Sprintf("cma_%d-%d-%d.png", z, y, x))
fmt.Println("[radar] render bin -> png ...")
if err := RenderBinToPNG(binPath, cmaPNG, true); err != nil {
return fmt.Errorf("render PNG: %w", err)
}
// Decode grid and detect clusters (>=40 dBZ)
fmt.Println("[radar] decode grid & detect clusters ...")
grid := make([][]*float64, 256)
{
const w, h = 256, 256
if len(binBytes) == w*h*2 {
for row := 0; row < h; row++ {
line := make([]*float64, w)
for col := 0; col < w; col++ {
off := (row*w + col) * 2
u := uint16(binBytes[off])<<8 | uint16(binBytes[off+1])
vv := int16(u)
if vv == 32767 || vv < 0 {
line[col] = nil
continue
}
dbz := float64(vv) / 10.0
line[col] = &dbz
}
grid[row] = line
}
}
}
// 3) Write metadata and update latest
w, s, e, n, res := Bounds4326(z, y, x)
meta := Metadata{
TimestampLocal: tsLocal,
Date: date,
Hour: hour,
Minute: minute,
Z: z,
Y: y,
X: x,
Bounds: Bounds{West: w, South: s, East: e, North: n},
ResDeg: res,
Sources: Sources{NmcHTML: huananURL, NmcImg: huananImg, CmaBin: binURL},
Files: Files{HTML: "", PNG: filepath.Join(outDir, "nmc_huanan.png"), BIN: binPath, Metadata: filepath.Join(outDir, "metadata.json"), CMAPNG: cmaPNG},
Sizes: Sizes{PNG: fileSize(filepath.Join(outDir, "nmc_huanan.png")), BIN: int64(len(binBytes))},
CreatedAt: time.Now().Format(time.RFC3339),
}
// Attach clusters if grid decoded
if grid[0] != nil {
meta.Clusters = SegmentClusters(grid, Bounds{West: w, South: s, East: e, North: n}, res, 40.0)
// Render small PNGs per cluster
if len(meta.Clusters) > 0 {
if updated, err2 := AttachClusterPNGs(grid, 40.0, meta.Clusters, outDir); err2 == nil {
meta.Clusters = updated
}
}
meta.AnalysisNote = "clusters>=40dBZ; samples=center+4rays (N/S/E/W)"
// Build wind query plan with defaults
meta.QueryParams = QueryParams{MinAreaPx: 9, StrongDBZOverride: 50, MaxSamplesPerCluster: 5, MaxCandidatesTotal: 25}
cl2, cands := PlanWindQuery(meta.Clusters, meta.QueryParams)
meta.Clusters = cl2
meta.QueryCandidates = cands
}
if err := WriteMetadata(filepath.Join(outDir, "metadata.json"), &meta); err != nil {
return fmt.Errorf("write metadata: %w", err)
}
fmt.Println("[radar] update latest snapshot ...")
if err := UpdateLatest(outRoot, outDir, &meta); err != nil {
return fmt.Errorf("update latest: %w", err)
}
return nil
}
func fileSize(p string) int64 {
fi, err := os.Stat(p)
if err != nil {
return 0
}
return fi.Size()
}