2025-09-23 16:54:35 +08:00

423 lines
11 KiB
Go
Raw 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"
"math"
"net/http"
"time"
"weatherstation/internal/database"
"github.com/gin-gonic/gin"
)
type radarTileRecord 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 radarTileResponse 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"` // null 表示无效值
}
func getLatestRadarTile(db *sql.DB, z, y, x int) (*radarTileRecord, error) {
const q = `
SELECT dt, z, y, x, width, height, west, south, east, north, res_deg, data
FROM radar_tiles
WHERE z=$1 AND y=$2 AND x=$3
ORDER BY dt DESC
LIMIT 1`
var r radarTileRecord
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 getRadarTileAt(db *sql.DB, z, y, x int, dt time.Time) (*radarTileRecord, error) {
const q = `
SELECT dt, z, y, x, width, height, west, south, east, north, res_deg, data
FROM radar_tiles
WHERE z=$1 AND y=$2 AND x=$3 AND dt=$4
LIMIT 1`
var r radarTileRecord
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
}
// latestRadarTileHandler 返回指定 z/y/x 的最新瓦片,包含栅格 dBZ 值及元数据
func latestRadarTileHandler(c *gin.Context) {
// 固定默认 7/40/102可通过查询参数覆盖
z := parseIntDefault(c.Query("z"), 7)
y := parseIntDefault(c.Query("y"), 40)
x := parseIntDefault(c.Query("x"), 102)
rec, err := getLatestRadarTile(database.GetDB(), z, y, x)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到雷达瓦片"})
return
}
// 解码大端 int16 → dBZ (raw/10). >=32766 视为无效(null)
w, h := rec.Width, rec.Height
vals := make([][]*float64, h)
// 每行 256 单元,每单元 2 字节
if len(rec.Data) < w*h*2 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据长度异常"})
return
}
off := 0
for row := 0; row < h; row++ {
rowVals := make([]*float64, w)
for col := 0; col < w; col++ {
v := int16(binary.BigEndian.Uint16(rec.Data[off : off+2]))
off += 2
if v >= 32766 {
rowVals[col] = nil
continue
}
dbz := float64(v) / 10.0
// 限幅到 [0,75](大部分 CREF 标准范围),便于颜色映射
if dbz < 0 {
dbz = 0
} else if dbz > 75 {
dbz = 75
}
vv := dbz
rowVals[col] = &vv
}
vals[row] = rowVals
}
resp := radarTileResponse{
DT: rec.DT.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)
}
// radarTileAtHandler 返回指定 z/y/x 的指定时间瓦片
func radarTileAtHandler(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 := getRadarTileAt(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 := 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(rec.Data[off : off+2]))
off += 2
if v >= 32766 {
rowVals[col] = nil
continue
}
dbz := float64(v) / 10.0
if dbz < 0 {
dbz = 0
} else if dbz > 75 {
dbz = 75
}
vv := dbz
rowVals[col] = &vv
}
vals[row] = rowVals
}
resp := radarTileResponse{
DT: rec.DT.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)
}
// radarTileTimesHandler 返回指定 z/y/x 的可用时间列表(倒序)
func radarTileTimesHandler(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 radar_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 radar_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.Format("2006-01-02 15:04:05"))
}
c.JSON(http.StatusOK, gin.H{"times": times})
}
func parseIntDefault(s string, def int) int {
if s == "" {
return def
}
var n int
_, err := fmtSscanf(s, &n)
if err != nil || n == 0 || n == math.MinInt || n == math.MaxInt {
return def
}
return n
}
// fmtSscanf is a tiny wrapper to avoid importing fmt only for Sscanf
func fmtSscanf(s string, n *int) (int, error) {
// naive fast parse
sign := 1
i := 0
if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
if s[0] == '-' {
sign = -1
}
i = 1
}
val := 0
for ; i < len(s); i++ {
ch := s[i]
if ch < '0' || ch > '9' {
return 0, fmtError("invalid")
}
val = val*10 + int(ch-'0')
}
*n = sign * val
return 1, nil
}
type fmtError string
func (e fmtError) Error() string { return string(e) }
// ---------------- Radar station realtime API ----------------
type radarWeatherRecord struct {
Alias string
Lat float64
Lon float64
DT time.Time
Temperature float64
Humidity float64
Cloudrate float64
Visibility float64
Dswrf float64
WindSpeed float64
WindDirection float64
Pressure float64
}
type radarWeatherResponse struct {
Alias string `json:"alias"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
DT string `json:"dt"`
Temperature float64 `json:"temperature"`
Humidity float64 `json:"humidity"`
Cloudrate float64 `json:"cloudrate"`
Visibility float64 `json:"visibility"`
Dswrf float64 `json:"dswrf"`
WindSpeed float64 `json:"wind_speed"`
WindDirection float64 `json:"wind_direction"`
Pressure float64 `json:"pressure"`
}
func latestRadarWeatherHandler(c *gin.Context) {
alias := c.Query("alias")
if alias == "" {
alias = "南宁雷达站"
}
const q = `
SELECT alias, lat, lon, dt,
temperature, humidity, cloudrate, visibility, dswrf,
wind_speed, wind_direction, pressure
FROM radar_weather
WHERE alias = $1
ORDER BY dt DESC
LIMIT 1`
var r radarWeatherRecord
err := database.GetDB().QueryRow(q, alias).Scan(
&r.Alias, &r.Lat, &r.Lon, &r.DT,
&r.Temperature, &r.Humidity, &r.Cloudrate, &r.Visibility, &r.Dswrf,
&r.WindSpeed, &r.WindDirection, &r.Pressure,
)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到实时气象数据"})
return
}
resp := radarWeatherResponse{
Alias: r.Alias,
Lat: r.Lat,
Lon: r.Lon,
DT: r.DT.Format("2006-01-02 15:04:05"),
Temperature: r.Temperature,
Humidity: r.Humidity,
Cloudrate: r.Cloudrate,
Visibility: r.Visibility,
Dswrf: r.Dswrf,
WindSpeed: r.WindSpeed,
WindDirection: r.WindDirection,
Pressure: r.Pressure,
}
c.JSON(http.StatusOK, resp)
}
// radarWeatherAtHandler returns the radar weather record at the given dt (CST),
// rounded/exact 10-minute bucket time string "YYYY-MM-DD HH:MM:SS".
func radarWeatherAtHandler(c *gin.Context) {
alias := c.Query("alias")
if alias == "" {
alias = "南宁雷达站"
}
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 alias, lat, lon, dt,
temperature, humidity, cloudrate, visibility, dswrf,
wind_speed, wind_direction, pressure
FROM radar_weather
WHERE alias = $1 AND dt = $2
LIMIT 1`
var r radarWeatherRecord
err = database.GetDB().QueryRow(q, alias, dt).Scan(
&r.Alias, &r.Lat, &r.Lon, &r.DT,
&r.Temperature, &r.Humidity, &r.Cloudrate, &r.Visibility, &r.Dswrf,
&r.WindSpeed, &r.WindDirection, &r.Pressure,
)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到指定时间的实时气象数据"})
return
}
resp := radarWeatherResponse{
Alias: r.Alias,
Lat: r.Lat,
Lon: r.Lon,
DT: r.DT.Format("2006-01-02 15:04:05"),
Temperature: r.Temperature,
Humidity: r.Humidity,
Cloudrate: r.Cloudrate,
Visibility: r.Visibility,
Dswrf: r.Dswrf,
WindSpeed: r.WindSpeed,
WindDirection: r.WindDirection,
Pressure: r.Pressure,
}
c.JSON(http.StatusOK, resp)
}