yarnom a03c60469f Revert "feat: 新增雷达图"
This reverts commit 4fa9822405104095a9923e9762a2f65a1973d903.
2025-09-23 09:33:07 +08:00

203 lines
6.4 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)
}
// 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),
}
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()
}