From cfb0bca7238a3eada215993da840db0e9c5d20a2 Mon Sep 17 00:00:00 2001 From: yarnom Date: Tue, 23 Sep 2025 09:33:00 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20=E6=96=B0=E5=A2=9E=E9=9B=B7?= =?UTF-8?q?=E8=BE=BE=E5=9B=BE"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit df7358530f428751cdbce3f4220f1925e7b616c2. --- internal/server/gin.go | 146 +++++++++++++++++++++-------------------- templates/index.html | 129 +++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 131 deletions(-) diff --git a/internal/server/gin.go b/internal/server/gin.go index 244e712..d1b6888 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -339,13 +339,12 @@ func intFromMeta(m map[string]any, key string) int { // radarLatestWindHandler queries Caiyun realtime wind for the latest query candidates // and provides per-cluster aggregated wind and basic coming/ETA analysis toward station. func radarLatestWindHandler(c *gin.Context) { - // 使用极坐标法:对每个云团仅在质心取一次风,直接判定靠近与ETA - // 常量:目标点(站点/雷达点)坐标 + // Constants per user request const ( stationLat = 23.097234 stationLon = 108.715433 ) - // 读取最新元数据 + // Read latest metadata into struct latestRoot := "./radar_data/latest" metaPath := latestRoot + "/metadata.json" b, err := os.ReadFile(metaPath) @@ -358,8 +357,7 @@ func radarLatestWindHandler(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "解析元数据失败"}) return } - - // 输出结构保持兼容:仍提供 candidates,但每个cluster仅一个(质心) + // For each query candidate, call Caiyun type Wind struct { Speed float64 `json:"speed_ms"` DirFrom float64 `json:"dir_from_deg"` @@ -375,38 +373,29 @@ func radarLatestWindHandler(c *gin.Context) { Wind *Wind `json:"wind,omitempty"` Error string `json:"error,omitempty"` } - - outs := make([]CandOut, 0, len(meta.Clusters)) - - // 工具函数 - mPerDegLat := 111320.0 - mPerDegLon := func(lat float64) float64 { return 111320.0 * math.Cos(lat*math.Pi/180.0) } - // 计算极坐标ETA(到站点本身,不再使用侧向与半径作为命中条件) - approachETA := func(lonC, latC, speedMS, dirToDeg, lonS, latS float64) (coming bool, etaMin float64, distanceKm float64, vrMS float64) { - wx := mPerDegLon(latC) - wy := mPerDegLat - dx := (lonS - lonC) * wx // 东向米 - dy := (latS - latC) * wy // 北向米 - D := math.Hypot(dx, dy) - if D == 0 { - return true, 0, 0, speedMS + outs := make([]CandOut, 0, len(meta.QueryCandidates)) + for _, q := range meta.QueryCandidates { + speed, dirFrom, tempC, rh, pPa, err := rf.FetchCaiyunRealtime(q.Lon, q.Lat) + co := CandOut{QueryCandidate: q} + if err != nil { + co.Error = err.Error() + } else { + dirTo := mathMod(dirFrom+180.0, 360.0) + u, v := windVectorUV(speed, dirTo) + // pressure in hPa for display + pHpa := pPa / 100.0 + co.Wind = &Wind{Speed: speed, DirFrom: dirFrom, DirTo: dirTo, U: u, V: v, TempC: tempC, RH: rh, PressureHpa: pHpa} } - // 云→站方位角(北=0,顺时针) - theta := math.Atan2(dx, dy) * 180 / math.Pi - if theta < 0 { - theta += 360 - } - beta := mathMod(dirToDeg, 360.0) - delta := (beta - theta) * math.Pi / 180.0 - vr := speedMS * math.Cos(delta) // 指向站点的径向速度 - if vr <= 0 { - return false, -1, D / 1000.0, vr - } - etaSec := D / vr - return true, etaSec / 60.0, D / 1000.0, vr + outs = append(outs, co) + } + // Aggregate by cluster id + agg := map[int][]Wind{} + for _, co := range outs { + if co.Wind == nil { + continue + } + agg[co.ClusterID] = append(agg[co.ClusterID], *co.Wind) } - - // 为每个云团质心取一次风,构造 candidates 与 per-cluster 分析 type ClusterAnal struct { ClusterID int `json:"cluster_id"` Lon float64 `json:"lon"` @@ -422,64 +411,77 @@ func radarLatestWindHandler(c *gin.Context) { DistanceKm float64 `json:"distance_km"` LateralKm float64 `json:"lateral_km"` RCloudKm float64 `json:"r_cloud_km"` - VrMS float64 `json:"vr_ms"` } analyses := []ClusterAnal{} - - // 等效云半径与侧向距离仅用于展示(不再作为判定条件) - cellDims := func(lat float64) (float64, float64) { // 每像素米宽/米高 + // helpers + mPerDegLat := 111320.0 + mPerDegLon := func(lat float64) float64 { return 111320.0 * math.Cos(lat*math.Pi/180.0) } + cellDims := func(lat float64) (float64, float64) { // width (lon), height (lat) in meters per pixel return meta.ResDeg * mPerDegLon(lat), meta.ResDeg * mPerDegLat } - + const hitRadiusM = 5000.0 for _, cl := range meta.Clusters { - // 取质心风 - speed, dirFrom, tempC, rh, pPa, err := rf.FetchCaiyunRealtime(cl.Lon, cl.Lat) - q := rf.QueryCandidate{ClusterID: cl.ID, Role: "center", Lon: cl.Lon, Lat: cl.Lat} - co := CandOut{QueryCandidate: q} - if err != nil { - co.Error = err.Error() - outs = append(outs, co) - // 即便取风失败,也继续下一个云团 + winds := agg[cl.ID] + if len(winds) == 0 { continue } - dirTo := mathMod(dirFrom+180.0, 360.0) - u, v := windVectorUV(speed, dirTo) - pHpa := pPa / 100.0 - co.Wind = &Wind{Speed: speed, DirFrom: dirFrom, DirTo: dirTo, U: u, V: v, TempC: tempC, RH: rh, PressureHpa: pHpa} - outs = append(outs, co) - - // 极坐标法靠近/ETA(到站点) - coming, etaMin, distKm, vr := approachETA(cl.Lon, cl.Lat, speed, dirTo, stationLon, stationLat) - - // 展示参数:侧向距与等效半径 - // 侧向距 = 距离向量对速度方向单位向量的叉积绝对值 - wx, wy := mPerDegLon(cl.Lat), mPerDegLat - px := (stationLon - cl.Lon) * wx - py := (stationLat - cl.Lat) * wy - vnorm := math.Hypot(u, v) - lateral := 0.0 - if vnorm > 0 { - vx, vy := u/vnorm, v/vnorm - lateral = math.Abs(px*vy - py*vx) + // vector average in u,v (to-direction) + sumU, sumV := 0.0, 0.0 + for _, wv := range winds { + sumU += wv.U + sumV += wv.V } + u := sumU / float64(len(winds)) + v := sumV / float64(len(winds)) + speed := math.Hypot(u, v) + dirTo := uvToDirTo(u, v) + // project geometry + wx, wy := mPerDegLon(cl.Lat), mPerDegLat + // position of cluster and station in meters (local tangent plane) + px := (cl.Lon - stationLon) * wx + py := (cl.Lat - stationLat) * wy + // vector from cluster to station + dx := -px + dy := -py + d := math.Hypot(dx, dy) + // radial component of velocity towards station + if d == 0 { + d = 1e-6 + } + vr := (dx*u + dy*v) / d + // cluster equivalent radius cw, ch := cellDims(cl.Lat) areaM2 := float64(cl.AreaPx) * cw * ch rCloud := math.Sqrt(areaM2 / math.Pi) - + // lateral offset (perpendicular distance from station line) + vnorm := math.Hypot(u, v) + lateral := 0.0 + if vnorm > 0 { + // |d x vhat| + vx, vy := u/vnorm, v/vnorm + lateral = math.Abs(dx*vy - dy*vx) + } + coming := vr > 0 && lateral <= (rCloud+hitRadiusM) + etaMin := 0.0 + if coming && vr > 0 { + distToEdge := d - (rCloud + hitRadiusM) + if distToEdge < 0 { + distToEdge = 0 + } + etaMin = distToEdge / vr / 60.0 + } analyses = append(analyses, ClusterAnal{ ClusterID: cl.ID, Lon: cl.Lon, Lat: cl.Lat, AreaPx: cl.AreaPx, MaxDBZ: cl.MaxDBZ, SpeedMS: speed, DirToDeg: dirTo, U: u, V: v, Coming: coming, ETAMin: round2(etaMin), - DistanceKm: round2(distKm), LateralKm: round2(lateral / 1000.0), RCloudKm: round2(rCloud / 1000.0), - VrMS: round2(vr), + DistanceKm: round2(d / 1000.0), LateralKm: round2(lateral / 1000.0), RCloudKm: round2(rCloud / 1000.0), }) } - c.JSON(http.StatusOK, gin.H{ "station": gin.H{"lon": stationLon, "lat": stationLat}, - "params": meta.QueryParams, // 兼容保留 + "params": meta.QueryParams, "candidates": outs, "clusters": analyses, }) diff --git a/templates/index.html b/templates/index.html index e3c6bf4..e59e604 100644 --- a/templates/index.html +++ b/templates/index.html @@ -720,7 +720,7 @@ window.RadarLatestGrid = data; renderPlotlyHeat(data); renderClustersPanel(); - renderMethodNote(); + renderWindQueryList(); renderWindResults(); } @@ -838,7 +838,11 @@ + '质心: '+cl.lon.toFixed(4)+', '+cl.lat.toFixed(4)+'
' + 'dBZ: max '+cl.max_dbz.toFixed(1)+' / avg '+cl.avg_dbz.toFixed(1) + ''; - // 极坐标法:不再展示采样点列表(仅使用质心) + if (cl.samples && cl.samples.length) { + html += '
采样点: ' + cl.samples.map(function(s){ + return s.role+':('+s.lon.toFixed(3)+','+s.lat.toFixed(3)+')'; + }).join(' | ') + '
'; + } html += ''; }); html += ''; @@ -846,20 +850,40 @@ }).catch(function(){ /* ignore */ }); } -function renderMethodNote(){ - var containerId = 'radar-wind-query'; - var parent = document.getElementById(containerId); - if (!parent) { - var sec = document.createElement('div'); - sec.id = containerId; - sec.className = 'mt-4'; - var root = document.getElementById('view-radar').querySelector('.radar-grid'); - root.appendChild(sec); - parent = sec; - } - var html = '
方法
'; - html += '
极坐标(质心单点):使用云团质心处彩云风,计算与站点的径向分量与 ETA。
'; - parent.innerHTML = html; + function renderWindQueryList(){ + fetch('/api/radar/latest').then(r=>r.json()).then(function(resp){ + var meta = resp.meta || {}; + var params = meta.query_params || {}; + var cands = meta.query_candidates || []; + var containerId = 'radar-wind-query'; + var parent = document.getElementById(containerId); + if (!parent) { + var sec = document.createElement('div'); + sec.id = containerId; + sec.className = 'mt-4'; + var root = document.getElementById('view-radar').querySelector('.radar-grid'); + root.appendChild(sec); + parent = sec; + } + var html = '
风场查询参数
'; + html += '
' + + 'min_area_px='+ (params.min_area_px||9) + + ',strong_dbz_override=' + (params.strong_dbz_override||50) + + ',max_samples_per_cluster=' + (params.max_samples_per_cluster||5) + + ',max_candidates_total=' + (params.max_candidates_total||25) + + '
'; + if (!cands.length) { + html += '
暂无需要查询的采样点
'; + } else { + html += '
需要查询的采样点(共 '+cands.length+' 个)
'; + html += ''; + } + parent.innerHTML = html; + }).catch(function(){}); } function renderWindResults(){ @@ -891,51 +915,38 @@ function renderMethodNote(){ }); html += ''; } - // 朝向云团:极坐标计算明细列表 - var candByCluster = {}; - (cands||[]).forEach(function(co){ candByCluster[co.cluster_id] = co; }); - var comings = (clusters||[]).filter(function(cl){ return cl.coming; }); - if (comings.length) { - html += '
朝向云团(极坐标计算明细)
'; - comings.forEach(function(cl){ - var co = candByCluster[cl.cluster_id] || {}; - var w = co.wind || {}; - var speed = (w.speed_ms!=null)? w.speed_ms : (cl.speed_ms||0); - var dirFrom = (w.dir_from_deg!=null)? w.dir_from_deg : null; - var dirTo = (w.dir_to_deg!=null)? w.dir_to_deg : (cl.dir_to_deg||0); - // 计算几何与极坐标过程(前端复算,便于展示公式与数值) - var mPerDegLat = 111320.0; - var mPerDegLon = 111320.0 * Math.cos((cl.lat||0) * Math.PI/180.0); - var dx = ((station.lon||0) - (cl.lon||0)) * mPerDegLon; // 东向 - var dy = ((station.lat||0) - (cl.lat||0)) * mPerDegLat; // 北向 - var D = Math.hypot(dx, dy); // m - var theta = Math.atan2(dx, dy) * 180/Math.PI; // 北=0, 顺时针 - if (theta < 0) theta += 360; - var delta = dirTo - theta; // deg - // wrap 到 [-180,180] - delta = ((delta + 540) % 360) - 180; - var vr = speed * Math.cos(delta * Math.PI/180.0); // m/s(指向站点为正) - var etaMin = (vr>0) ? (D/vr/60.0) : null; - var code = '' - + 'station = ('+(station.lon||0).toFixed(6)+', '+(station.lat||0).toFixed(6)+')\n' - + 'centroid = ('+(cl.lon||0).toFixed(6)+', '+(cl.lat||0).toFixed(6)+')\n' - + 'speed = '+speed.toFixed(2)+' m/s\n' - + (dirFrom!=null?('dir_from = '+dirFrom.toFixed(0)+'°\n'):'') - + 'dir_to = '+dirTo.toFixed(0)+'°\n' - + 'dx = (lonS-lonC) * 111320*cos(latC) = '+dx.toFixed(1)+' m\n' - + 'dy = (latS-latC) * 111320 = '+dy.toFixed(1)+' m\n' - + 'D = hypot(dx,dy) = '+(D/1000.0).toFixed(2)+' km\n' - + 'theta = atan2(dx,dy) = '+theta.toFixed(1)+'°\n' - + 'delta = dir_to - theta = '+delta.toFixed(1)+'°\n' - + 'vr = speed * cos(delta) = '+vr.toFixed(2)+' m/s\n' - + (etaMin!=null?('ETA = D/vr = '+etaMin.toFixed(1)+' min\n'):'ETA = N/A (vr<=0)\n'); - html += '
' - + '
ID '+cl.cluster_id+' | 质心: '+cl.lon.toFixed(4)+', '+cl.lat.toFixed(4)+'
' - + '
'+code.replace(/'
-                                 + '
'; + // candidate details + if (cands.length) { + html += '
采样点明细:
'; + html += '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + cands.forEach(function(p){ + var w = p.wind || {}; + html += '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; }); + html += '
clusterrolelonlatspd(m/s)dir_from(°)T(°C)RHP(hPa)err
'+p.cluster_id+''+p.role+''+p.lon.toFixed(4)+''+p.lat.toFixed(4)+''+(w.speed_ms!=null?w.speed_ms.toFixed(1):'')+''+(w.dir_from_deg!=null?w.dir_from_deg.toFixed(0):'')+''+(w.temp_c!=null?w.temp_c.toFixed(1):'')+''+(w.rh!=null?(w.rh*100).toFixed(0)+'%':'')+''+(w.pressure_hpa!=null?w.pressure_hpa.toFixed(1):'')+''+(p.error||'')+'
'; } - // 极坐标法:省略采样点明细表,仅保留汇总与朝向明细 parent.innerHTML = html; }).catch(function(){}); }