diff --git a/internal/server/gin.go b/internal/server/gin.go index 0c2e01d..5759140 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -50,6 +50,7 @@ func StartGinServer() error { api.GET("/radar/weather_at", radarWeatherAtHandler) api.GET("/radar/weather_aliases", radarWeatherAliasesHandler) api.GET("/radar/aliases", radarConfigAliasesHandler) + api.GET("/radar/weather_nearest", radarWeatherNearestHandler) } // 获取配置的Web端口 diff --git a/internal/server/radar_api.go b/internal/server/radar_api.go index 9bdf09d..65bd048 100644 --- a/internal/server/radar_api.go +++ b/internal/server/radar_api.go @@ -520,6 +520,86 @@ func radarWeatherAtHandler(c *gin.Context) { c.JSON(http.StatusOK, resp) } +// radarWeatherNearestHandler returns the nearest radar_weather record to the given dt. +// prefer=lte will pick the latest record not later than dt; else chooses absolute nearest. +func radarWeatherNearestHandler(c *gin.Context) { + alias := c.Query("alias") + if alias == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 alias 参数"}) + return + } + dtStr := c.Query("dt") + if dtStr == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 dt 参数(YYYY-MM-DD HH:MM:SS)"}) + return + } + prefer := c.DefaultQuery("prefer", "lte") // lte|nearest + loc, _ := time.LoadLocation("Asia/Shanghai") + if loc == nil { + loc = time.FixedZone("CST", 8*3600) + } + target, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 dt 格式"}) + return + } + + var row *sql.Row + db := database.GetDB() + if prefer == "nearest" { + 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 ABS(EXTRACT(EPOCH FROM (dt - $2))) ASC + LIMIT 1` + row = db.QueryRow(q, alias, target) + } else { // lte default + 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 + ORDER BY dt DESC + LIMIT 1` + row = db.QueryRow(q, alias, target) + } + + var r radarWeatherRecord + if err := row.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, + ); err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "未找到就近的实时气象数据"}) + return + } + ageMin := int(target.Sub(r.DT).Minutes()) + if ageMin < 0 { // for nearest mode, could be future relative to target + ageMin = -ageMin + } + resp := gin.H{ + "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, + "wind_speed": r.WindSpeed, + "wind_direction": r.WindDirection, + "pressure": r.Pressure, + "age_minutes": ageMin, + "stale": ageMin > 120, + } + c.JSON(http.StatusOK, resp) +} + // radarWeatherAliasesHandler 返回 radar_weather 中存在的站点别名及经纬度(按最近记录去重) func radarWeatherAliasesHandler(c *gin.Context) { const q = ` diff --git a/templates/imdroid_radar.html b/templates/imdroid_radar.html index 99d898f..aaccd17 100644 --- a/templates/imdroid_radar.html +++ b/templates/imdroid_radar.html @@ -113,11 +113,16 @@ setRealtimeBox(j); } - async function loadRealtimeAt(alias, dtStr) { - const res = await fetch(`/api/radar/weather_at?alias=${encodeURIComponent(alias)}&dt=${encodeURIComponent(dtStr)}`); - if (!res.ok) { return false; } + async function loadRealtimeNearest(alias, dtStr) { + const res = await fetch(`/api/radar/weather_nearest?prefer=lte&alias=${encodeURIComponent(alias)}&dt=${encodeURIComponent(dtStr)}`); + if (!res.ok) return false; const j = await res.json(); setRealtimeBox(j); + const bar = document.getElementById('rt_stale'); + if (bar) { + if (j.stale) { bar.classList.remove('hidden'); } + else { bar.classList.add('hidden'); } + } return true; } @@ -152,13 +157,8 @@ const sel = document.getElementById('timeSelect'); sel.value = t.dt; } - // 同步气象:按瓦片时间向下取整到10分钟,查询该桶的实况 - if (gAlias) { - const bucket = dtToBucket10(t.dt); - if (bucket) { - await loadRealtimeAt(gAlias, bucket); - } - } + // 同步气象:就近(优先<=)匹配该瓦片时间 + if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} } maybeCalcSector(); maybePlotSquare(); maybeCalcNearbyRain(); @@ -188,10 +188,7 @@ document.getElementById('tile_res').textContent = fmt(t.res_deg, 6); status.textContent = ''; renderTilePlot(t); - if (gAlias) { - const bucket = dtToBucket10(t.dt); - if (bucket) { await loadRealtimeAt(gAlias, bucket); } - } + if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} } maybeCalcSector(); maybePlotSquare(); maybeCalcNearbyRain(); @@ -557,16 +554,17 @@ document.getElementById('btnLoad').addEventListener('click', async ()=>{ const sel = document.getElementById('stationSelect'); if (!sel || !sel.value) return; - const alias = sel.options[sel.selectedIndex].textContent || sel.value; + const aliasText = sel.options[sel.selectedIndex].textContent || sel.value; + const aliasParam = sel.value; gZ = Number(sel.options[sel.selectedIndex].dataset.z || 0); gY = Number(sel.options[sel.selectedIndex].dataset.y || 0); gX = Number(sel.options[sel.selectedIndex].dataset.x || 0); const lat = Number(sel.options[sel.selectedIndex].dataset.lat); const lon = Number(sel.options[sel.selectedIndex].dataset.lon); gStLat = isNaN(lat)? null : lat; gStLon = isNaN(lon)? null : lon; - gAlias = alias; + gAlias = aliasParam; document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`; - try { await loadRealtime(alias); } catch (e) { console.warn(e); } + try { await loadRealtime(aliasParam); } catch (e) { console.warn(e); } if (gZ && gY && gX) { const from = fromDTLocalInput(document.getElementById('tsStart').value); const to = fromDTLocalInput(document.getElementById('tsEnd').value); @@ -678,7 +676,7 @@
下行短波: W/m²
气压: Pa
- +