package server import ( "database/sql" "encoding/binary" "net/http" "time" "weatherstation/core/internal/data" "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"` } func handleRainTimes(c *gin.Context) { z := parseInt(c.Query("z"), 7) y := parseInt(c.Query("y"), 40) x := parseInt(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": "invalid time range"}) 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 = data.DB().Query(qRange, z, y, x, from, to) } else { limit := parseInt(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 = data.DB().Query(q, z, y, x, limit) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"}) return } defer rows.Close() var times []string for rows.Next() { var dt time.Time if err := rows.Scan(&dt); err == nil { times = append(times, dt.In(loc).Format("2006-01-02 15:04:05")) } } c.JSON(http.StatusOK, gin.H{"times": times}) } func handleRainTilesAt(c *gin.Context) { z := parseInt(c.Query("z"), 7) dtStr := c.Query("dt") if dtStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing dt"}) 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": "invalid dt"}) return } const q = `SELECT dt,z,y,x,width,height,west,south,east,north,res_deg,data FROM rain_tiles WHERE z=$1 AND dt=$2 ORDER BY y,x` rows, qerr := data.DB().Query(q, z, dt) if qerr != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db failed"}) return } defer rows.Close() var tiles []rainTileResponse for rows.Next() { var r rainTileRecord if err := rows.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); err != nil { continue } w, h := r.Width, r.Height if w <= 0 || h <= 0 || len(r.Data) < w*h*2 { continue } vals := decodeRain(r.Data, w, h) tiles = append(tiles, rainTileResponse{DT: r.DT.In(loc).Format("2006-01-02 15:04:05"), Z: r.Z, Y: r.Y, X: r.X, Width: w, Height: h, West: r.West, South: r.South, East: r.East, North: r.North, ResDeg: r.ResDeg, Values: vals}) } if len(tiles) == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, gin.H{"tiles": tiles}) } 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 if mm < 0 { mm = 0 } vv := mm rowVals[col] = &vv } vals[row] = rowVals } return vals }