feat: 优化地图瓦片显示

This commit is contained in:
yarnom 2025-10-14 18:08:14 +08:00
parent 7b3ce46f04
commit 93a94c3149
4 changed files with 111 additions and 13 deletions

View File

@ -138,6 +138,7 @@ func StartGinServer() error {
api.GET("/rain/at", rainTileAtHandler) api.GET("/rain/at", rainTileAtHandler)
api.GET("/rain/nearest", nearestRainTileHandler) api.GET("/rain/nearest", nearestRainTileHandler)
api.GET("/rain/times", rainTileTimesHandler) api.GET("/rain/times", rainTileTimesHandler)
api.GET("/rain/tiles_at", rainTilesAtHandler)
} }
// 获取配置的Web端口 // 获取配置的Web端口

View File

@ -3,6 +3,7 @@ package server
import ( import (
"database/sql" "database/sql"
"encoding/binary" "encoding/binary"
"fmt"
"net/http" "net/http"
"time" "time"
"weatherstation/internal/database" "weatherstation/internal/database"
@ -286,6 +287,75 @@ func nearestRainTileHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp) 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 { func decodeRain(buf []byte, w, h int) [][]*float64 {
vals := make([][]*float64, h) vals := make([][]*float64, h)
off := 0 off := 0

View File

@ -269,11 +269,22 @@ const WeatherMap = {
try{ try{
const z=this.tileZ,y=this.tileY,x=this.tileX; const z=this.tileZ,y=this.tileY,x=this.tileX;
if (this.tileProduct === 'rain') { 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); const r = await fetch(url);
if(!r.ok){ console.warn('雨量瓦片未找到', dtStr); return; } if(!r.ok){
const t = await r.json(); // 回退单块接口
await this.renderTileOnMap('rain', t); 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 { } else {
// radar: 取同一时次该 z 下的所有 y/x 瓦片 // radar: 取同一时次该 z 下的所有 y/x 瓦片
const url = `/api/radar/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`; const url = `/api/radar/tiles_at?z=${z}&dt=${encodeURIComponent(dtStr)}`;
@ -326,7 +337,8 @@ const WeatherMap = {
if (product==='rain'){ if (product==='rain'){
const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity]; const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity];
const colors = [ 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] [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)=>{ colorFunc = (mm)=>{
@ -382,13 +394,28 @@ const WeatherMap = {
canvas.width = w; canvas.height = h; canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(w, h); 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]]; let colorFunc;
const colorFunc = (dbz)=>{ if (product==='rain'){
let v = Math.max(0, Math.min(75, dbz)); const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity];
if (v===0) return [0,0,0,0]; const colors = [
let bin = Math.floor(v/5); if (bin>=colors.length) bin=colors.length-1; // 0值透明(0,5) 用绿色,不再用白色
const c = colors[bin]; return [c[0],c[1],c[2],220]; [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.length-1 && !(mm>=edges[idx] && mm<edges[idx+1])) idx++;
const c = colors[Math.min(idx, colors.length-1)]; return [c[0],c[1],c[2],220];
};
} else {
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]];
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];
};
}
for(let row=0; row<h; row++){ for(let row=0; row<h; row++){
const srcRow = t.values[row] || []; const srcRow = t.values[row] || [];
const dstRow = (h-1-row); const dstRow = (h-1-row);

View File

@ -526,7 +526,7 @@
<label class="text-sm text-gray-600">叠加显示:</label> <label class="text-sm text-gray-600">叠加显示:</label>
<select id="tileProduct" class="px-2 py-1 border border-gray-300 rounded text-sm"> <select id="tileProduct" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="none" selected>不显示</option> <option value="none" selected>不显示</option>
<option value="rain">上小时降雨</option> <option value="rain">1h 实际降雨</option>
<option value="radar">水汽含量</option> <option value="radar">水汽含量</option>
</select> </select>
</div> </div>