feat: 新增扇形和圆形区域的统计
This commit is contained in:
parent
c420608387
commit
4efc4cc3ce
@ -73,6 +73,7 @@
|
|||||||
<select class="px-2 py-1 border rounded text-sm" [(ngModel)]="tileProduct" (change)="onProductChange()">
|
<select class="px-2 py-1 border rounded text-sm" [(ngModel)]="tileProduct" (change)="onProductChange()">
|
||||||
<option value="none">不显示</option>
|
<option value="none">不显示</option>
|
||||||
<option value="radar">水汽含量</option>
|
<option value="radar">水汽含量</option>
|
||||||
|
<option value="radar_detail">水汽含量(详细)</option>
|
||||||
<option value="rain">1h 实际降雨</option>
|
<option value="rain">1h 实际降雨</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
@ -90,6 +91,22 @@
|
|||||||
<div id="mapContainer" class="rounded border mb-4" [ngClass]="{ 'collapsed': isMapCollapsed }" [style.borderColor]="'#ddd'" style="position:relative; overflow:hidden;" [style.height]="isMapCollapsed ? '38vh' : '60vh'">
|
<div id="mapContainer" class="rounded border mb-4" [ngClass]="{ 'collapsed': isMapCollapsed }" [style.borderColor]="'#ddd'" style="position:relative; overflow:hidden;" [style.height]="isMapCollapsed ? '38vh' : '60vh'">
|
||||||
<div id="map" style="width:100%; height:100%;"></div>
|
<div id="map" style="width:100%; height:100%;"></div>
|
||||||
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" style="position:absolute;top:10px;right:10px;z-index:1001;border-radius:4px;padding:5px 10px;font-size:12px;font-weight:bold;" (click)="toggleMap()">{{ isMapCollapsed ? '展开地图' : '折叠地图' }}</button>
|
<button class="map-toggle-btn bg-blue-600 hover:bg-blue-700 text-white" style="position:absolute;top:10px;right:10px;z-index:1001;border-radius:4px;padding:5px 10px;font-size:12px;font-weight:bold;" (click)="toggleMap()">{{ isMapCollapsed ? '展开地图' : '折叠地图' }}</button>
|
||||||
|
<div id="regionStats" style="position:absolute;top:50px;right:10px;z-index:1002;display:none;">
|
||||||
|
<div style="background:#ffffff; border:1px solid #ddd; border-radius:4px; padding:8px 10px; font-size:12px; color:#111; box-shadow:0 2px 6px rgba(0,0,0,0.08); min-width: 160px;">
|
||||||
|
<div style="font-weight:700; margin-bottom:6px;">区域强回波统计</div>
|
||||||
|
<div style="margin-bottom:4px;">
|
||||||
|
<span style="display:inline-block; width:64px;">风向</span>
|
||||||
|
<span id="statWindDir">--</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:4px;">
|
||||||
|
<span style="display:inline-block; width:64px;">风速</span>
|
||||||
|
<span id="statWindSpd">--</span>
|
||||||
|
</div>
|
||||||
|
<div><span style="display:inline-block; width:64px;">≥30 dBZ</span> <span id="statDbz30">0</span></div>
|
||||||
|
<div><span style="display:inline-block; width:64px;">≥35 dBZ</span> <span id="statDbz35">0</span></div>
|
||||||
|
<div><span style="display:inline-block; width:64px;">≥40 dBZ</span> <span id="statDbz40">0</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="tileValueTooltip" style="position:absolute;pointer-events:none;z-index:1003;display:none;background:rgba(0,0,0,0.65);color:#fff;font-size:12px;padding:4px 6px;border-radius:4px;"></div>
|
<div id="tileValueTooltip" style="position:absolute;pointer-events:none;z-index:1003;display:none;background:rgba(0,0,0,0.65);color:#fff;font-size:12px;padding:4px 6px;border-radius:4px;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private kmlOverlay: any;
|
private kmlOverlay: any;
|
||||||
private CLUSTER_THRESHOLD = 10;
|
private CLUSTER_THRESHOLD = 10;
|
||||||
private tileOverlayGroup: any;
|
private tileOverlayGroup: any;
|
||||||
|
private windOverlayLayer: any;
|
||||||
private tileLastList: any[] = [];
|
private tileLastList: any[] = [];
|
||||||
private refreshTimer: any;
|
private refreshTimer: any;
|
||||||
private mapEventsBound = false;
|
private mapEventsBound = false;
|
||||||
@ -66,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
tileIndex = -1;
|
tileIndex = -1;
|
||||||
tileZ = 7; tileY = 40; tileX = 102;
|
tileZ = 7; tileY = 40; tileX = 102;
|
||||||
tileDt = '';
|
tileDt = '';
|
||||||
tileProduct: 'none'|'radar'|'rain' = 'radar';
|
tileProduct: 'none'|'radar'|'radar_detail'|'rain' = 'radar';
|
||||||
isMapCollapsed = false;
|
isMapCollapsed = false;
|
||||||
kmlInfoTitle = '';
|
kmlInfoTitle = '';
|
||||||
kmlInfoHtml = '';
|
kmlInfoHtml = '';
|
||||||
@ -142,6 +143,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.clusterLayer = new ol.layer.Vector({ source: this.clusterSource, style: (f:any)=> this.createClusterStyle(f) });
|
this.clusterLayer = new ol.layer.Vector({ source: this.clusterSource, style: (f:any)=> this.createClusterStyle(f) });
|
||||||
this.stationLayer = new ol.layer.Vector({ source: this.stationSource, visible: false, style: (f:any)=> this.createStationStyle(f) });
|
this.stationLayer = new ol.layer.Vector({ source: this.stationSource, visible: false, style: (f:any)=> this.createStationStyle(f) });
|
||||||
this.tileOverlayGroup = new ol.layer.Group({ layers: [], zIndex: 999, visible: true });
|
this.tileOverlayGroup = new ol.layer.Group({ layers: [], zIndex: 999, visible: true });
|
||||||
|
this.windOverlayLayer = new ol.layer.Vector({ source: new ol.source.Vector(), zIndex: 1000, visible: true });
|
||||||
|
|
||||||
// Load KML overlay from /static/kml/selected_polygons.kml
|
// Load KML overlay from /static/kml/selected_polygons.kml
|
||||||
try {
|
try {
|
||||||
@ -159,6 +161,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
this.layers.hybrid,
|
this.layers.hybrid,
|
||||||
this.kmlLayer,
|
this.kmlLayer,
|
||||||
this.tileOverlayGroup,
|
this.tileOverlayGroup,
|
||||||
|
this.windOverlayLayer,
|
||||||
this.clusterLayer,
|
this.clusterLayer,
|
||||||
this.stationLayer
|
this.stationLayer
|
||||||
], view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }) });
|
], view: new ol.View({ center: ol.proj.fromLonLat([108, 35]), zoom: 5, minZoom: 3, maxZoom: 18 }) });
|
||||||
@ -314,7 +317,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTileTimes(product: 'radar'|'rain') {
|
async loadTileTimes(product: 'radar'|'rain'|'radar_detail') {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ z: String(this.tileZ), y: String(this.tileY), x: String(this.tileX) });
|
const params = new URLSearchParams({ z: String(this.tileZ), y: String(this.tileY), x: String(this.tileX) });
|
||||||
// 若指定了开始/结束时间,则按时间范围查询;否则按最近limit条
|
// 若指定了开始/结束时间,则按时间范围查询;否则按最近limit条
|
||||||
@ -339,13 +342,22 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
async renderTilesAt(dt: string) {
|
async renderTilesAt(dt: string) {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({ z: String(this.tileZ), dt: dt });
|
const params = new URLSearchParams({ z: String(this.tileZ), dt: dt });
|
||||||
const path = this.tileProduct==='rain' ? '/api/rain/tiles_at' : '/api/radar/tiles_at';
|
const isRain = this.tileProduct === 'rain';
|
||||||
|
const path = isRain ? '/api/rain/tiles_at' : '/api/radar/tiles_at';
|
||||||
const r = await fetch(`${path}?${params.toString()}`);
|
const r = await fetch(`${path}?${params.toString()}`);
|
||||||
if (!r.ok) { this.clearTileOverlays(); return; }
|
if (!r.ok) { this.clearTileOverlays(); return; }
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
const tiles = Array.isArray(j.tiles) ? j.tiles : [];
|
const tiles = Array.isArray(j.tiles) ? j.tiles : [];
|
||||||
if (tiles.length === 0) { this.clearTileOverlays(); return; }
|
if (tiles.length === 0) { this.clearTileOverlays(); this.clearWindOverlays(); return; }
|
||||||
await this.renderTilesOnMap(this.tileProduct, tiles);
|
await this.renderTilesOnMap(isRain ? 'rain' : 'radar', tiles);
|
||||||
|
// 同步 tileIndex 以匹配选择的时次
|
||||||
|
const idx = this.tileTimes.findIndex(t => t === dt);
|
||||||
|
if (idx >= 0) { this.tileIndex = idx; this.tileDt = dt; }
|
||||||
|
if (!isRain && this.tileProduct === 'radar_detail') {
|
||||||
|
this.drawWindOverlays(dt);
|
||||||
|
} else {
|
||||||
|
this.clearWindOverlays();
|
||||||
|
}
|
||||||
} catch { this.clearTileOverlays(); }
|
} catch { this.clearTileOverlays(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +367,149 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
if (coll) coll.clear();
|
if (coll) coll.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearWindOverlays() {
|
||||||
|
const ol: any = (window as any).ol; if (!ol || !this.windOverlayLayer) return;
|
||||||
|
const src = this.windOverlayLayer.getSource();
|
||||||
|
if (src) src.clear();
|
||||||
|
try { const box = document.getElementById('regionStats'); if (box) box.style.display='none'; } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectedStation(): Station | undefined {
|
||||||
|
try {
|
||||||
|
const sid = this.makeStationIdFromHex(this.decimalId || '');
|
||||||
|
if (!sid) return undefined;
|
||||||
|
return this.stations.find(s => s.station_id === sid);
|
||||||
|
} catch { return undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async drawWindOverlays(dtStr: string) {
|
||||||
|
const ol: any = (window as any).ol; if (!ol || !this.map || !this.windOverlayLayer) return;
|
||||||
|
const src = this.windOverlayLayer.getSource(); if (!src) return;
|
||||||
|
src.clear();
|
||||||
|
// 选择中心:优先当前选中站点,否则地图中心
|
||||||
|
let centerLon = 108, centerLat = 35;
|
||||||
|
const st = this.getSelectedStation();
|
||||||
|
if (st && typeof st.longitude==='number' && typeof st.latitude==='number') { centerLon = st.longitude!; centerLat = st.latitude!; }
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
const c = this.map.getView().getCenter();
|
||||||
|
const lonlat = ol.proj.toLonLat(c, this.map.getView().getProjection());
|
||||||
|
centerLon = lonlat[0]; centerLat = lonlat[1];
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
// 调用后端根据经纬度与时间查询最近的 radar_weather 风数据
|
||||||
|
let windDir: number|null = null, windSpd: number|null = null;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({ lat: String(centerLat), lon: String(centerLon), dt: dtStr });
|
||||||
|
const r = await fetch(`/api/radar/weather_nearest?${params.toString()}`);
|
||||||
|
if (r.ok) {
|
||||||
|
const j = await r.json();
|
||||||
|
if (j && j.wind_direction != null) windDir = Number(j.wind_direction);
|
||||||
|
if (j && j.wind_speed != null) windSpd = Number(j.wind_speed);
|
||||||
|
if (isFinite(windDir as any)) windDir = ((windDir as number) % 360 + 360) % 360;
|
||||||
|
if (!isFinite(windSpd as any)) windSpd = null;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
const proj = this.map.getView().getProjection();
|
||||||
|
const center = ol.proj.fromLonLat([centerLon, centerLat], proj);
|
||||||
|
|
||||||
|
// 8km 圆(红色虚线边框)
|
||||||
|
try {
|
||||||
|
const circle = new ol.geom.Circle(center, 8000);
|
||||||
|
const cf = new ol.Feature({ geometry: circle });
|
||||||
|
cf.setStyle(new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(255,0,0,0.95)', width: 2, lineDash: [6,4] }), fill: new ol.style.Fill({ color: 'rgba(255,0,0,0.03)' }) }));
|
||||||
|
src.addFeature(cf);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (windDir != null && windSpd != null && windSpd > 0.01) {
|
||||||
|
const bearingTo = (windDir + 180) % 360; // 去向
|
||||||
|
const hours = 3;
|
||||||
|
const radius = windSpd * 3600 * hours; // m
|
||||||
|
const half = 25; // 半角
|
||||||
|
const pts: number[][] = [];
|
||||||
|
pts.push(center);
|
||||||
|
for (let a = -half; a <= half; a += 2.5) {
|
||||||
|
const ang = (bearingTo + a) * Math.PI / 180;
|
||||||
|
const dx = radius * Math.sin(ang);
|
||||||
|
const dy = radius * Math.cos(ang);
|
||||||
|
pts.push([center[0] + dx, center[1] + dy]);
|
||||||
|
}
|
||||||
|
pts.push(center);
|
||||||
|
const poly = new ol.geom.Polygon([pts]);
|
||||||
|
const pf = new ol.Feature({ geometry: poly });
|
||||||
|
pf.setStyle(new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(255,0,0,0.95)', width: 2, lineDash: [6,4] }), fill: new ol.style.Fill({ color: 'rgba(255,0,0,0.05)' }) }));
|
||||||
|
src.addFeature(pf);
|
||||||
|
|
||||||
|
// 统计扇形区域内的强回波像元数量(基于当前已加载的雷达瓦片)
|
||||||
|
try {
|
||||||
|
const polyLonLat: [number,number][] = pts.map(p => {
|
||||||
|
const lonlat = ol.proj.toLonLat(p, this.map.getView().getProjection());
|
||||||
|
return [lonlat[0], lonlat[1]];
|
||||||
|
});
|
||||||
|
const counts = this.countDbzInPolygon(polyLonLat);
|
||||||
|
this.updateRegionStats(counts, windDir, windSpd);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateRegionStats(counts?: { ge30:number; ge35:number; ge40:number } | null, windDir?: number|null, windSpd?: number|null) {
|
||||||
|
try {
|
||||||
|
const box = document.getElementById('regionStats');
|
||||||
|
const s30 = document.getElementById('statDbz30');
|
||||||
|
const s35 = document.getElementById('statDbz35');
|
||||||
|
const s40 = document.getElementById('statDbz40');
|
||||||
|
const sDir = document.getElementById('statWindDir');
|
||||||
|
const sSpd = document.getElementById('statWindSpd');
|
||||||
|
if (!box || !s30 || !s35 || !s40 || !sDir || !sSpd) return;
|
||||||
|
if (!counts) { box.style.display='none'; return; }
|
||||||
|
s30.textContent = String(counts.ge30);
|
||||||
|
s35.textContent = String(counts.ge35);
|
||||||
|
s40.textContent = String(counts.ge40);
|
||||||
|
if (windDir != null && isFinite(windDir)) { sDir.textContent = String(Math.round(windDir)); } else { sDir.textContent = '--'; }
|
||||||
|
if (windSpd != null && isFinite(windSpd)) { sSpd.textContent = String(Math.round((windSpd as number)*10)/10); } else { sSpd.textContent = '--'; }
|
||||||
|
box.style.display = 'block';
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private countDbzInPolygon(poly: [number,number][]): { ge30:number; ge35:number; ge40:number } | null {
|
||||||
|
// 需要已有雷达瓦片数据
|
||||||
|
const tiles = (this.tileLastList || []).filter(it => it && it.product==='radar');
|
||||||
|
if (!tiles.length) return null;
|
||||||
|
const ptInPoly = (x:number,y:number, polygon:[number,number][]) => {
|
||||||
|
// ray casting
|
||||||
|
let inside = false;
|
||||||
|
for (let i=0, j=polygon.length-1; i<polygon.length; j=i++) {
|
||||||
|
const xi = polygon[i][0], yi = polygon[i][1];
|
||||||
|
const xj = polygon[j][0], yj = polygon[j][1];
|
||||||
|
const intersect = ((yi>y) !== (yj>y)) && (x < (xj - xi) * (y - yi) / ((yj - yi) || 1e-9) + xi);
|
||||||
|
if (intersect) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
};
|
||||||
|
let c30=0, c35=0, c40=0;
|
||||||
|
for (const t of tiles) {
|
||||||
|
const { west, south, east, north, width, height } = t.meta;
|
||||||
|
const dlon = (east - west) / width;
|
||||||
|
const dlat = (north - south) / height;
|
||||||
|
const vals: (number|null)[][] = t.values || [];
|
||||||
|
for (let row=0; row<height; row++) {
|
||||||
|
const lat = south + (row + 0.5) * dlat;
|
||||||
|
const srcRow = vals[row] as (number|null)[];
|
||||||
|
if (!srcRow) continue;
|
||||||
|
for (let col=0; col<width; col++) {
|
||||||
|
const v = srcRow[col];
|
||||||
|
if (v == null || v < 30) continue; // 低于30无需落点判断
|
||||||
|
const lon = west + (col + 0.5) * dlon;
|
||||||
|
if (!ptInPoly(lon, lat, poly)) continue;
|
||||||
|
if (v >= 30) c30++;
|
||||||
|
if (v >= 35) c35++;
|
||||||
|
if (v >= 40) c40++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ge30: c30, ge35: c35, ge40: c40 };
|
||||||
|
}
|
||||||
|
|
||||||
addImageOverlayFromCanvas(canvas: HTMLCanvasElement, extent4326: [number,number,number,number]) {
|
addImageOverlayFromCanvas(canvas: HTMLCanvasElement, extent4326: [number,number,number,number]) {
|
||||||
const ol: any = (window as any).ol; if (!ol || !this.map) return;
|
const ol: any = (window as any).ol; if (!ol || !this.map) return;
|
||||||
const proj = this.map.getView().getProjection();
|
const proj = this.map.getView().getProjection();
|
||||||
|
|||||||
45
core/internal/data/radar_weather.go
Normal file
45
core/internal/data/radar_weather.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RadarWeather struct {
|
||||||
|
Alias string
|
||||||
|
Lat float64
|
||||||
|
Lon float64
|
||||||
|
DT time.Time
|
||||||
|
Temperature sql.NullFloat64
|
||||||
|
Humidity sql.NullFloat64
|
||||||
|
CloudRate sql.NullFloat64
|
||||||
|
Visibility sql.NullFloat64
|
||||||
|
DSWRF sql.NullFloat64
|
||||||
|
WindSpeed sql.NullFloat64
|
||||||
|
WindDirection sql.NullFloat64
|
||||||
|
Pressure sql.NullFloat64
|
||||||
|
}
|
||||||
|
|
||||||
|
// RadarWeatherNearest returns the nearest radar_weather row to (lat,lon) around dt within a window.
|
||||||
|
// It orders by absolute time difference then squared distance.
|
||||||
|
func RadarWeatherNearest(lat, lon float64, dt time.Time, window time.Duration) (*RadarWeather, error) {
|
||||||
|
from := dt.Add(-window)
|
||||||
|
to := dt.Add(window)
|
||||||
|
const q = `
|
||||||
|
SELECT alias, lat, lon, dt, temperature, humidity, cloudrate, visibility, dswrf,
|
||||||
|
wind_speed, wind_direction, pressure
|
||||||
|
FROM radar_weather
|
||||||
|
WHERE dt BETWEEN $1 AND $2
|
||||||
|
ORDER BY ABS(EXTRACT(EPOCH FROM (dt - $3))) ASC,
|
||||||
|
((lat - $4)*(lat - $4) + (lon - $5)*(lon - $5)) ASC
|
||||||
|
LIMIT 1`
|
||||||
|
row := DB().QueryRow(q, from, to, dt, lat, lon)
|
||||||
|
var rw RadarWeather
|
||||||
|
if err := row.Scan(&rw.Alias, &rw.Lat, &rw.Lon, &rw.DT, &rw.Temperature, &rw.Humidity, &rw.CloudRate, &rw.Visibility, &rw.DSWRF, &rw.WindSpeed, &rw.WindDirection, &rw.Pressure); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rw, nil
|
||||||
|
}
|
||||||
69
core/internal/server/radar_weather_handlers.go
Normal file
69
core/internal/server/radar_weather_handlers.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
"weatherstation/core/internal/data"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /api/radar/weather_nearest?lat=..&lon=..&dt=YYYY-MM-DD HH:MM:SS
|
||||||
|
func handleRadarWeatherNearest(c *gin.Context) {
|
||||||
|
latStr := c.Query("lat")
|
||||||
|
lonStr := c.Query("lon")
|
||||||
|
if latStr == "" || lonStr == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing lat/lon"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lat, err1 := strconv.ParseFloat(latStr, 64)
|
||||||
|
lon, err2 := strconv.ParseFloat(lonStr, 64)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid lat/lon"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dtStr := c.Query("dt")
|
||||||
|
loc, _ := time.LoadLocation("Asia/Shanghai")
|
||||||
|
if loc == nil {
|
||||||
|
loc = time.FixedZone("CST", 8*3600)
|
||||||
|
}
|
||||||
|
dt := time.Now().In(loc)
|
||||||
|
if dtStr != "" {
|
||||||
|
if x, err := time.ParseInLocation("2006-01-02 15:04:05", dtStr, loc); err == nil {
|
||||||
|
dt = x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// search window +/- 6h
|
||||||
|
rw, err := data.RadarWeatherNearest(lat, lon, dt, 6*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rw == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"alias": rw.Alias,
|
||||||
|
"lat": rw.Lat,
|
||||||
|
"lon": rw.Lon,
|
||||||
|
"dt": rw.DT.In(loc).Format("2006-01-02 15:04:05"),
|
||||||
|
"temperature": f64(rw.Temperature),
|
||||||
|
"humidity": f64(rw.Humidity),
|
||||||
|
"cloudrate": f64(rw.CloudRate),
|
||||||
|
"visibility": f64(rw.Visibility),
|
||||||
|
"dswrf": f64(rw.DSWRF),
|
||||||
|
"wind_speed": f64(rw.WindSpeed),
|
||||||
|
"wind_direction": f64(rw.WindDirection),
|
||||||
|
"pressure": f64(rw.Pressure),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func f64(v sql.NullFloat64) interface{} {
|
||||||
|
if v.Valid {
|
||||||
|
return v.Float64
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -51,6 +51,7 @@ func NewRouter(opts Options) *gin.Engine {
|
|||||||
api.GET("/forecast/perf", handleForecastPerf)
|
api.GET("/forecast/perf", handleForecastPerf)
|
||||||
api.GET("/radar/times", handleRadarTimes)
|
api.GET("/radar/times", handleRadarTimes)
|
||||||
api.GET("/radar/tiles_at", handleRadarTilesAt)
|
api.GET("/radar/tiles_at", handleRadarTilesAt)
|
||||||
|
api.GET("/radar/weather_nearest", handleRadarWeatherNearest)
|
||||||
api.GET("/rain/times", handleRainTimes)
|
api.GET("/rain/times", handleRainTimes)
|
||||||
api.GET("/rain/tiles_at", handleRainTilesAt)
|
api.GET("/rain/tiles_at", handleRainTilesAt)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user