diff --git a/internal/server/gin.go b/internal/server/gin.go index ef5fdd6..751e887 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -138,6 +138,7 @@ func StartGinServer() error { api.GET("/rain/at", rainTileAtHandler) api.GET("/rain/nearest", nearestRainTileHandler) api.GET("/rain/times", rainTileTimesHandler) + api.GET("/rain/tiles_at", rainTilesAtHandler) } // 获取配置的Web端口 diff --git a/internal/server/rain_api.go b/internal/server/rain_api.go index 9ed7079..9f57671 100644 --- a/internal/server/rain_api.go +++ b/internal/server/rain_api.go @@ -3,6 +3,7 @@ package server import ( "database/sql" "encoding/binary" + "fmt" "net/http" "time" "weatherstation/internal/database" @@ -286,6 +287,75 @@ func nearestRainTileHandler(c *gin.Context) { 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 diff --git a/static/js/weather-app.js b/static/js/weather-app.js index d8d2b87..d43417d 100644 --- a/static/js/weather-app.js +++ b/static/js/weather-app.js @@ -269,11 +269,22 @@ const WeatherMap = { try{ const z=this.tileZ,y=this.tileY,x=this.tileX; if (this.tileProduct === 'rain') { - const url = `/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`; + // 雨量:同一时次下叠加所有 y/x 瓦片 + const url = `/api/rain/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`; const r = await fetch(url); - if(!r.ok){ console.warn('雨量瓦片未找到', dtStr); return; } - const t = await r.json(); - await this.renderTileOnMap('rain', t); + if(!r.ok){ + // 回退单块接口 + const url1 = `/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`; + const r1 = await fetch(url1); + if(!r1.ok){ console.warn('雨量瓦片未找到', dtStr); return; } + const t1 = await r1.json(); + await this.renderTileOnMap('rain', t1); + return; + } + const j = await r.json(); + const tiles = Array.isArray(j.tiles) ? j.tiles : []; + if (tiles.length === 0) { console.warn('该时次无雨量瓦片集合'); this.clearTileOverlays(); return; } + await this.renderTilesOnMap('rain', tiles); } else { // radar: 取同一时次该 z 下的所有 y/x 瓦片 const url = `/api/radar/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`; @@ -326,7 +337,8 @@ const WeatherMap = { if (product==='rain'){ const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity]; const colors = [ - [255,255,255], [126,212,121], [110,200,109], [97,169,97], [81,148,76], [90,158,112], + // 0值透明,(0,5) 用绿色,不再用白色 + [126,212,121], [126,212,121], [110,200,109], [97,169,97], [81,148,76], [90,158,112], [143,194,254], [92,134,245], [66,87,240], [45,48,214], [26,15,166], [63,22,145], [191,70,148], [213,1,146], [213,1,146] ]; colorFunc = (mm)=>{ @@ -382,13 +394,28 @@ const WeatherMap = { canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); const imgData = ctx.createImageData(w, h); - const colors = [[0,0,255],[0,191,255],[0,255,255],[127,255,212],[124,252,0],[173,255,47],[255,255,0],[255,215,0],[255,165,0],[255,140,0],[255,69,0],[255,0,0],[220,20,60],[199,21,133],[139,0,139]]; - const colorFunc = (dbz)=>{ - let v = Math.max(0, Math.min(75, dbz)); - if (v===0) return [0,0,0,0]; - let bin = Math.floor(v/5); if (bin>=colors.length) bin=colors.length-1; - const c = colors[bin]; return [c[0],c[1],c[2],220]; - }; + let colorFunc; + if (product==='rain'){ + const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity]; + const colors = [ + // 0值透明,(0,5) 用绿色,不再用白色 + [126,212,121], [126,212,121], [110,200,109], [97,169,97], [81,148,76], [90,158,112], + [143,194,254], [92,134,245], [66,87,240], [45,48,214], [26,15,166], [63,22,145], [191,70,148], [213,1,146], [213,1,146] + ]; + colorFunc = (mm)=>{ + if (mm===0) return [0,0,0,0]; + let idx=0; while(idx=edges[idx] && mm{ + let v = Math.max(0, Math.min(75, dbz)); + if (v===0) return [0,0,0,0]; + let bin = Math.floor(v/5); if (bin>=colors.length) bin=colors.length-1; + const c = colors[bin]; return [c[0],c[1],c[2],220]; + }; + } for(let row=0; row叠加显示: