package server import ( "database/sql" "encoding/binary" "net/http" "time" "weatherstation/internal/database" "github.com/gin-gonic/gin" ) type rainTileRecord struct { DT time.Time Z int Y int X int Width int Height int West float64 South float64 East float64 North float64 ResDeg float64 Data []byte } type rainTileResponse struct { DT string `json:"dt"` Z int `json:"z"` Y int `json:"y"` X int `json:"x"` Width int `json:"width"` Height int `json:"height"` West float64 `json:"west"` South float64 `json:"south"` East float64 `json:"east"` North float64 `json:"north"` ResDeg float64 `json:"res_deg"` Values [][]*float64 `json:"values"` // 单位:mm,null 表示无效 } func getLatestRainTile(db *sql.DB, z, y, x int) (*rainTileRecord, error) { const q = ` SELECT dt, z, y, x, width, height, west, south, east, north, res_deg, data FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT 1` var r rainTileRecord err := db.QueryRow(q, z, y, x).Scan( &r.DT, &r.Z, &r.Y, &r.X, &r.Width, &r.Height, &r.West, &r.South, &r.East, &r.North, &r.ResDeg, &r.Data, ) if err != nil { return nil, err } return &r, nil } func getRainTileAt(db *sql.DB, z, y, x int, dt time.Time) (*rainTileRecord, error) { const q = ` SELECT dt, z, y, x, width, height, west, south, east, north, res_deg, data FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt=$4 LIMIT 1` var r rainTileRecord err := db.QueryRow(q, z, y, x, dt).Scan( &r.DT, &r.Z, &r.Y, &r.X, &r.Width, &r.Height, &r.West, &r.South, &r.East, &r.North, &r.ResDeg, &r.Data, ) if err != nil { return nil, err } return &r, nil } // latestRainTileHandler 返回指定 z/y/x 的最新一小时降雨瓦片 func latestRainTileHandler(c *gin.Context) { z := parseIntDefault(c.Query("z"), 7) y := parseIntDefault(c.Query("y"), 40) x := parseIntDefault(c.Query("x"), 102) rec, err := getLatestRainTile(database.GetDB(), z, y, x) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "未找到雨量瓦片"}) return } w, h := rec.Width, rec.Height if len(rec.Data) < w*h*2 { c.JSON(http.StatusInternalServerError, gin.H{"error": "数据长度异常"}) return } vals := decodeRain(rec.Data, w, h) loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } resp := rainTileResponse{ DT: rec.DT.In(loc).Format("2006-01-02 15:04:05"), Z: rec.Z, Y: rec.Y, X: rec.X, Width: rec.Width, Height: rec.Height, West: rec.West, South: rec.South, East: rec.East, North: rec.North, ResDeg: rec.ResDeg, Values: vals, } c.JSON(http.StatusOK, resp) } // rainTileAtHandler 返回指定 z/y/x 的指定时间(CST)瓦片 func rainTileAtHandler(c *gin.Context) { z := parseIntDefault(c.Query("z"), 7) y := parseIntDefault(c.Query("y"), 40) x := parseIntDefault(c.Query("x"), 102) dtStr := c.Query("dt") if dtStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 dt 参数(YYYY-MM-DD HH:MM:SS)"}) return } loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } dt, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 dt 格式"}) return } rec, err := getRainTileAt(database.GetDB(), z, y, x, dt) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "未找到指定时间瓦片"}) return } w, h := rec.Width, rec.Height if len(rec.Data) < w*h*2 { c.JSON(http.StatusInternalServerError, gin.H{"error": "数据长度异常"}) return } vals := decodeRain(rec.Data, w, h) // 始终以 CST(+8) 输出 resp := rainTileResponse{ DT: rec.DT.In(loc).Format("2006-01-02 15:04:05"), Z: rec.Z, Y: rec.Y, X: rec.X, Width: rec.Width, Height: rec.Height, West: rec.West, South: rec.South, East: rec.East, North: rec.North, ResDeg: rec.ResDeg, Values: vals, } c.JSON(http.StatusOK, resp) } // rainTileTimesHandler 返回指定 z/y/x 的可用时间列表(倒序) func rainTileTimesHandler(c *gin.Context) { z := parseIntDefault(c.Query("z"), 7) y := parseIntDefault(c.Query("y"), 40) x := parseIntDefault(c.Query("x"), 102) fromStr := c.Query("from") toStr := c.Query("to") loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } var rows *sql.Rows var err error if fromStr != "" && toStr != "" { from, err1 := time.ParseInLocation("2006-01-02 15:04:05", fromStr, loc) to, err2 := time.ParseInLocation("2006-01-02 15:04:05", toStr, loc) if err1 != nil || err2 != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 from/to 时间格式"}) return } const qRange = ` SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY dt DESC` rows, err = database.GetDB().Query(qRange, z, y, x, from, to) } else { limit := parseIntDefault(c.Query("limit"), 48) const q = ` SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 ORDER BY dt DESC LIMIT $4` rows, err = database.GetDB().Query(q, z, y, x, limit) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "查询时间列表失败"}) return } defer rows.Close() var times []string for rows.Next() { var dt time.Time if err := rows.Scan(&dt); err != nil { continue } times = append(times, dt.In(loc).Format("2006-01-02 15:04:05")) } c.JSON(http.StatusOK, gin.H{"times": times}) } // nearestRainTileHandler 返回最接近给定时间的瓦片 func nearestRainTileHandler(c *gin.Context) { z := parseIntDefault(c.Query("z"), 7) y := parseIntDefault(c.Query("y"), 40) x := parseIntDefault(c.Query("x"), 102) dtStr := c.Query("dt") if dtStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 dt 参数(YYYY-MM-DD HH:MM:SS)"}) return } tolMin := parseIntDefault(c.Query("tolerance_min"), 90) prefer := c.DefaultQuery("prefer", "lte") // lte|nearest loc, _ := time.LoadLocation("Asia/Shanghai") if loc == nil { loc = time.FixedZone("CST", 8*3600) } target, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 dt 格式"}) return } from := target.Add(-time.Duration(tolMin) * time.Minute) to := target.Add(time.Duration(tolMin) * time.Minute) db := database.GetDB() var row *sql.Row if prefer == "lte" { const q = ` SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 AND dt <= $6 ORDER BY ($6 - dt) ASC LIMIT 1` row = db.QueryRow(q, z, y, x, from, to, target) } else { const q = ` SELECT dt FROM rain_tiles WHERE z=$1 AND y=$2 AND x=$3 AND dt BETWEEN $4 AND $5 ORDER BY ABS(EXTRACT(EPOCH FROM (dt - $6))) ASC LIMIT 1` row = db.QueryRow(q, z, y, x, from, to, target) } var picked time.Time if err := row.Scan(&picked); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "在容差范围内未找到匹配瓦片"}) return } rec, err := getRainTileAt(db, z, y, x, picked) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "未找到匹配瓦片数据"}) return } w, h := rec.Width, rec.Height if len(rec.Data) < w*h*2 { c.JSON(http.StatusInternalServerError, gin.H{"error": "数据长度异常"}) return } vals := decodeRain(rec.Data, w, h) // 以 CST(+8) 输出 resp := rainTileResponse{ DT: rec.DT.In(loc).Format("2006-01-02 15:04:05"), Z: rec.Z, Y: rec.Y, X: rec.X, Width: rec.Width, Height: rec.Height, West: rec.West, South: rec.South, East: rec.East, North: rec.North, ResDeg: rec.ResDeg, Values: vals, } c.JSON(http.StatusOK, resp) } func decodeRain(buf []byte, w, h int) [][]*float64 { vals := make([][]*float64, h) off := 0 for row := 0; row < h; row++ { rowVals := make([]*float64, w) for col := 0; col < w; col++ { v := int16(binary.BigEndian.Uint16(buf[off : off+2])) off += 2 if v >= 32766 { // 无效 rowVals[col] = nil continue } mm := float64(v) / 10.0 // 0.1 mm 精度 if mm < 0 { mm = 0 } vv := mm rowVals[col] = &vv } vals[row] = rowVals } return vals }