From da67660fe78b0d2130e1f45a90bcf9ba4e9bdfb8 Mon Sep 17 00:00:00 2001 From: yarnom Date: Tue, 23 Sep 2025 09:33:12 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20=E6=96=B0=E5=A2=9E=E9=9B=B7?= =?UTF-8?q?=E8=BE=BE=E5=9B=BE"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 448b13c2f6eb9b505e516858c27b571eafca1879. --- cmd/radar/main.go | 47 ------ internal/radarfetch/fetch.go | 37 ----- internal/radarfetch/job.go | 202 -------------------------- internal/radarfetch/parse.go | 51 ------- internal/radarfetch/radar.go | 33 ----- internal/radarfetch/render.go | 97 ------------- internal/radarfetch/store.go | 89 ------------ internal/radarfetch/timeutil.go | 37 ----- internal/server/gin.go | 128 ----------------- templates/index.html | 245 +------------------------------- 10 files changed, 2 insertions(+), 964 deletions(-) delete mode 100644 cmd/radar/main.go delete mode 100644 internal/radarfetch/fetch.go delete mode 100644 internal/radarfetch/job.go delete mode 100644 internal/radarfetch/parse.go delete mode 100644 internal/radarfetch/radar.go delete mode 100644 internal/radarfetch/render.go delete mode 100644 internal/radarfetch/store.go delete mode 100644 internal/radarfetch/timeutil.go diff --git a/cmd/radar/main.go b/cmd/radar/main.go deleted file mode 100644 index b22fd2e..0000000 --- a/cmd/radar/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "flag" - "time" - "weatherstation/internal/radarfetch" -) - -func main() { - var outRoot string - var interval time.Duration - var tz int - var z, y, x int - var chinaURL, huananURL, nanningURL, cmaBase string - - flag.StringVar(&outRoot, "out-root", "./radar_data", "output root directory for radar data") - flag.DurationVar(&interval, "interval", 10*time.Minute, "download interval") - flag.IntVar(&tz, "tz-offset", 8, "local tz offset to UTC (hours)") - flag.IntVar(&z, "z", 7, "tile z") - flag.IntVar(&y, "y", 40, "tile y") - flag.IntVar(&x, "x", 102, "tile x") - flag.StringVar(&chinaURL, "nmc-china-url", "https://www.nmc.cn/publish/radar/chinaall.html", "NMC China page URL") - flag.StringVar(&huananURL, "nmc-huanan-url", "https://www.nmc.cn/publish/radar/huanan.html", "NMC Huanan page URL") - flag.StringVar(&nanningURL, "nmc-nanning-url", "https://www.nmc.cn/publish/radar/guang-xi/nan-ning.htm", "NMC Nanning page URL") - flag.StringVar(&cmaBase, "cma-base", "https://image.data.cma.cn", "CMA base URL") - var once bool - flag.BoolVar(&once, "once", false, "run a single cycle and exit") - flag.Parse() - - opts := radarfetch.Options{ - OutRoot: outRoot, - TZOffset: tz, - Interval: interval, - NMCChinaURL: chinaURL, - NMCHuananURL: huananURL, - NMCNanningURL: nanningURL, - CMABase: cmaBase, - Z: z, - Y: y, - X: x, - } - if once { - _ = radarfetch.RunOnce(opts) - return - } - radarfetch.Run(opts) -} diff --git a/internal/radarfetch/fetch.go b/internal/radarfetch/fetch.go deleted file mode 100644 index 3e0f629..0000000 --- a/internal/radarfetch/fetch.go +++ /dev/null @@ -1,37 +0,0 @@ -package radarfetch - -import ( - "io" - "net/http" - "time" -) - -const DefaultUA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36" - -func GetWithUA(url string, ua string, timeout time.Duration, extraHeaders map[string]string) ([]byte, error) { - c := &http.Client{Timeout: timeout} - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - if ua == "" { - ua = DefaultUA - } - req.Header.Set("User-Agent", ua) - for k, v := range extraHeaders { - req.Header.Set(k, v) - } - resp, err := c.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return nil, io.ErrUnexpectedEOF - } - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return b, nil -} diff --git a/internal/radarfetch/job.go b/internal/radarfetch/job.go deleted file mode 100644 index 32eb096..0000000 --- a/internal/radarfetch/job.go +++ /dev/null @@ -1,202 +0,0 @@ -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() -} diff --git a/internal/radarfetch/parse.go b/internal/radarfetch/parse.go deleted file mode 100644 index ab63ad8..0000000 --- a/internal/radarfetch/parse.go +++ /dev/null @@ -1,51 +0,0 @@ -package radarfetch - -import ( - "bytes" -) - -// ExtractFirstImageAndTime tries to extract data-img and data-time from NMC HTML. -// It first searches for an element with class "time" that carries data-img/time; -// falls back to the first occurrence of data-img / data-time attributes in the HTML. -func ExtractFirstImageAndTime(html []byte) (img string, timeStr string, ok bool) { - // naive scan for data-img and data-time on the same segment first - // Search for class="time" anchor to bias to the right element - idx := bytes.Index(html, []byte("class=\"time\"")) - start := 0 - if idx >= 0 { - // back up a bit to include attributes on same tag - if idx > 200 { - start = idx - 200 - } else { - start = 0 - } - } - seg := html[start:] - img = findAttr(seg, "data-img") - timeStr = findAttr(seg, "data-time") - if img != "" { - return img, timeStr, true - } - // fallback: first data-img anywhere - img = findAttr(html, "data-img") - timeStr = findAttr(html, "data-time") - if img != "" { - return img, timeStr, true - } - return "", "", false -} - -func findAttr(b []byte, key string) string { - // look for key="..." - pat := []byte(key + "=\"") - i := bytes.Index(b, pat) - if i < 0 { - return "" - } - i += len(pat) - j := bytes.IndexByte(b[i:], '"') - if j < 0 { - return "" - } - return string(b[i : i+j]) -} diff --git a/internal/radarfetch/radar.go b/internal/radarfetch/radar.go deleted file mode 100644 index 3cb7ca5..0000000 --- a/internal/radarfetch/radar.go +++ /dev/null @@ -1,33 +0,0 @@ -package radarfetch - -import ( - "fmt" - "math" -) - -func BuildCMAURL(base string, date int, hour int, minute int, z int, y int, x int) string { - yyyy := date / 10000 - mm := (date / 100) % 100 - dd := date % 100 - return fmt.Sprintf("%s/tiles/China/RADAR_L3_MST_CREF_GISJPG_Tiles_CR/%04d%02d%02d/%02d/%02d/%d/%d/%d.bin", - trimSlash(base), yyyy, mm, dd, hour, minute, z, y, x) -} - -func Bounds4326(z, y, x int) (west, south, east, north, resDeg float64) { - step := 360.0 / math.Ldexp(1.0, z) - west = -180.0 + float64(x)*step - east = west + step - south = -90.0 + float64(y)*step - north = south + step - resDeg = step / 256.0 - return -} - -func trimSlash(s string) string { - n := len(s) - for n > 0 && s[n-1] == '/' { - s = s[:n-1] - n-- - } - return s -} diff --git a/internal/radarfetch/render.go b/internal/radarfetch/render.go deleted file mode 100644 index ba8f10b..0000000 --- a/internal/radarfetch/render.go +++ /dev/null @@ -1,97 +0,0 @@ -package radarfetch - -import ( - "image" - "image/color" - "image/png" - "os" -) - -var colors15 = []string{ - "#0000F6", "#01A0F6", "#00ECEC", "#01FF00", "#00C800", - "#019000", "#FFFF00", "#E7C000", "#FF9000", "#FF0000", - "#D60000", "#C00000", "#FF00F0", "#780084", "#AD90F0", -} - -func hexToRGBA(s string, a uint8) color.RGBA { - if len(s) >= 7 && s[0] == '#' { - r := xtoi(s[1:3]) - g := xtoi(s[3:5]) - b := xtoi(s[5:7]) - return color.RGBA{uint8(r), uint8(g), uint8(b), a} - } - return color.RGBA{0, 0, 0, 0} -} - -func xtoi(h string) int { - v := 0 - for i := 0; i < len(h); i++ { - c := h[i] - v <<= 4 - switch { - case c >= '0' && c <= '9': - v |= int(c - '0') - case c >= 'a' && c <= 'f': - v |= int(c-'a') + 10 - case c >= 'A' && c <= 'F': - v |= int(c-'A') + 10 - } - } - return v -} - -func colorForDBZ(dbz float64) color.RGBA { - if dbz < 0 { - return color.RGBA{0, 0, 0, 0} - } - idx := int(dbz / 5.0) - if idx < 0 { - idx = 0 - } - if idx >= len(colors15) { - idx = len(colors15) - 1 - } - return hexToRGBA(colors15[idx], 255) -} - -// RenderBinToPNG renders 256x256 BE int16 .bin into a PNG using CMA-style colors. -func RenderBinToPNG(srcPath, dstPath string, flipY bool) error { - b, err := os.ReadFile(srcPath) - if err != nil { - return err - } - const w, h = 256, 256 - if len(b) != w*h*2 { - return ErrSize - } - img := image.NewRGBA(image.Rect(0, 0, w, h)) - for row := 0; row < h; row++ { - outRow := row - if flipY { - outRow = h - 1 - row - } - for col := 0; col < w; col++ { - off := (row*w + col) * 2 - u := uint16(b[off])<<8 | uint16(b[off+1]) - v := int16(u) - if v == 32767 || v < 0 { - img.SetRGBA(col, outRow, color.RGBA{0, 0, 0, 0}) - continue - } - dbz := float64(v) / 10.0 - img.SetRGBA(col, outRow, colorForDBZ(dbz)) - } - } - f, err := os.Create(dstPath) - if err != nil { - return err - } - defer f.Close() - return png.Encode(f, img) -} - -var ErrSize = errSize{} - -type errSize struct{} - -func (errSize) Error() string { return "unexpected .bin size (expected 131072 bytes)" } diff --git a/internal/radarfetch/store.go b/internal/radarfetch/store.go deleted file mode 100644 index 10e658f..0000000 --- a/internal/radarfetch/store.go +++ /dev/null @@ -1,89 +0,0 @@ -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"` -} - -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 { - _ = 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)) - } - return nil -} diff --git a/internal/radarfetch/timeutil.go b/internal/radarfetch/timeutil.go deleted file mode 100644 index a0b5f59..0000000 --- a/internal/radarfetch/timeutil.go +++ /dev/null @@ -1,37 +0,0 @@ -package radarfetch - -import ( - "strconv" - "strings" - "time" -) - -// ParseNmcTime parses like "MM/DD HH:MM" with a local tz offset (hours). -func ParseNmcTime(s string, tzOffset int) (date int, hour int, minute int, tsLocal string) { - parts := strings.Fields(s) - if len(parts) >= 2 { - md := strings.Split(parts[0], "/") - hm := strings.Split(parts[1], ":") - if len(md) == 2 && len(hm) == 2 { - now := time.Now().UTC().Add(time.Duration(tzOffset) * time.Hour) - y := now.Year() - m, _ := strconv.Atoi(md[0]) - d, _ := strconv.Atoi(md[1]) - h, _ := strconv.Atoi(hm[0]) - mm, _ := strconv.Atoi(hm[1]) - loc := time.FixedZone("LOCAL", tzOffset*3600) - t := time.Date(y, time.Month(m), d, h, mm, 0, 0, loc) - date = t.Year()*10000 + int(t.Month())*100 + t.Day() - hour = t.Hour() - minute = t.Minute() - tsLocal = t.Format("2006-01-02 15:04:05") - return - } - } - now := time.Now().UTC().Add(time.Duration(tzOffset) * time.Hour) - date = now.Year()*10000 + int(now.Month())*100 + now.Day() - hour = now.Hour() - minute = now.Minute() - tsLocal = now.Format("2006-01-02 15:04:05") - return -} diff --git a/internal/server/gin.go b/internal/server/gin.go index b2f4b67..ad36cb2 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -1,12 +1,9 @@ package server import ( - "encoding/json" "fmt" "log" "net/http" - "os" - "path" "strconv" "time" "weatherstation/internal/config" @@ -29,8 +26,6 @@ func StartGinServer() error { // 静态文件服务 r.Static("/static", "./static") - // 雷达数据静态目录(用于访问 latest 下的图片/二进制) - r.Static("/radar", "./radar_data") // 路由设置 r.GET("/", indexHandler) @@ -42,8 +37,6 @@ func StartGinServer() error { api.GET("/stations", getStationsHandler) api.GET("/data", getDataHandler) api.GET("/forecast", getForecastHandler) - api.GET("/radar/latest", radarLatestHandler) - api.GET("/radar/latest/grid", radarLatestGridHandler) } // 获取配置的Web端口 @@ -52,8 +45,6 @@ func StartGinServer() error { port = 10003 // 默认端口 } - // 备注:雷达抓取改为独立 CLI 触发,Web 服务不自动启动后台任务 - // 启动服务器 fmt.Printf("Gin Web服务器启动,监听端口 %d...\n", port) return r.Run(fmt.Sprintf(":%d", port)) @@ -70,8 +61,6 @@ func indexHandler(c *gin.Context) { c.HTML(http.StatusOK, "index.html", data) } -// 备注:雷达站采用前端 Tab(hash)切换,无需单独路由 - // systemStatusHandler 处理系统状态API请求 func systemStatusHandler(c *gin.Context) { status := types.SystemStatus{ @@ -215,120 +204,3 @@ func getForecastHandler(c *gin.Context) { log.Printf("查询到预报数据: %d 条", len(points)) c.JSON(http.StatusOK, points) } - -// radarLatestHandler 返回最新一次雷达抓取的元数据与图片URL -func radarLatestHandler(c *gin.Context) { - // 读取 latest/metadata.json - latestRoot := "./radar_data/latest" - metaPath := latestRoot + "/metadata.json" - b, err := os.ReadFile(metaPath) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "未找到最新雷达元数据"}) - return - } - var meta map[string]any - if err := json.Unmarshal(b, &meta); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "解析元数据失败"}) - return - } - // 构造图片URL(通过 /radar/latest/* 静态路径访问) - images := map[string]string{ - "china": "/radar/latest/nmc_chinaall.png", - "huanan": "/radar/latest/nmc_huanan.png", - "nanning": "/radar/latest/nmc_nanning.png", - } - if files, ok := meta["files"].(map[string]any); ok { - if v, ok2 := files["cma_png"].(string); ok2 && v != "" { - _, name := path.Split(v) - images["cma"] = "/radar/latest/" + name - } - } - c.JSON(http.StatusOK, gin.H{ - "meta": meta, - "images": images, - }) -} - -// radarLatestGridHandler 读取 latest 下的 z-y-x.bin 并返回 256x256 的 dBZ 二维数组(无效为 null) -func radarLatestGridHandler(c *gin.Context) { - latestRoot := "./radar_data/latest" - metaPath := latestRoot + "/metadata.json" - b, err := os.ReadFile(metaPath) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "未找到最新雷达元数据"}) - return - } - var meta map[string]any - if err := json.Unmarshal(b, &meta); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "解析元数据失败"}) - return - } - z := intFromMeta(meta, "z") - y := intFromMeta(meta, "y") - x := intFromMeta(meta, "x") - binName := fmt.Sprintf("%d-%d-%d.bin", z, y, x) - binPath := path.Join(latestRoot, binName) - buf, err := os.ReadFile(binPath) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "未找到最新BIN文件"}) - return - } - const w, h = 256, 256 - if len(buf) != w*h*2 { - c.JSON(http.StatusBadRequest, gin.H{"error": "BIN尺寸异常"}) - return - } - grid := make([][]*float64, h) - for r := 0; r < h; r++ { - row := make([]*float64, w) - for c2 := 0; c2 < w; c2++ { - off := (r*w + c2) * 2 - u := uint16(buf[off])<<8 | uint16(buf[off+1]) - v := int16(u) - if v == 32767 || v < 0 { - row[c2] = nil - continue - } - dbz := float64(v) / 10.0 - row[c2] = &dbz - } - grid[r] = row - } - bounds := map[string]float64{"west": 0, "south": 0, "east": 0, "north": 0} - if v, ok := meta["bounds"].(map[string]any); ok { - if f, ok2 := v["west"].(float64); ok2 { - bounds["west"] = f - } - if f, ok2 := v["south"].(float64); ok2 { - bounds["south"] = f - } - if f, ok2 := v["east"].(float64); ok2 { - bounds["east"] = f - } - if f, ok2 := v["north"].(float64); ok2 { - bounds["north"] = f - } - } - resDeg := 0.0 - if f, ok := meta["res_deg"].(float64); ok { - resDeg = f - } - c.JSON(http.StatusOK, gin.H{ - "z": z, "y": y, "x": x, - "bounds": bounds, - "res_deg": resDeg, - "grid": grid, - }) -} - -func intFromMeta(m map[string]any, key string) int { - if v, ok := m[key]; ok { - switch t := v.(type) { - case float64: - return int(t) - case int: - return t - } - } - return 0 -} diff --git a/templates/index.html b/templates/index.html index 4f36a18..ecef289 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,6 @@ -