<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{{ .Title }}</title>
  <link rel="stylesheet" href="/static/css/tailwind.min.css">
  <style>
    body { font-family: Arial, sans-serif; margin: 0; }
    .content-narrow { width: 86%; max-width: 1200px; margin: 0 auto; }
    .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 6px; padding: 20px; }
    .plot-box { width: clamp(320px, 72vw, 680px); margin: 0 auto; aspect-ratio: 1 / 1; overflow: hidden; }
    .plot-box-sm { width: clamp(320px, 65vw, 612px); margin: 0 auto; aspect-ratio: 1 / 1; overflow: hidden; }
    @supports not (aspect-ratio: 1 / 1) { .plot-box { height: 520px; } }
  </style>
  <script src="/static/js/plotly-2.27.0.min.js"></script>
  <script>
    let gZ=0, gY=0, gX=0;
    let gTimes = [];
    let gCurrentIdx = -1;
    let gAlias = '';
    // 雨量瓦片（小时累计）当前展示的时间，便于状态同步
    let gRainDT = '';
    // 3H 预报相关全局量
    let gTileValues = null, gXs = null, gYs = null;
    let gWindFromDeg = null, gWindSpeedMS = null;
    let gTileDT = null;
    let gTileDTStr = '';
    let gStLat = null, gStLon = null;

    async function loadStations() {
      const sel = document.getElementById('stationSelect');
      sel.innerHTML = '';
      const opt = document.createElement('option');
      opt.value = ''; opt.textContent = '请选择站点…';
      sel.appendChild(opt);
      try {
        // 1) 实际设备站点
        const res = await fetch('/api/stations');
        const stations = await res.json();
        stations
          .filter(s => s.device_type === 'WH65LP' && s.latitude && s.longitude)
          .forEach(s => {
            const o = document.createElement('option');
            o.value = s.station_id; // 用 station_id 作为联动主键
            const alias = (s.station_alias && String(s.station_alias).trim().length > 0) ? s.station_alias : s.station_id;
            o.textContent = alias; // 仅显示别名
            o.dataset.z = s.z; o.dataset.y = s.y; o.dataset.x = s.x;
            o.dataset.lat = s.latitude; o.dataset.lon = s.longitude;
            o.dataset.kind = 'station';
            sel.appendChild(o);
          });
      } catch {}
      try {
        // 2) 从配置读取别名（如 海珠/番禺），追加到同一下拉
        const res2 = await fetch('/api/radar/aliases');
        if (res2.ok) {
          const j = await res2.json();
          (j.aliases || []).forEach(a => {
            const o = document.createElement('option');
            o.value = a.alias;
            o.textContent = a.alias;
            o.dataset.z = a.z; o.dataset.y = a.y; o.dataset.x = a.x;
            o.dataset.lat = a.lat; o.dataset.lon = a.lon;
            o.dataset.kind = 'alias';
            sel.appendChild(o);
          });
        }
      } catch {}
    }

    async function loadAliases() {
      const sel = document.getElementById('aliasSelect');
      if (!sel) return;
      sel.innerHTML = '';
      const opt = document.createElement('option');
      opt.value = ''; opt.textContent = '或选择雷达别名（海珠/番禺）…';
      sel.appendChild(opt);
      try {
        const res = await fetch('/api/radar/weather_aliases');
        if (!res.ok) return;
        const j = await res.json();
        (j.aliases || []).forEach(a => {
          const o = document.createElement('option');
          o.value = a.alias;
          o.textContent = a.alias;
          o.dataset.lat = a.lat;
          o.dataset.lon = a.lon;
          sel.appendChild(o);
        });
      } catch {}
    }

    function setRealtimeBox(j){
      const set = (id, v) => document.getElementById(id).textContent = (v ?? '') === '' ? '' : String(v);
      set('rt_alias', j.alias);
      set('rt_dt', j.dt);
      set('rt_lat', j.lat);
      set('rt_lon', j.lon);
      if (typeof j.temperature === 'number') set('rt_t', j.temperature.toFixed(2)); else set('rt_t','');
      if (typeof j.humidity === 'number') set('rt_h', j.humidity.toFixed(0)*100); else set('rt_h','');
      if (typeof j.wind_speed === 'number') set('rt_ws', j.wind_speed.toFixed(2)); else set('rt_ws','');
      if (typeof j.wind_direction === 'number') set('rt_wd', j.wind_direction.toFixed(0)); else set('rt_wd','');
      if (typeof j.cloudrate === 'number') set('rt_c', j.cloudrate.toFixed(2)*100); else set('rt_c','');
      if (typeof j.visibility === 'number') set('rt_vis', j.visibility.toFixed(2)); else set('rt_vis','');
      if (typeof j.dswrf === 'number') set('rt_dswrf', j.dswrf.toFixed(1)); else set('rt_dswrf','');
      if (typeof j.pressure === 'number') set('rt_p', j.pressure.toFixed(0)); else set('rt_p','');
      if (typeof j.wind_direction === 'number') gWindFromDeg = Number(j.wind_direction);
      if (typeof j.wind_speed === 'number') gWindSpeedMS = Number(j.wind_speed);
    }

    async function loadRealtime(alias) {
      const res = await fetch(`/api/radar/weather_latest?alias=${encodeURIComponent(alias)}`);
      if (!res.ok) throw new Error('实时数据不存在');
      const j = await res.json();
      setRealtimeBox(j);
    }

    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;
    }

    async function loadLatestTile(z, y, x) {
      const status = document.getElementById('tile_status');
      const res = await fetch(`/api/radar/latest?z=${z}&y=${y}&x=${x}`);
      if (!res.ok) {
        status.textContent = '未找到瓦片';
        // 当没有瓦片可用时，清空附近降雨提示，避免遗留旧提示
        const nb = document.getElementById('nearbyStatus');
        if (nb) { nb.textContent=''; nb.classList.remove('text-red-700'); nb.classList.add('text-gray-700'); }
        return;
      }
      const t = await res.json();
      const fmt = (n, d=5)=> Number(n).toFixed(d);
      document.getElementById('tile_dt').textContent = t.dt;
      document.getElementById('tile_z').textContent = t.z;
      document.getElementById('tile_y').textContent = t.y;
      document.getElementById('tile_x').textContent = t.x;
      document.getElementById('tile_w').textContent = fmt(t.west);
      document.getElementById('tile_s').textContent = fmt(t.south);
      document.getElementById('tile_e').textContent = fmt(t.east);
      document.getElementById('tile_n').textContent = fmt(t.north);
      document.getElementById('tile_res').textContent = fmt(t.res_deg, 6);
      status.textContent = '';
      renderTilePlot(t);
      // 同步：按北京时间整点对齐加载小时雨量（优先从列表选择 ≤ 该整点）
      try { await loadRainAligned(z,y,x, t.dt); } catch(e) { /* ignore */ }
      const idx = gTimes.indexOf(t.dt);
      if (idx >= 0) {
        gCurrentIdx = idx;
        updateCountAndButtons();
        updateSlider();
        const sel = document.getElementById('timeSelect');
        sel.value = t.dt;
      }
      // 同步气象：就近（优先<=）匹配该瓦片时间
      if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} }
      maybeCalcSector();
      maybePlotSquare();
      maybeCalcNearbyRain();
      maybeCalcTileRegionStats();
    }

    async function loadTileAt(z, y, x, dtStr) {
      const status = document.getElementById('tile_status');
      const res = await fetch(`/api/radar/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`);
      if (!res.ok) {
        status.textContent = '未找到瓦片';
        // 清空附近降雨提示，避免遗留
        const nb = document.getElementById('nearbyStatus');
        if (nb) { nb.textContent=''; nb.classList.remove('text-red-700'); nb.classList.add('text-gray-700'); }
        return;
      }
      const t = await res.json();
      const fmt = (n, d=5)=> Number(n).toFixed(d);
      document.getElementById('tile_dt').textContent = t.dt;
      document.getElementById('tile_z').textContent = t.z;
      document.getElementById('tile_y').textContent = t.y;
      document.getElementById('tile_x').textContent = t.x;
      document.getElementById('tile_w').textContent = fmt(t.west);
      document.getElementById('tile_s').textContent = fmt(t.south);
      document.getElementById('tile_e').textContent = fmt(t.east);
      document.getElementById('tile_n').textContent = fmt(t.north);
      document.getElementById('tile_res').textContent = fmt(t.res_deg, 6);
      status.textContent = '';
      renderTilePlot(t);
      // 同步：按北京时间整点对齐加载小时雨量（优先从列表选择 ≤ 该整点）
      try { await loadRainAligned(z,y,x, t.dt); } catch(e) { /* ignore */ }
      if (gAlias) { try { await loadRealtimeNearest(gAlias, t.dt); } catch(e){} }
      maybeCalcSector();
      maybePlotSquare();
      maybeCalcNearbyRain();
      maybeCalcTileRegionStats();
    }

    // ---- 小时雨量（CMPA）渲染 ----
    async function loadLatestRainTile(z, y, x){
      const status = document.getElementById('rain_tile_status');
      const res = await fetch(`/api/rain/latest?z=${z}&y=${y}&x=${x}`);
      if(!res.ok){ status.textContent='未找到雨量瓦片'; return; }
      const t = await res.json();
      fillRainMetaAndPlot(t);
    }

    function floorHourStr(dtStr){
      if(!dtStr) return '';
      const m = dtStr.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}):\d{2}:\d{2}$/);
      if(!m) return dtStr;
      return `${m[1]} ${m[2]}:00:00`;
    }


    async function loadNearestRainTile(z, y, x, dtStr){
      const status = document.getElementById('rain_tile_status');
      if(!dtStr){ return loadLatestRainTile(z,y,x); }
      // 将查询时间按北京时间整点对齐，提高匹配准确性
      const base = floorHourStr(dtStr);
      let url = `/api/rain/nearest?prefer=lte&tolerance_min=120&z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(base)}`;
      let res = await fetch(url);
      if(!res.ok){
        // 二次尝试：放宽容差到 24 小时
        url = `/api/rain/nearest?prefer=lte&tolerance_min=1440&z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(base)}`;
        res = await fetch(url);
      }
      if(!res.ok){
        // 优雅降级：若有时间列表，下拉默认第一项
        const sel = document.getElementById('rainTimeSelect');
        if (sel && sel.options.length > 1) {
          const first = sel.options[1].value;
          sel.value = first;
          await loadRainAt(z,y,x, first);
          return;
        }
        status.textContent='未找到匹配的雨量瓦片';
        return;
      }
      const t = await res.json();
      fillRainMetaAndPlot(t);
    }

    function compareDTStr(a, b){
      // a,b 格式 "YYYY-MM-DD HH:MM:SS"，直接字符串比较即可
      return a === b ? 0 : (a < b ? -1 : 1);
    }

    async function loadRainAligned(z, y, x, dtStr){
      const sel = document.getElementById('rainTimeSelect');
      const status = document.getElementById('rain_tile_status');
      if(!dtStr){ return loadLatestRainTile(z,y,x); }
      const base = floorHourStr(dtStr);
      // 优先从下拉（倒序）中选择第一个 <= base 的时次
      if (sel && sel.options.length > 1){
        for (let i=1; i<sel.options.length; i++){
          const v = sel.options[i].value;
          if (compareDTStr(v, base) <= 0){ sel.value = v; await loadRainAt(z,y,x, v); return; }
        }
        // 未命中则继续兜底
      }
      // 兜底：尝试 exact at(base)，失败再 lte 24h，再 latest
      let res = await fetch(`/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(base)}`);
      if (res.ok){ const t = await res.json(); fillRainMetaAndPlot(t); return; }
      res = await fetch(`/api/rain/nearest?prefer=lte&tolerance_min=1440&z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(base)}`);
      if (res.ok){ const t = await res.json(); fillRainMetaAndPlot(t); return; }
      if (sel && sel.options.length > 1){ const first = sel.options[1].value; sel.value = first; await loadRainAt(z,y,x, first); return; }
      status.textContent='未找到匹配的雨量瓦片';
    }

    function fillRainMetaAndPlot(t){
      const fmt=(n,d=5)=> Number(n).toFixed(d);
      document.getElementById('rain_tile_dt').textContent = t.dt || '';
      document.getElementById('rain_tile_z').textContent = t.z ?? '';
      document.getElementById('rain_tile_y').textContent = t.y ?? '';
      document.getElementById('rain_tile_x').textContent = t.x ?? '';
      document.getElementById('rain_tile_w').textContent = fmt(t.west);
      document.getElementById('rain_tile_s').textContent = fmt(t.south);
      document.getElementById('rain_tile_e').textContent = fmt(t.east);
      document.getElementById('rain_tile_n').textContent = fmt(t.north);
      document.getElementById('rain_tile_res').textContent = fmt(t.res_deg, 6);
      const status = document.getElementById('rain_tile_status');
      if (status) status.textContent='';
      gRainDT = t.dt || '';
      // 同步下拉：若该 dt 存在于列表，选中它
      try{
        const sel = document.getElementById('rainTimeSelect');
        if (sel && gRainDT) {
          for (let i=0;i<sel.options.length;i++){
            if (sel.options[i].value === gRainDT){ sel.value = gRainDT; break; }
          }
        }
      } catch {}
      renderRainTilePlot(t);
    }

    function renderRainTilePlot(t){
      if(!t || !t.values) return;
      const w=t.width, h=t.height, resDeg=t.res_deg;
      const west=t.west, south=t.south;
      const xs=new Array(w); for(let c=0;c<w;c++) xs[c] = west + (c+0.5)*resDeg;
      const ys=new Array(h); for(let r=0;r<h;r++) ys[r] = south + (r+0.5)*resDeg;

      // 色带按照给定定义，增加 index=0 作为 0mm 的纯白
      // 之后 14 段与示例保持一致
      const bandColors = [
        'rgba(255, 255, 255, 1.0)', // 0 mm 专用（白色）
        'rgba(126, 212, 121, 0.78)', // (0,5)
        'rgba(110, 200, 109, 0.78)', // 5–7.5
        'rgba(97, 169, 97, 0.78)',   // 7.5–10
        'rgba(81, 148, 76, 0.78)',   // 10–12.5
        'rgba(90, 158, 112, 0.78)',  // 12.5–15
        'rgba(143, 194, 254, 0.78)', // 15–17.5
        'rgba(92, 134, 245, 0.78)',  // 17.5–20
        'rgba(66, 87, 240, 0.78)',   // 20–25
        'rgba(45, 48, 214, 0.78)',   // 25–30
        'rgba(26, 15, 166, 0.78)',   // 30–40
        'rgba(63, 22, 145, 0.78)',   // 40–50
        'rgba(191, 70, 148, 0.78)',  // 50–75
        'rgba(213, 1, 146, 0.78)',   // 75–100
        'rgba(213, 1, 146, 0.78)'    // >=100（饱和）
      ];
      // 非零阈值分割（mm）：用于映射到上面颜色段（索引需 +1）
      const edges = [0,5,7.5,10,12.5,15,17.5,20,25,30,40,50,75,100, Infinity];

      // 量化到颜色段，同时保留原值用于 hover
      const zBins = []; const custom = [];
      for(let r=0;r<h;r++){
        const row = t.values[r] || [];
        const rowBins = []; const rowCustom = [];
        for(let c=0;c<w;c++){
          const v = row[c];
          if(v==null){ rowBins.push(null); rowCustom.push([r,c,null]); continue; }
          let mm = Number(v); if(mm<0) mm=0;
          let idx = 0;
          if (mm === 0) {
            idx = 0; // 白色 0mm
          } else {
            let nz = 0; while(nz < edges.length-1 && !(mm>=edges[nz] && mm<edges[nz+1])) nz++;
            idx = Math.min(nz + 1, bandColors.length - 1);
          }
          rowBins.push(idx);
          rowCustom.push([r,c,mm]);
        }
        zBins.push(rowBins); custom.push(rowCustom);
      }
      // 将 14 段映射为均匀 positions 的 colorscale
      const colorscale = [];
      for(let i=0;i<bandColors.length;i++){
        const tpos = bandColors.length===1 ? 0 : i/(bandColors.length-1);
        colorscale.push([tpos, bandColors[i]]);
      }
      const data=[{
        type:'heatmap', x:xs, y:ys, z:zBins, customdata:custom,
        colorscale: colorscale, zmin:0, zmax:bandColors.length-1,
        colorbar:{
          orientation:'h', x:0.5, y:-0.12, xanchor:'center', yanchor:'top',
          len:0.8, thickness:16, title:{text:'mm', side:'bottom'},
          // 对应 5/10/15/20/30/50/100 的分段索引：在加入 0mm 白色后需整体 +1
          tickmode:'array', tickvals:[1,3,5,7,9,11,13], ticktext:['5','10','15','20','30','50','100']
        },
        hovertemplate: 'lon=%{x:.3f}, lat=%{y:.3f}<br>rain=%{customdata[2]:.1f} mm<extra></extra>'
      }];
      const el=document.getElementById('rain_tile_plot');
      const layout={ autosize:true, margin:{l:40,r:8,t:8,b:90},
        xaxis:{title:{text:'经度', standoff: 12}, tickformat:'.2f', constrain:'domain', automargin:true},
        yaxis:{title:{text:'纬度', standoff: 12}, tickformat:'.2f', showticklabels:true, scaleanchor:'x', scaleratio:1, constrain:'domain', automargin:true}
      };
      Plotly.newPlot(el, data, layout, {responsive:true, displayModeBar:false}).then(()=>{
        const s = el.clientWidth; Plotly.relayout(el,{height:s});
      });
      window.addEventListener('resize', ()=>{ const s=el.clientWidth; Plotly.relayout(el,{height:s}); Plotly.Plots.resize(el); });
    }

    async function populateRainTimes(z, y, x, fromStr, toStr){
      try{
        let url = `/api/rain/times?z=${z}&y=${y}&x=${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('rainTimeSelect');
        if (!sel) return;
        while (sel.options.length > 1) sel.remove(1);
        const times = j.times || [];
        times.forEach(dt=>{ const opt=document.createElement('option'); opt.value=dt; opt.textContent=dt; sel.appendChild(opt); });
        // 若当前已选中的 gRainDT 在列表里，则保持选中
        if (gRainDT){ for(let i=0;i<sel.options.length;i++){ if(sel.options[i].value===gRainDT){ sel.value=gRainDT; break; } } }
      } catch {}
    }

    async function loadRainAt(z, y, x, dtStr){
      const status = document.getElementById('rain_tile_status');
      if(!dtStr){ return loadNearestRainTile(z,y,x, (gTileDTStr || '')); }
      const res = await fetch(`/api/rain/at?z=${z}&y=${y}&x=${x}&dt=${encodeURIComponent(dtStr)}`);
      if(!res.ok){ status.textContent='未找到雨量瓦片'; return; }
      const t = await res.json();
      fillRainMetaAndPlot(t);
    }

    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(z, y, x, fromStr, toStr) {
      try {
        let url = `/api/radar/times?z=${z}&y=${y}&x=${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) {
          sel.value = gTimes[0];
          gCurrentIdx = 0;
        } else {
          gCurrentIdx = -1;
        }
        updateCountAndButtons();
        updateSlider();
      } catch {}
    }

    function updateCountAndButtons(){
      const info = document.getElementById('countInfo');
      const total = gTimes.length;
      const idxDisp = gCurrentIdx >= 0 ? (gCurrentIdx+1) : 0;
      info.textContent = `共${total}条，第${idxDisp}条`;
      const prev = document.getElementById('btnPrev');
      const next = document.getElementById('btnNext');
      // gTimes 按时间倒序（最新在前），上一时次=更老 => 允许在 idx < total-1
      if (prev) prev.disabled = !(total > 0 && gCurrentIdx >= 0 && gCurrentIdx < total - 1);
      // 下一时次=更近 => 允许在 idx > 0
      if (next) next.disabled = !(total > 0 && gCurrentIdx > 0);
    }

    function updateSlider(){
      const slider = document.getElementById('timeSlider');
      if (!slider) return;
      const total = gTimes.length;
      slider.max = total > 0 ? String(total-1) : '0';
      // 最新（gCurrentIdx=0）时，滑块在最右端（value=max）
      slider.value = gCurrentIdx >= 0 ? String((total-1) - gCurrentIdx) : '0';
    }

    function dtToBucket10(dtStr){
      // dtStr: "YYYY-MM-DD HH:MM:SS"
      if (!dtStr) return '';
      const parts = dtStr.split(/[- :]/g);
      if (parts.length < 6) return '';
      const y=+parts[0], m=+parts[1]-1, d=+parts[2], hh=+parts[3], mm=+parts[4];
      const date = new Date(y, m, d, hh, mm, 0, 0);
      const bucketMin = Math.floor(mm/10)*10;
      date.setMinutes(bucketMin, 0, 0);
      const pad=(n)=> String(n).padStart(2,'0');
      return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:00`;
    }

    function renderTilePlot(t) {
      if (!t || !t.values) return;
      const w = t.width, h = t.height;
      const resDeg = t.res_deg;
      const west = t.west, south = t.south;
      const xs = new Array(w);
      for (let c = 0; c < w; c++) xs[c] = west + (c + 0.5) * resDeg;
      const ys = new Array(h);
      for (let r = 0; r < h; r++) ys[r] = south + (r + 0.5) * resDeg;
      // 保存到全局供后续计算
      gTileValues = t.values; gXs = xs; gYs = ys;
      try { gTileDT = new Date((t.dt || '').replace(/-/g,'/')); gTileDTStr = t.dt || ''; } catch { gTileDTStr = t.dt || ''; }
      const colors = ["#0000FF","#00BFFF","#00FFFF","#7FFFD4","#7CFC00","#ADFF2F","#FFFF00","#FFD700","#FFA500","#FF8C00","#FF4500","#FF0000","#DC143C","#C71585","#8B008B"];
      // 构建离散色阶（0..14）+ customdata（用于 hover 展示 dBZ）
      const zBins = []; const custom = [];
      for (let r = 0; r < h; r++) {
        const row = t.values[r]; const rowBins = []; const rowCustom = [];
        for (let c = 0; c < w; c++) {
          const val = row[c];
          if (val == null) { rowBins.push(null); rowCustom.push([r,c,null]); continue; }
          let dbz = Number(val); if (dbz < 0) dbz = 0; if (dbz > 75) dbz = 75;
          let bin = Math.floor(dbz / 5); if (bin >= 15) bin = 14;
          rowBins.push(bin); rowCustom.push([r,c,dbz]);
        }
        zBins.push(rowBins); custom.push(rowCustom);
      }
      const scale = []; for (let i = 0; i < colors.length; i++) { const tpos = colors.length === 1 ? 0 : i / (colors.length - 1); scale.push([tpos, colors[i]]); }
      const data = [{
        type: 'heatmap', x: xs, y: ys, z: zBins, customdata: custom,
        colorscale: scale, zmin: 0, zmax: 14,
        colorbar: {
          orientation: 'h', x: 0.5, y: -0.12, xanchor: 'center', yanchor: 'top',
          len: 0.8, thickness: 16, title: { text: 'dBZ', side: 'bottom' },
          tickmode: 'array', tickvals: Array.from({length:15}, (_,i)=>i), ticktext: Array.from({length:15}, (_,i)=>String(i*5))
        },
        hovertemplate: 'lon=%{x:.3f}, lat=%{y:.3f}<br>dBZ=%{customdata[2]:.1f}<extra></extra>'
      }];
      // 叠加扇形区域
      if (gWindFromDeg !== null && gWindSpeedMS > 0 && gStLat !== null && gStLon !== null) {
        const half = 30, samples = 64, start = gWindFromDeg - half, end = gWindFromDeg + half, rangeM = gWindSpeedMS * 3 * 3600;
        const xsFan = [gStLon], ysFan = [gStLat];
        for (let i = 0; i <= samples; i++) {
          const θ = start + (end - start) * (i / samples);
          const p = destPoint(gStLat, gStLon, ((θ % 360) + 360) % 360, rangeM);
          xsFan.push(p.lon); ysFan.push(p.lat);
        }
        xsFan.push(gStLon); ysFan.push(gStLat);
        data.push({ type:'scatter', mode:'lines', x:xsFan, y:ysFan, line:{ color:'#FFFFFF', width:2, dash:'dash' }, fill:'toself', fillcolor:'rgba(255,255,255,0.18)', hoverinfo:'skip', showlegend:false });
      }
      // 叠加附近5km圆
      if (gStLat !== null && gStLon !== null) {
        const Rm = 8000; const samples=128; const xsC=[], ysC=[];
        for(let i=0;i<=samples;i++){ const θ=i*(360/samples); const p=destPoint(gStLat,gStLon,θ,Rm); xsC.push(p.lon); ysC.push(p.lat); }
        data.push({ type:'scatter', mode:'lines', x:xsC, y:ysC, line:{ color:'#66CC66', width:1, dash:'dot' }, hoverinfo:'skip', showlegend:false });
      }
      Plotly.newPlot('tile_plot', data, {
        margin:{l:36,r:8,t:8,b:90},
        xaxis:{title:{text:'经度', standoff: 12}, tickformat:'.2f', constrain:'domain', automargin:true},
        yaxis:{title:{text:'纬度', standoff: 12}, tickformat:'.2f', showticklabels: true, scaleanchor:'x', scaleratio:1, constrain:'domain', automargin:true}
      }, {responsive:true, displayModeBar:false}).then(()=>{
        const el = document.getElementById('tile_plot'); const s = el.clientWidth; Plotly.relayout(el,{height:s});
      });
      window.addEventListener('resize',()=>{ const el=document.getElementById('tile_plot'); const s=el.clientWidth; Plotly.relayout(el,{height:s}); Plotly.Plots.resize(el); });
    }

    // 地理与3H预报工具
    function toRad(d){ return d * Math.PI / 180; }
    function toDeg(r){ return r * 180 / Math.PI; }
    function haversine(lat1, lon1, lat2, lon2){
      const R=6371000; const dLat=toRad(lat2-lat1); const dLon=toRad(lon2-lon1);
      const a=Math.sin(dLat/2)**2 + Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2;
      const c=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return R*c;
    }
    function bearingDeg(lat1, lon1, lat2, lon2){
      const φ1=toRad(lat1), φ2=toRad(lat2), Δλ=toRad(lon2-lon1);
      const y=Math.sin(Δλ)*Math.cos(φ2); const x=Math.cos(φ1)*Math.sin(φ2)-Math.sin(φ1)*Math.cos(φ2)*Math.cos(Δλ);
      return (toDeg(Math.atan2(y,x))+360)%360;
    }
    function angDiff(a,b){ let d=((a-b+540)%360)-180; return Math.abs(d); }
    function destPoint(lat, lon, brgDeg, distM){
      const R=6371000, δ=distM/R, θ=toRad(brgDeg), φ1=toRad(lat), λ1=toRad(lon);
      const sinφ1=Math.sin(φ1), cosφ1=Math.cos(φ1), sinδ=Math.sin(δ), cosδ=Math.cos(δ);
      const sinφ2=sinφ1*cosδ+cosφ1*sinδ*Math.cos(θ); const φ2=Math.asin(sinφ2);
      const y=Math.sin(θ)*sinδ*cosφ1; const x=cosδ-sinφ1*sinφ2; const λ2=λ1+Math.atan2(y,x);
      return { lat: toDeg(φ2), lon: ((toDeg(λ2)+540)%360)-180 };
    }

    function maybeCalcSector(){
      try{
        if(!gTileValues || !gXs || !gYs || gWindFromDeg===null || gWindSpeedMS===null || gStLat===null || gStLon===null) return;
        const halfAngle=30; const rangeM=gWindSpeedMS*3*3600; let best=null;
        const h=gTileValues.length, w=gTileValues[0].length;
        for(let r=0;r<h;r++){
          const lat=gYs[r]; for(let c=0;c<w;c++){
            const val=gTileValues[r][c]; if(val==null) continue; const dbz=Number(val); if(!(dbz>=40)) continue;
            const lon=gXs[c]; const dist=haversine(gStLat,gStLon,lat,lon); if(dist>rangeM) continue;
            const brg=bearingDeg(gStLat,gStLon,lat,lon); if(angDiff(brg,gWindFromDeg)>halfAngle) continue;
            if(!best || dist<best.dist){ best={dist,lat,lon,dbz}; }
          }
        }
        const statusEl=document.getElementById('sectorStatus'); const detailEl=document.getElementById('sectorDetail');
        if(!best){ statusEl.textContent='无（≥40 dBZ）'; detailEl.classList.add('hidden'); }
        else {
          const etaSec=best.dist/gWindSpeedMS;
          const base = gTileDT instanceof Date ? gTileDT : new Date();
          const eta=new Date(base.getTime()+etaSec*1000);
          const pad=(n)=>String(n).padStart(2,'0'); const etaStr=`${eta.getFullYear()}-${pad(eta.getMonth()+1)}-${pad(eta.getDate())} ${pad(eta.getHours())}:${pad(eta.getMinutes())}`;
          document.getElementById('sectorDist').textContent=(best.dist/1000).toFixed(1);
          document.getElementById('sectorETA').textContent=etaStr;
          document.getElementById('sectorLat').textContent=Number(best.lat).toFixed(4);
          document.getElementById('sectorLon').textContent=Number(best.lon).toFixed(4);
          document.getElementById('sectorDBZ').textContent=Number(best.dbz).toFixed(1);
          statusEl.textContent='三小时内可能有降雨（≥40 dBZ ）'; detailEl.classList.remove('hidden');
        }
      }catch(e){ document.getElementById('sectorStatus').textContent='计算失败：'+e.message; }
    }

    function maybePlotSquare(){
      try{
        if(!gTileValues || !gXs || !gYs || gWindSpeedMS===null || gStLat===null || gStLon===null) return;
        const R=6371000; const rangeM=gWindSpeedMS*3*3600;
        const dLat=(rangeM/R)*(180/Math.PI);
        const dLon=(rangeM/(R*Math.cos(toRad(gStLat))))*(180/Math.PI);
        const latMin=gStLat-dLat, latMax=gStLat+dLat, lonMin=gStLon-dLon, lonMax=gStLon+dLon;
        const xs=gXs, ys=gYs; const h=gTileValues.length,w=gTileValues[0].length;
        const colors=["#0000FF","#00BFFF","#00FFFF","#7FFFD4","#7CFC00","#ADFF2F","#FFFF00","#FFD700","#FFA500","#FF8C00","#FF4500","#FF0000","#DC143C","#C71585","#8B008B"];
        const scale=[]; for(let i=0;i<colors.length;i++){ const tpos=colors.length===1?0:i/(colors.length-1); scale.push([tpos,colors[i]]); }
        const zBins=[], custom=[];
        for(let r=0;r<h;r++){
          const row=gTileValues[r]; const rowBins=[], rowCustom=[]; const lat=ys[r];
          for(let c=0;c<w;c++){
            const lon=xs[c]; if(lat<latMin||lat>latMax||lon<lonMin||lon>lonMax){ rowBins.push(null); rowCustom.push([r,c,null]); continue; }
            const val=row[c]; if(val==null){ rowBins.push(null); rowCustom.push([r,c,null]); continue; }
            let dbz=Number(val); if(dbz<0)dbz=0; if(dbz>75)dbz=75; let bin=Math.floor(dbz/5); if(bin>=15)bin=14;
            rowBins.push(bin); rowCustom.push([r,c,dbz]);
          }
          zBins.push(rowBins); custom.push(rowCustom);
        }
        const data=[{ type:'heatmap', x:xs, y:ys, z:zBins, customdata:custom, colorscale:scale, zmin:0, zmax:14,
          colorbar:{orientation:'h',x:0.5,y:-0.12,xanchor:'center',yanchor:'top',len:0.8,thickness:16,title:{text:'dBZ',side:'bottom'},
            tickmode:'array',tickvals:Array.from({length:15},(_,i)=>i),ticktext:Array.from({length:15},(_,i)=>String(i*5))},
          hovertemplate:'lon=%{x:.3f}, lat=%{y:.3f}<br>dBZ=%{customdata[2]:.1f}<extra></extra>' }];
        if(gWindFromDeg!==null && gWindSpeedMS>0){
          const half=30, samples=64, start=gWindFromDeg-half, end=gWindFromDeg+half;
          const xsFan=[gStLon], ysFan=[gStLat];
          for(let i=0;i<=samples;i++){ const θ=start+(end-start)*(i/samples); const p=destPoint(gStLat,gStLon,((θ%360)+360)%360,rangeM); xsFan.push(p.lon); ysFan.push(p.lat); }
          xsFan.push(gStLon); ysFan.push(gStLat);
          data.push({ type:'scatter', mode:'lines', x:xsFan, y:ysFan, line:{color:'#FFFFFF',width:2,dash:'dash'}, fill:'toself', fillcolor:'rgba(255,255,255,0.18)', hoverinfo:'skip', showlegend:false });
        }
        // 叠加附近8km圆
        {
          const Rm = 8000; const samples=128; const xsC=[], ysC=[];
          for(let i=0;i<=samples;i++){ const θ=i*(360/samples); const p=destPoint(gStLat,gStLon,θ,Rm); xsC.push(p.lon); ysC.push(p.lat); }
          data.push({ type:'scatter', mode:'lines', x:xsC, y:ysC, line:{ color:'#66CC66', width:1, dash:'dot' }, hoverinfo:'skip', showlegend:false });
        }
        const el=document.getElementById('squarePlot');
        const layout={ autosize:true, margin:{l:40,r:8,t:8,b:90},
          xaxis:{title:'经度', tickformat:'.2f', zeroline:false, constrain:'domain', automargin:true, range:[lonMin, lonMax]},
          yaxis:{title:{text:'纬度',standoff:12}, tickformat:'.2f', zeroline:false, scaleanchor:'x', scaleratio:1, constrain:'domain', automargin:true, range:[latMin, latMax]},
          shapes:[{type:'rect',xref:'x',yref:'y',x0:lonMin,x1:lonMax,y0:latMin,y1:latMax,line:{color:'#111',width:1,dash:'dot'},fillcolor:'rgba(0,0,0,0)'}] };
        Plotly.newPlot(el, data, layout, {responsive:true, displayModeBar:false}).then(()=>{ const s=el.clientWidth; Plotly.relayout(el,{height:s}); });
        window.addEventListener('resize',()=>{ const s=el.clientWidth; Plotly.relayout(el,{height:s}); Plotly.Plots.resize(el); });
      }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>`; }
    }

    function maybeCalcNearbyRain(){
      try{
        const el = document.getElementById('nearbyStatus'); if(!el) return;
        if(!gTileValues || !gXs || !gYs || gStLat===null || gStLon===null){
          // 数据不足时清空提示并恢复默认样式
          el.textContent='';
          el.classList.remove('text-red-700');
          el.classList.add('text-gray-700');
          return;
        }
        const radiusM = 8000; // 8km
        const h=gTileValues.length, w=gTileValues[0].length;
        let hit=false, maxDBZ=null;
        for(let r=0;r<h && !hit;r++){
          const lat=gYs[r];
          for(let c=0;c<w;c++){
            const val=gTileValues[r][c]; if(val==null) continue; const dbz=Number(val); if(!(dbz>=40)) continue;
            const lon=gXs[c]; const dist=haversine(gStLat,gStLon,lat,lon);
            if(dist <= radiusM){ hit=true; maxDBZ = maxDBZ==null?dbz:Math.max(maxDBZ, dbz); break; }
          }
        }
        if(hit){
          el.textContent = `附近8公里内检测到大于等于 40dBz 的雷达反射率，短时间内可能会下雨`;
          el.classList.remove('text-gray-700');
          el.classList.add('text-red-700');
        } else {
          // 未命中时清空并恢复默认样式，避免提示“停留不消失”
          el.textContent = '';
          el.classList.remove('text-red-700');
          el.classList.add('text-gray-700');
        }
      }catch(e){ /* ignore */ }
    }

    // 计算并展示：扇形区域与8km圆形区域的 >=40 和 >=30 dBZ 点数与累计值
    function maybeCalcTileRegionStats(){
      try{
        const s40CntEl = document.getElementById('sector_ge40_cnt');
        if(!s40CntEl) return; // 元素不存在则不计算
        // 先清空展示
        const ids=[
          'sector_ge40_cnt','sector_ge40_sum','sector_ge30_cnt','sector_ge30_sum',
          'circle_ge40_cnt','circle_ge40_sum','circle_ge30_cnt','circle_ge30_sum'
        ];
        ids.forEach(id=>{ const el=document.getElementById(id); if(el) el.textContent='-'; });

        if(!gTileValues || !gXs || !gYs || gStLat===null || gStLon===null) return;
        const h=gTileValues.length, w=gTileValues[0].length;

        // 扇形区域定义：风向 ±30°，半径 = 3 小时移动距离
        const hasWind = (gWindFromDeg!==null && gWindSpeedMS!==null && gWindSpeedMS>0);
        const halfAngle=30;
        const rangeM = hasWind ? (gWindSpeedMS*3*3600) : 0;

        // 圆形区域：半径 8km
        const circleR = 8000;

        let sec40Cnt=0, sec40Sum=0, sec30Cnt=0, sec30Sum=0;
        let cir40Cnt=0, cir40Sum=0, cir30Cnt=0, cir30Sum=0;

        for(let r=0;r<h;r++){
          const lat=gYs[r];
          for(let c=0;c<w;c++){
            const v=gTileValues[r][c];
            if(v==null) continue;
            const dbz=Number(v);
            const lon=gXs[c];
            const dist=haversine(gStLat,gStLon,lat,lon);

            // 圆形区域统计
            if(dist<=circleR){
              if(dbz>=40){ cir40Cnt++; cir40Sum+=dbz; }
              if(dbz>=30){ cir30Cnt++; cir30Sum+=dbz; }
            }

            // 扇形区域统计（需要风向风速）
            if(hasWind){
              if(dist<=rangeM){
                const brg=bearingDeg(gStLat,gStLon,lat,lon);
                if(angDiff(brg,gWindFromDeg)<=halfAngle){
                  if(dbz>=40){ sec40Cnt++; sec40Sum+=dbz; }
                  if(dbz>=30){ sec30Cnt++; sec30Sum+=dbz; }
                }
              }
            }
          }
        }

        // 更新展示
        const set = (id, val)=>{ const el=document.getElementById(id); if(el) el.textContent=String(val); };
        if(hasWind){
          set('sector_ge40_cnt', sec40Cnt); set('sector_ge40_sum', sec40Sum.toFixed(1));
          set('sector_ge30_cnt', sec30Cnt); set('sector_ge30_sum', sec30Sum.toFixed(1));
        } else {
          // 无风场信息时显示提示
          set('sector_ge40_cnt', '无风场'); set('sector_ge40_sum', '-');
          set('sector_ge30_cnt', '无风场'); set('sector_ge30_sum', '-');
        }
        set('circle_ge40_cnt', cir40Cnt); set('circle_ge40_sum', cir40Sum.toFixed(1));
        set('circle_ge30_cnt', cir30Cnt); set('circle_ge30_sum', cir30Sum.toFixed(1));
      }catch(e){ /* ignore */ }
    }

    async function main() {
      await loadStations();
      // 初始化时间范围为最近24小时
      (function initRange(){
        const now = new Date();
        const end = now; // 到现在
        const start = new Date(now.getTime() - 6*3600*1000); // 6 小时前
        const pad = (n)=> String(n).padStart(2,'0');
        const toLocalInput=(d)=> `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
        const sEl = document.getElementById('tsStart');
        const eEl = document.getElementById('tsEnd');
        if (sEl && eEl) { sEl.value = toLocalInput(start); eEl.value = toLocalInput(end); }
      })();
      document.getElementById('btnLoad').addEventListener('click', async ()=>{
        const sel = document.getElementById('stationSelect');
        if (!sel || !sel.value) return;
        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 = aliasParam;
        document.getElementById('rt_zyx').textContent = `z=${gZ}, y=${gY}, x=${gX}`;
        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);
          await populateTimes(gZ,gY,gX, from, to);
          await populateRainTimes(gZ,gY,gX, from, to);
          await loadLatestTile(gZ,gY,gX);
          // 同步：加载最新小时雨量（若 radar 未返回 dt，则先尝试最新）
          try { await loadLatestRainTile(gZ,gY,gX); } catch(e) { /* ignore */ }
        }
      });

      const tsQuery = document.getElementById('tsQuery');
      if (tsQuery) tsQuery.addEventListener('click', async ()=>{
        if (!(gZ && gY && gX)) return;
        const from = fromDTLocalInput(document.getElementById('tsStart').value);
        const to = fromDTLocalInput(document.getElementById('tsEnd').value);
        await populateTimes(gZ,gY,gX, from, to);
        await populateRainTimes(gZ,gY,gX, from, to);
        if (gCurrentIdx >= 0) await loadTileAt(gZ,gY,gX, gTimes[gCurrentIdx]);
      });

      const timeSelect = document.getElementById('timeSelect');
      if (timeSelect) timeSelect.addEventListener('change', async (e)=>{
        if (!(gZ && gY && gX)) return;
        const dt = e.target.value;
        if (!dt) { await loadLatestTile(gZ,gY,gX); return; }
        gCurrentIdx = gTimes.indexOf(dt);
        updateCountAndButtons();
        updateSlider();
        await loadTileAt(gZ,gY,gX, dt);
      });

      const timeSlider = document.getElementById('timeSlider');
      if (timeSlider) timeSlider.addEventListener('input', async (e)=>{
        if (!(gZ && gY && gX)) return;
        const total = gTimes.length;
        const raw = Number(e.target.value);
        const idx = (total - 1) - raw; // 右端=最新
        if (idx >= 0 && idx < gTimes.length) {
          gCurrentIdx = idx;
          updateCountAndButtons();
          const sel = document.getElementById('timeSelect');
          if (sel) sel.value = gTimes[idx] || '';
          await loadTileAt(gZ,gY,gX, gTimes[idx]);
        }
      });

      const rainTimeSelect = document.getElementById('rainTimeSelect');
      if (rainTimeSelect) rainTimeSelect.addEventListener('change', async (e)=>{
        if (!(gZ && gY && gX)) return;
        const dt = e.target.value;
        if (!dt) {
          // 自动匹配到当前雷达时次（就近<=）
          const target = (gCurrentIdx>=0 && gTimes[gCurrentIdx]) ? gTimes[gCurrentIdx] : (gTileDTStr || '');
          await loadNearestRainTile(gZ,gY,gX, target);
        } else {
          await loadRainAt(gZ,gY,gX, dt);
        }
      });

      const btnPrev = document.getElementById('btnPrev');
      if (btnPrev) btnPrev.addEventListener('click', async ()=>{
        if (!(gZ && gY && gX)) return;
        // 上一时次：向更老的时间移动（索引+1）
        if (gCurrentIdx < gTimes.length - 1) {
          gCurrentIdx++;
          updateCountAndButtons();
          updateSlider();
          const sel = document.getElementById('timeSelect');
          if (sel) sel.value = gTimes[gCurrentIdx] || '';
          await loadTileAt(gZ,gY,gX, gTimes[gCurrentIdx]);
        }
      });

      const btnNext = document.getElementById('btnNext');
      if (btnNext) btnNext.addEventListener('click', async ()=>{
        if (!(gZ && gY && gX)) return;
        // 下一时次：向更新的时间移动（索引-1）
        if (gCurrentIdx > 0) {
          gCurrentIdx--;
          updateCountAndButtons();
          updateSlider();
          const sel = document.getElementById('timeSelect');
          if (sel) sel.value = gTimes[gCurrentIdx] || '';
          await loadTileAt(gZ,gY,gX, gTimes[gCurrentIdx]);
        }
      });
    }
    window.addEventListener('DOMContentLoaded', main);
  </script>
</head>
<body>
  {{ template "header" . }}
  <div class="content-narrow p-4 text-sm">
    <div class="card mb-4">
      <div class="text-lg font-semibold mb-2">雷达站</div>
      <div class="flex flex-col gap-3">
        <div class="flex items-center gap-2">
          <label class="text-sm">选择站点：</label>
          <select id="stationSelect" class="border rounded px-2 py-1 text-sm min-w-[360px]"></select>
          <button id="btnLoad" class="px-2.5 py-1 text-sm bg-blue-600 text-white rounded">加载数据</button>
        </div>
        <div class="flex flex-wrap items-center gap-2">
          <span class="text-sm text-gray-700">时间范围：</span>
          <input id="tsStart" type="datetime-local" class="border rounded px-2 py-1 text-sm"/>
          <span>至</span>
          <input id="tsEnd" type="datetime-local" class="border rounded px-2 py-1 text-sm"/>
          <button id="tsQuery" class="px-2.5 py-1 text-sm bg-blue-600 text-white rounded">查询</button>
        </div>
      </div>
    </div>

    <div class="card mb-4">
      <div class="text-base font-semibold mb-2">气象数据</div>
      <div id="rtBox" class="text-sm grid grid-cols-2 gap-y-1.5 gap-x-6">
        <div>站点：<span id="rt_alias"></span></div>
        <div>Z/Y/X：<span id="rt_zyx"></span></div>
        <div>时间：<span id="rt_dt"></span></div>
        <div>位置：<span id="rt_lat"></span>，<span id="rt_lon"></span></div>
        <div>温度：<span id="rt_t"></span> ℃</div>
        <div>湿度：<span id="rt_h"></span> %</div>
        <div>风速：<span id="rt_ws"></span> m/s</div>
        <div>风向：<span id="rt_wd"></span> °</div>
        <div>云量：<span id="rt_c"></span> %</div>
        <div>能见度：<span id="rt_vis"></span> km</div>
        <div>下行短波：<span id="rt_dswrf"></span> W/m²</div>
        <div>气压：<span id="rt_p"></span> Pa</div>
      </div>
      <div id="rt_stale" class="mt-2 hidden p-2 text-sm text-yellow-800 bg-yellow-50 border border-yellow-200 rounded">提示：该瓦片时次的就近实况相差超过 2 小时</div>
    </div>

    <div class="card my-4">
      <div class="text-lg font-semibold mb-2">未来 3H 预报</div>
      <div id="sectorInfo" class="text-sm">
        <div id="sectorStatus">计算中…</div>
        <div id="sectorDetail" class="mt-1 hidden">
          最近距离：<span id="sectorDist"></span> km；预计到达时间：<span id="sectorETA"></span><br>
          位置：lat=<span id="sectorLat"></span>，lon=<span id="sectorLon"></span> 组合反射率：<span id="sectorDBZ"></span> dBZ
        </div>
        <div id="nearbyStatus" class="mt-1 text-gray-700"></div>
      </div>
    </div>

    <div class="card">
      <div class="text-base font-semibold mb-2">雷达瓦片</div>
      <div class="text-sm space-y-1">
        <div>时间：<span id="tile_dt" class="font-mono"></span></div>
        <div>索引：z=<span id="tile_z"></span> / y=<span id="tile_y"></span> / x=<span id="tile_x"></span></div>
        <div>边界：W=<span id="tile_w"></span>，S=<span id="tile_s"></span>，E=<span id="tile_e"></span>，N=<span id="tile_n"></span></div>
        <div>分辨率（度/像素）：<span id="tile_res"></span></div>
        <div id="tile_status" class="text-gray-500"></div>
      </div>
      <div class="flex flex-wrap items-center justify-center gap-2 mt-3">
        <button id="btnPrev" class="px-2.5 py-1 text-sm border rounded">上一时次</button>
        <span id="countInfo" class="text-gray-600 text-sm">共0条，第0条</span>
        <button id="btnNext" class="px-2.5 py-1 text-sm border rounded">下一时次</button>
        <select id="timeSelect" class="border rounded px-2 py-1 text-sm min-w-[240px]">
          <option value="">最新</option>
        </select>
      </div>

      <div class="w-full flex justify-center my-2">
        <input id="timeSlider" type="range" min="0" max="0" value="0" step="1" class="slider slider-horizontal w-64" />
      </div>
      <div id="tile_plot" class="plot-box-sm mt-3"></div>
    </div>

    <div class="card mt-4">
      <div class="text-base font-semibold mb-2">一小时降雨瓦片</div>
      <div class="flex flex-wrap items-center gap-2 mb-2">
        <span class="text-sm text-gray-700">选择雨量时次：</span>
        <select id="rainTimeSelect" class="border rounded px-2 py-1 text-sm min-w-[240px]">
          <option value="">自动匹配（就近≤）</option>
        </select>
      </div>
      <div class="text-sm space-y-1">
        <div>时间：<span id="rain_tile_dt" class="font-mono"></span></div>
        <div>索引：z=<span id="rain_tile_z"></span> / y=<span id="rain_tile_y"></span> / x=<span id="rain_tile_x"></span></div>
        <div>边界：W=<span id="rain_tile_w"></span>，S=<span id="rain_tile_s"></span>，E=<span id="rain_tile_e"></span>，N=<span id="rain_tile_n"></span></div>
        <div>分辨率（度/像素）：<span id="rain_tile_res"></span></div>
        <div id="rain_tile_status" class="text-gray-500"></div>
      </div>
      <div id="rain_tile_plot" class="plot-box-sm mt-3"></div>
    </div>


    <div class="card mt-4" style="width:100%;">
      <div class="text-lg font-semibold mb-2">正方形裁减区域</div>
      <div class="mt-3 text-sm space-y-1">
        <div class="font-bold">扇形区域统计</div>
        <span class="text-gray-700">≥40 dBZ：点数 <span id="sector_ge40_cnt">-</span>，累计 <span id="sector_ge40_sum">-</span></span>
        <span class="text-gray-700"> | ≥30 dBZ：点数 <span id="sector_ge30_cnt">-</span>，累计 <span id="sector_ge30_sum">-</span></span>
        <div class="font-bold mt-2">圆形区域统计</div>
        <span class="text-gray-700">≥40 dBZ：点数 <span id="circle_ge40_cnt">-</span>，累计 <span id="circle_ge40_sum">-</span></span>
        <span class="text-gray-700"> | ≥30 dBZ：点数 <span id="circle_ge30_cnt">-</span>，累计 <span id="circle_ge30_sum">-</span></span>
      </div>
      <div id="squarePlot" class="plot-box"></div>
    </div>
  </div>
</body>
</html>
