feat: 雷达历史数据
This commit is contained in:
parent
e6f9d500ea
commit
12b2ad5ace
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,6 +18,22 @@
|
||||
{{ template "header" . }}
|
||||
|
||||
<div class="content-narrow p-4">
|
||||
<div class="card mb-4">
|
||||
<div class="text-lg font-semibold mb-2">历史时次查询</div>
|
||||
<div class="w-full flex flex-col items-start gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<input id="tsStart" type="datetime-local" class="border rounded px-2 py-1"/>
|
||||
<span>至</span>
|
||||
<input id="tsEnd" type="datetime-local" class="border rounded px-2 py-1"/>
|
||||
<button id="tsQuery" class="px-3 py-1 bg-blue-600 text-white rounded">查询</button>
|
||||
</div>
|
||||
<div>
|
||||
<select id="timeSelect" class="border rounded px-2 py-1 min-w-[260px]">
|
||||
<option value="">最新</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-4">
|
||||
<div class="text-lg font-semibold mb-2">最新 7/40/104 瓦片信息</div>
|
||||
<div class="text-sm space-y-1">
|
||||
@ -57,7 +73,16 @@
|
||||
</div>
|
||||
|
||||
<div class="card" style="width:100%;">
|
||||
<div class="text-lg font-semibold mb-2">雷达组合反射率</div>
|
||||
<div class="text-lg font-semibold mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
雷达组合反射率 <span id="titleDt" class="text-gray-500 text-sm"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<button id="btnPrev" class="px-2 py-1 border rounded">上一时次</button>
|
||||
<span id="countInfo" class="text-gray-600">共0条,第0条</span>
|
||||
<button id="btnNext" class="px-2 py-1 border rounded">下一时次</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="radarPlot" class="plot-box"></div>
|
||||
</div>
|
||||
|
||||
@ -74,6 +99,8 @@
|
||||
|
||||
let gTileValues = null, gXs = null, gYs = null;
|
||||
let gWindFromDeg = null, gWindSpeedMS = null;
|
||||
let gTimes = [];
|
||||
let gCurrentIdx = -1; // 在 gTimes 中的索引(倒序:0=最新)
|
||||
|
||||
function toRad(d){ return d * Math.PI / 180; }
|
||||
function toDeg(r){ return r * 180 / Math.PI; }
|
||||
@ -100,6 +127,32 @@
|
||||
const res = await fetch(`/api/radar/latest?z=${TILE_Z}&y=${TILE_Y}&x=${TILE_X}`);
|
||||
if(!res.ok) throw new Error('加载最新瓦片失败');
|
||||
const t = await res.json();
|
||||
await renderTile(t);
|
||||
}
|
||||
|
||||
async function loadTileAt(dtStr){
|
||||
const res = await fetch(`/api/radar/at?z=${TILE_Z}&y=${TILE_Y}&x=${TILE_X}&dt=${encodeURIComponent(dtStr)}`);
|
||||
if(!res.ok) throw new Error('加载指定时间瓦片失败');
|
||||
const t = await res.json();
|
||||
await renderTile(t, dtStr);
|
||||
}
|
||||
|
||||
function fmtDTLocal(dt){ const pad=(n)=>String(n).padStart(2,'0'); return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`; }
|
||||
function fromDTLocalInput(s){ if(!s) return null; const t=new Date(s.replace('T','-').replace(/-/g,'/')); const pad=(n)=>String(n).padStart(2,'0'); return `${t.getFullYear()}-${pad(t.getMonth()+1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:00`; }
|
||||
async function populateTimes(fromStr, toStr){
|
||||
try{
|
||||
let url = `/api/radar/times?z=${TILE_Z}&y=${TILE_Y}&x=${TILE_X}`;
|
||||
if(fromStr && toStr){ url += `&from=${encodeURIComponent(fromStr)}&to=${encodeURIComponent(toStr)}`; } else { url += `&limit=60`; }
|
||||
const res = await fetch(url); if(!res.ok) return; const j = await res.json();
|
||||
const sel = document.getElementById('timeSelect'); while (sel.options.length > 1) sel.remove(1);
|
||||
gTimes = j.times || [];
|
||||
gTimes.forEach(dt=>{ const opt=document.createElement('option'); opt.value=dt; opt.textContent=dt; sel.appendChild(opt); });
|
||||
if (gTimes.length>0 && gCurrentIdx<0){ sel.value=gTimes[0]; gCurrentIdx=0; }
|
||||
updateCountAndButtons();
|
||||
}catch{}
|
||||
}
|
||||
|
||||
async function renderTile(t, forcedDt){
|
||||
const fmt5 = (n)=>Number(n).toFixed(5);
|
||||
|
||||
document.getElementById('dt').textContent = t.dt;
|
||||
@ -112,6 +165,12 @@
|
||||
document.getElementById('east').textContent = fmt5(t.east);
|
||||
document.getElementById('north').textContent = fmt5(t.north);
|
||||
document.getElementById('res').textContent = fmt5(t.res_deg);
|
||||
// 标题时间与索引同步
|
||||
document.getElementById('titleDt').textContent = `(${t.dt})`;
|
||||
const selBox = document.getElementById('timeSelect');
|
||||
for(let i=0;i<selBox.options.length;i++){ if(selBox.options[i].value===t.dt){ selBox.selectedIndex=i; break; } }
|
||||
gCurrentIdx = gTimes.indexOf(t.dt);
|
||||
updateCountAndButtons();
|
||||
|
||||
// x/y 等角坐标
|
||||
const w=t.width,h=t.height; gTileValues=t.values;
|
||||
@ -155,7 +214,8 @@
|
||||
|
||||
// 与瓦片时间对齐的10分钟气象
|
||||
try{
|
||||
const dt=new Date(t.dt.replace(/-/g,'/')); const ceil10=new Date(dt); const m=dt.getMinutes(); const up=(Math.floor(m/10)*10+10)%60; ceil10.setMinutes(up,0,0); if(up===0){ ceil10.setHours(dt.getHours()+1); }
|
||||
const base = forcedDt ? new Date(forcedDt.replace(/-/g,'/')) : new Date(t.dt.replace(/-/g,'/'));
|
||||
const ceil10=new Date(base); const m=base.getMinutes(); const up=(Math.floor(m/10)*10+10)%60; ceil10.setMinutes(up,0,0); if(up===0){ ceil10.setHours(base.getHours()+1); }
|
||||
const pad=(n)=>String(n).padStart(2,'0'); const dtStr=`${ceil10.getFullYear()}-${pad(ceil10.getMonth()+1)}-${pad(ceil10.getDate())} ${pad(ceil10.getHours())}:${pad(ceil10.getMinutes())}:00`;
|
||||
await loadRealtimeAt(dtStr);
|
||||
maybeCalcSector();
|
||||
@ -259,9 +319,31 @@
|
||||
}catch(e){ document.getElementById('squarePlot').innerHTML=`<div class="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">正方形热力图渲染失败:${e.message}</div>`; }
|
||||
}
|
||||
|
||||
// 启动加载
|
||||
loadLatestTile().catch(err=>{ document.getElementById('radarPlot').innerHTML=`<div class="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">加载失败:${err.message}</div>`; });
|
||||
// 启动加载:预填近3小时范围并填充时次
|
||||
(function initRange(){ const end=new Date(); const start=new Date(end.getTime()-3*3600*1000); document.getElementById('tsStart').value=fmtDTLocal(start); document.getElementById('tsEnd').value=fmtDTLocal(end); })();
|
||||
const startStr = fromDTLocalInput(document.getElementById('tsStart').value);
|
||||
const endStr = fromDTLocalInput(document.getElementById('tsEnd').value);
|
||||
loadLatestTile().then(()=>populateTimes(startStr, endStr)).catch(err=>{ document.getElementById('radarPlot').innerHTML=`<div class="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">加载失败:${err.message}</div>`; });
|
||||
loadRealtimeLatest().catch(err=>{ document.getElementById('rtInfo').innerHTML=`<div class="text-sm text-red-600">${err.message}</div>`; });
|
||||
document.getElementById('timeSelect').addEventListener('change', async (e)=>{
|
||||
const v=e.target.value;
|
||||
if(!v){ if(gTimes.length>0){ gCurrentIdx=0; await loadTileAt(gTimes[0]); } else { gCurrentIdx=-1; await loadLatestTile(); } }
|
||||
else { gCurrentIdx=gTimes.indexOf(v); await loadTileAt(v); }
|
||||
updateCountAndButtons();
|
||||
});
|
||||
document.getElementById('tsQuery').addEventListener('click', async ()=>{ const s=fromDTLocalInput(document.getElementById('tsStart').value); const e=fromDTLocalInput(document.getElementById('tsEnd').value); await populateTimes(s,e); });
|
||||
function updateCountAndButtons(){
|
||||
const N=gTimes.length; const k=gCurrentIdx>=0?(gCurrentIdx+1):0; document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时刻`;
|
||||
const prev=document.getElementById('btnPrev'); const next=document.getElementById('btnNext');
|
||||
prev.disabled = !(N>0 && gCurrentIdx>=0 && gCurrentIdx<N-1);
|
||||
next.disabled = !(N>0 && gCurrentIdx>0);
|
||||
}
|
||||
document.getElementById('btnPrev').addEventListener('click', async ()=>{
|
||||
if(gTimes.length===0) return; if(gCurrentIdx<0) gCurrentIdx=0; if(gCurrentIdx<gTimes.length-1){ gCurrentIdx++; const dt=gTimes[gCurrentIdx]; document.getElementById('timeSelect').value=dt; await loadTileAt(dt);} updateCountAndButtons();
|
||||
});
|
||||
document.getElementById('btnNext').addEventListener('click', async ()=>{
|
||||
if(gTimes.length===0) return; if(gCurrentIdx>0){ gCurrentIdx--; const dt=gTimes[gCurrentIdx]; document.getElementById('timeSelect').value=dt; await loadTileAt(dt);} updateCountAndButtons();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
@ -23,6 +23,23 @@
|
||||
{{ template "header" . }}
|
||||
|
||||
<div class="content-narrow p-4">
|
||||
<div class="card mb-4">
|
||||
<div class="text-lg font-semibold mb-2">历史时次查询</div>
|
||||
<div class="w-full flex flex-col items-start gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<input id="tsStart" type="datetime-local" class="border rounded px-2 py-1"/>
|
||||
<span>至</span>
|
||||
<input id="tsEnd" type="datetime-local" class="border rounded px-2 py-1"/>
|
||||
<button id="tsQuery" class="px-3 py-1 bg-blue-600 text-white rounded">查询</button>
|
||||
</div>
|
||||
<div>
|
||||
<select id="timeSelect" class="border rounded px-2 py-1 min-w-[260px]">
|
||||
<option value="">最新</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="text-lg font-semibold mb-2">最新 7/40/102 瓦片信息</div>
|
||||
<div id="tileInfo" class="text-sm space-y-1">
|
||||
@ -60,7 +77,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="width: 100%;">
|
||||
<div class="text-lg font-semibold mb-2">雷达组合反射率</div>
|
||||
<div class="text-lg font-semibold mb-2 flex items-center justify-between">
|
||||
<div>
|
||||
雷达组合反射率 <span id="titleDt" class="text-gray-500 text-sm"></span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<button id="btnPrev" class="px-2 py-1 border rounded">上一时次</button>
|
||||
<span id="countInfo" class="text-gray-600">共0条,第0条</span>
|
||||
<button id="btnNext" class="px-2 py-1 border rounded">下一时次</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="radarPlot" class="plot-box"></div>
|
||||
</div>
|
||||
<div class="card mt-4" style="width: 100%;">
|
||||
@ -72,11 +98,63 @@
|
||||
<script>
|
||||
let gTileValues = null, gXs = null, gYs = null;
|
||||
let gWindFromDeg = null, gWindSpeedMS = null; // 来风方向(度,0=北,顺时针),风速 m/s
|
||||
let gTimes = [];
|
||||
let gCurrentIdx = -1; // 在 gTimes 中的索引(倒序:0=最新)
|
||||
|
||||
async function loadLatestTile() {
|
||||
const res = await fetch('/api/radar/latest?z=7&y=40&x=102');
|
||||
if (!res.ok) { throw new Error('加载最新瓦片失败'); }
|
||||
const t = await res.json();
|
||||
await renderTile(t);
|
||||
}
|
||||
|
||||
async function loadTileAt(dtStr) {
|
||||
const url = `/api/radar/at?z=7&y=40&x=102&dt=${encodeURIComponent(dtStr)}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) { throw new Error('加载指定时间瓦片失败'); }
|
||||
const t = await res.json();
|
||||
await renderTile(t, dtStr);
|
||||
}
|
||||
|
||||
function fmtDTLocal(dt){
|
||||
const pad = (n)=> String(n).padStart(2,'0');
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
|
||||
}
|
||||
function fromDTLocalInput(s){
|
||||
// s like 'YYYY-MM-DDTHH:MM'
|
||||
if(!s) return null;
|
||||
const t = new Date(s.replace('T','-').replace(/-/g,'/')); // iOS safe parse later
|
||||
// We will manually format to server expected 'YYYY-MM-DD HH:MM:SS'
|
||||
const pad=(n)=> String(n).padStart(2,'0');
|
||||
return `${t.getFullYear()}-${pad(t.getMonth()+1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}:00`;
|
||||
}
|
||||
async function populateTimes(fromStr, toStr) {
|
||||
try {
|
||||
let url = '/api/radar/times?z=7&y=40&x=102';
|
||||
if (fromStr && toStr) {
|
||||
url += `&from=${encodeURIComponent(fromStr)}&to=${encodeURIComponent(toStr)}`;
|
||||
} else {
|
||||
url += '&limit=60';
|
||||
}
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return;
|
||||
const j = await res.json();
|
||||
const sel = document.getElementById('timeSelect');
|
||||
while (sel.options.length > 1) sel.remove(1);
|
||||
gTimes = j.times || [];
|
||||
gTimes.forEach(dt => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = dt; opt.textContent = dt; sel.appendChild(opt);
|
||||
});
|
||||
if (gTimes.length > 0 && gCurrentIdx < 0) {
|
||||
sel.value = gTimes[0];
|
||||
gCurrentIdx = 0;
|
||||
}
|
||||
updateCountAndButtons();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function renderTile(t, forcedDt) {
|
||||
const fmt5 = (n) => Number(n).toFixed(5);
|
||||
|
||||
// 填充元信息
|
||||
@ -90,6 +168,14 @@
|
||||
document.getElementById('east').textContent = fmt5(t.east);
|
||||
document.getElementById('north').textContent = fmt5(t.north);
|
||||
document.getElementById('res').textContent = fmt5(t.res_deg);
|
||||
// 标题时间与索引同步
|
||||
document.getElementById('titleDt').textContent = `(${t.dt})`;
|
||||
const selBox = document.getElementById('timeSelect');
|
||||
for (let i = 0; i < selBox.options.length; i++) {
|
||||
if (selBox.options[i].value === t.dt) { selBox.selectedIndex = i; break; }
|
||||
}
|
||||
gCurrentIdx = gTimes.indexOf(t.dt);
|
||||
updateCountAndButtons();
|
||||
|
||||
// Plotly 热力图(离散 5 dBZ 色带)
|
||||
const colors = [
|
||||
@ -208,7 +294,7 @@
|
||||
|
||||
// 同步加载与瓦片时间对应的10分钟气象(向上取整到10分)
|
||||
try {
|
||||
const dt = new Date(t.dt.replace(/-/g,'/')); // iOS兼容
|
||||
const dt = forcedDt ? new Date(forcedDt.replace(/-/g,'/')) : new Date(t.dt.replace(/-/g,'/'));
|
||||
const ceil10 = new Date(dt);
|
||||
const m = dt.getMinutes();
|
||||
const up = (Math.floor(m/10)*10 + 10) % 60;
|
||||
@ -485,10 +571,67 @@
|
||||
}
|
||||
}
|
||||
|
||||
loadLatestTile().catch(err => {
|
||||
// 初始化:加载最新瓦片、填充时间下拉(默认近3小时)、绑定事件
|
||||
// 预填范围
|
||||
(function initRange(){
|
||||
const end = new Date();
|
||||
const start = new Date(end.getTime() - 3*3600*1000);
|
||||
document.getElementById('tsStart').value = fmtDTLocal(start);
|
||||
document.getElementById('tsEnd').value = fmtDTLocal(end);
|
||||
})();
|
||||
const startStr = fromDTLocalInput(document.getElementById('tsStart').value);
|
||||
const endStr = fromDTLocalInput(document.getElementById('tsEnd').value);
|
||||
loadLatestTile().then(()=>populateTimes(startStr, endStr)).catch(err => {
|
||||
const plot = document.getElementById('radarPlot');
|
||||
plot.innerHTML = `<div class="p-3 text-sm text-red-600 bg-red-50 border border-red-200 rounded">加载失败:${err.message}(请确认 /static/js/plotly-2.27.0.min.js 已存在)</div>`;
|
||||
});
|
||||
document.getElementById('timeSelect').addEventListener('change', async (e) => {
|
||||
const v = e.target.value;
|
||||
if (!v) {
|
||||
if (gTimes.length > 0) { gCurrentIdx = 0; await loadTileAt(gTimes[0]); }
|
||||
else { gCurrentIdx = -1; await loadLatestTile(); }
|
||||
} else {
|
||||
gCurrentIdx = gTimes.indexOf(v);
|
||||
await loadTileAt(v);
|
||||
}
|
||||
updateCountAndButtons();
|
||||
});
|
||||
document.getElementById('tsQuery').addEventListener('click', async ()=>{
|
||||
const s = fromDTLocalInput(document.getElementById('tsStart').value);
|
||||
const e = fromDTLocalInput(document.getElementById('tsEnd').value);
|
||||
await populateTimes(s, e);
|
||||
});
|
||||
|
||||
function updateCountAndButtons(){
|
||||
const N = gTimes.length;
|
||||
const k = gCurrentIdx >= 0 ? (gCurrentIdx + 1) : 0;
|
||||
document.getElementById('countInfo').textContent=`共${N}条数据,-${k-1}时刻`;
|
||||
const prev = document.getElementById('btnPrev');
|
||||
const next = document.getElementById('btnNext');
|
||||
prev.disabled = !(N > 0 && gCurrentIdx >= 0 && gCurrentIdx < N-1);
|
||||
next.disabled = !(N > 0 && gCurrentIdx > 0);
|
||||
}
|
||||
document.getElementById('btnPrev').addEventListener('click', async ()=>{
|
||||
if (gTimes.length === 0) return;
|
||||
if (gCurrentIdx < 0) gCurrentIdx = 0;
|
||||
if (gCurrentIdx < gTimes.length - 1) {
|
||||
gCurrentIdx++;
|
||||
const dt = gTimes[gCurrentIdx];
|
||||
document.getElementById('timeSelect').value = dt;
|
||||
await loadTileAt(dt);
|
||||
}
|
||||
updateCountAndButtons();
|
||||
});
|
||||
document.getElementById('btnNext').addEventListener('click', async ()=>{
|
||||
if (gTimes.length === 0) return;
|
||||
if (gCurrentIdx > 0) {
|
||||
gCurrentIdx--;
|
||||
const dt = gTimes[gCurrentIdx];
|
||||
document.getElementById('timeSelect').value = dt;
|
||||
await loadTileAt(dt);
|
||||
}
|
||||
updateCountAndButtons();
|
||||
});
|
||||
// 兜底加载最新
|
||||
loadRealtimeLatest().catch(err => {
|
||||
const info = document.getElementById('rtInfo');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user