diff --git a/internal/server/gin.go b/internal/server/gin.go index 905534f..9fc19a4 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -40,6 +40,8 @@ func StartGinServer() error { api.GET("/data", getDataHandler) api.GET("/forecast", getForecastHandler) api.GET("/radar/latest", latestRadarTileHandler) + api.GET("/radar/at", radarTileAtHandler) + api.GET("/radar/times", radarTileTimesHandler) api.GET("/radar/weather_latest", latestRadarWeatherHandler) api.GET("/radar/weather_at", radarWeatherAtHandler) } diff --git a/internal/server/radar_api.go b/internal/server/radar_api.go index ba2d105..ddadd89 100644 --- a/internal/server/radar_api.go +++ b/internal/server/radar_api.go @@ -59,6 +59,23 @@ func getLatestRadarTile(db *sql.DB, z, y, x int) (*radarTileRecord, error) { 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,可通过查询参数覆盖 @@ -120,6 +137,124 @@ func latestRadarTileHandler(c *gin.Context) { 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 diff --git a/templates/radar_guangzhou.html b/templates/radar_guangzhou.html index 7654611..ca0032c 100644 --- a/templates/radar_guangzhou.html +++ b/templates/radar_guangzhou.html @@ -18,6 +18,22 @@ {{ template "header" . }}