382 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package server
import (
"database/sql"
"encoding/binary"
"fmt"
"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"` // 单位mmnull 表示无效
}
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)
}
// rainTilesAtHandler 返回指定 z 在 dt 时次的全部雨量瓦片(不同 y/x集合
// GET /api/rain/tiles_at?z=7&dt=YYYY-MM-DD HH:MM:SS
func rainTilesAtHandler(c *gin.Context) {
z := parseIntDefault(c.Query("z"), 7)
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
}
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 := database.GetDB().Query(q, z, dt)
if qerr != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("数据库查询失败: %v", qerr)})
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: r.Width,
Height: r.Height,
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": "未找到指定时间的雨量瓦片集合"})
return
}
c.JSON(http.StatusOK, gin.H{
"dt": dt.In(loc).Format("2006-01-02 15:04:05"),
"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 // 0.1 mm 精度
if mm < 0 {
mm = 0
}
vv := mm
rowVals[col] = &vv
}
vals[row] = rowVals
}
return vals
}