288 lines
7.3 KiB
Go
288 lines
7.3 KiB
Go
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
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
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)
|
||
}
|