Add upper-air structure signals to intraday analysis
This commit is contained in:
@@ -932,6 +932,43 @@ export function FutureForecastModal() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{view.front.upperAirSummary ||
|
||||||
|
(view.front.upperAirMetrics?.length || 0) > 0 ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: "var(--text-primary)",
|
||||||
|
fontSize: "0.95rem",
|
||||||
|
fontWeight: 700,
|
||||||
|
marginTop: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{locale === "en-US" ? "Upper-Air Structure" : "高空结构信号"}
|
||||||
|
</div>
|
||||||
|
{view.front.upperAirSummary ? (
|
||||||
|
<div className="future-trend-summary">
|
||||||
|
{view.front.upperAirSummary}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="future-trend-grid">
|
||||||
|
{(view.front.upperAirMetrics || []).map((metric) => (
|
||||||
|
<div key={metric.label} className="future-trend-card">
|
||||||
|
<div className="future-trend-label">{metric.label}</div>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"future-trend-value",
|
||||||
|
metric.tone === "warm" && "warm",
|
||||||
|
metric.tone === "cold" && "cold",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{metric.value}
|
||||||
|
</div>
|
||||||
|
<div className="future-trend-note">{metric.note}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="future-modal-section">
|
<section className="future-modal-section">
|
||||||
|
|||||||
@@ -153,9 +153,15 @@ export interface HourlySeries {
|
|||||||
pressure_msl?: Array<number | null>;
|
pressure_msl?: Array<number | null>;
|
||||||
wind_speed_10m?: Array<number | null>;
|
wind_speed_10m?: Array<number | null>;
|
||||||
wind_direction_10m?: Array<number | null>;
|
wind_direction_10m?: Array<number | null>;
|
||||||
|
wind_speed_180m?: Array<number | null>;
|
||||||
|
wind_direction_180m?: Array<number | null>;
|
||||||
precipitation_probability?: Array<number | null>;
|
precipitation_probability?: Array<number | null>;
|
||||||
cloud_cover?: Array<number | null>;
|
cloud_cover?: Array<number | null>;
|
||||||
radiation?: Array<number | null>;
|
radiation?: Array<number | null>;
|
||||||
|
cape?: Array<number | null>;
|
||||||
|
convective_inhibition?: Array<number | null>;
|
||||||
|
lifted_index?: Array<number | null>;
|
||||||
|
boundary_layer_height?: Array<number | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WeatherGovPeriod {
|
export interface WeatherGovPeriod {
|
||||||
@@ -298,6 +304,22 @@ export interface CityDetail {
|
|||||||
summary?: string | null;
|
summary?: string | null;
|
||||||
notes?: string[] | null;
|
notes?: string[] | null;
|
||||||
};
|
};
|
||||||
|
vertical_profile_signal?: {
|
||||||
|
source?: string | null;
|
||||||
|
window_start?: string | null;
|
||||||
|
window_end?: string | null;
|
||||||
|
cape_max?: number | null;
|
||||||
|
cin_min?: number | null;
|
||||||
|
lifted_index_min?: number | null;
|
||||||
|
boundary_layer_height_max?: number | null;
|
||||||
|
shear_10m_180m_max?: number | null;
|
||||||
|
suppression_risk?: string | null;
|
||||||
|
trigger_risk?: string | null;
|
||||||
|
mixing_strength?: string | null;
|
||||||
|
shear_risk?: string | null;
|
||||||
|
summary_zh?: string | null;
|
||||||
|
summary_en?: string | null;
|
||||||
|
};
|
||||||
ai_analysis?: string | AiAnalysisStructured | null;
|
ai_analysis?: string | AiAnalysisStructured | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
multi_model_daily?: Record<string, DailyModelForecast>;
|
multi_model_daily?: Record<string, DailyModelForecast>;
|
||||||
|
|||||||
@@ -579,6 +579,122 @@ export function computeFrontTrendSignal(
|
|||||||
dateStr: string,
|
dateStr: string,
|
||||||
locale: Locale = "zh-CN",
|
locale: Locale = "zh-CN",
|
||||||
) {
|
) {
|
||||||
|
const upperAirSignal = detail.vertical_profile_signal || {};
|
||||||
|
const upperAirMetrics = upperAirSignal.source
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: isEnglish(locale) ? "Convective suppression" : "对流压温风险",
|
||||||
|
note:
|
||||||
|
upperAirSignal.cape_max != null || upperAirSignal.cin_min != null
|
||||||
|
? isEnglish(locale)
|
||||||
|
? `CAPE max ${Math.round(Number(upperAirSignal.cape_max ?? 0))}, CIN min ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}.`
|
||||||
|
: `CAPE 峰值 ${Math.round(Number(upperAirSignal.cape_max ?? 0))},CIN 最低 ${Number(upperAirSignal.cin_min ?? 0).toFixed(0)}。`
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Derived from the next 48h upper-air profile."
|
||||||
|
: "根据未来 48 小时高空剖面估算。",
|
||||||
|
tone:
|
||||||
|
upperAirSignal.suppression_risk === "high"
|
||||||
|
? "cold"
|
||||||
|
: upperAirSignal.suppression_risk === "low"
|
||||||
|
? "warm"
|
||||||
|
: "",
|
||||||
|
value:
|
||||||
|
upperAirSignal.suppression_risk === "high"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "High"
|
||||||
|
: "高"
|
||||||
|
: upperAirSignal.suppression_risk === "medium"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Medium"
|
||||||
|
: "中"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Low"
|
||||||
|
: "低",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isEnglish(locale) ? "Trigger setup" : "午后触发性",
|
||||||
|
note:
|
||||||
|
upperAirSignal.lifted_index_min != null
|
||||||
|
? isEnglish(locale)
|
||||||
|
? `Lifted index min ${Number(upperAirSignal.lifted_index_min).toFixed(1)}.`
|
||||||
|
: `Lifted Index 最低 ${Number(upperAirSignal.lifted_index_min).toFixed(1)}。`
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Uses instability and lifted-index structure."
|
||||||
|
: "结合不稳定能量与抬升指数判断。",
|
||||||
|
tone:
|
||||||
|
upperAirSignal.trigger_risk === "high"
|
||||||
|
? "cold"
|
||||||
|
: upperAirSignal.trigger_risk === "low"
|
||||||
|
? "warm"
|
||||||
|
: "",
|
||||||
|
value:
|
||||||
|
upperAirSignal.trigger_risk === "high"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "High"
|
||||||
|
: "高"
|
||||||
|
: upperAirSignal.trigger_risk === "medium"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Medium"
|
||||||
|
: "中"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Low"
|
||||||
|
: "低",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isEnglish(locale) ? "Deep mixing" : "深层混合",
|
||||||
|
note:
|
||||||
|
upperAirSignal.boundary_layer_height_max != null
|
||||||
|
? isEnglish(locale)
|
||||||
|
? `Boundary-layer height peaks near ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} m.`
|
||||||
|
: `边界层高度峰值约 ${Math.round(Number(upperAirSignal.boundary_layer_height_max))} 米。`
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Tracks daytime boundary-layer depth."
|
||||||
|
: "跟踪白天边界层混合深度。",
|
||||||
|
tone:
|
||||||
|
upperAirSignal.mixing_strength === "strong"
|
||||||
|
? "warm"
|
||||||
|
: upperAirSignal.mixing_strength === "weak"
|
||||||
|
? "cold"
|
||||||
|
: "",
|
||||||
|
value:
|
||||||
|
upperAirSignal.mixing_strength === "strong"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Strong"
|
||||||
|
: "强"
|
||||||
|
: upperAirSignal.mixing_strength === "medium"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Medium"
|
||||||
|
: "中"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Weak"
|
||||||
|
: "弱",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: isEnglish(locale) ? "Shear proxy" : "高空风切变",
|
||||||
|
note:
|
||||||
|
upperAirSignal.shear_10m_180m_max != null
|
||||||
|
? isEnglish(locale)
|
||||||
|
? `10m-180m shear proxy peaks near ${Number(upperAirSignal.shear_10m_180m_max).toFixed(1)}.`
|
||||||
|
: `10m-180m 风切变代理峰值约 ${Number(upperAirSignal.shear_10m_180m_max).toFixed(1)}。`
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Uses 10m vs 180m wind-vector spread as a simple proxy."
|
||||||
|
: "使用 10m 与 180m 风矢量差做简化代理。",
|
||||||
|
tone: upperAirSignal.shear_risk === "high" ? "cold" : "",
|
||||||
|
value:
|
||||||
|
upperAirSignal.shear_risk === "high"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "High"
|
||||||
|
: "高"
|
||||||
|
: upperAirSignal.shear_risk === "medium"
|
||||||
|
? isEnglish(locale)
|
||||||
|
? "Medium"
|
||||||
|
: "中"
|
||||||
|
: isEnglish(locale)
|
||||||
|
? "Low"
|
||||||
|
: "低",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
const backendSummary =
|
const backendSummary =
|
||||||
dateStr === detail.local_date
|
dateStr === detail.local_date
|
||||||
? String(detail.dynamic_commentary?.summary || "").trim()
|
? String(detail.dynamic_commentary?.summary || "").trim()
|
||||||
@@ -602,6 +718,10 @@ export function computeFrontTrendSignal(
|
|||||||
tone?: string;
|
tone?: string;
|
||||||
value: string;
|
value: string;
|
||||||
}>,
|
}>,
|
||||||
|
upperAirMetrics,
|
||||||
|
upperAirSummary: isEnglish(locale)
|
||||||
|
? String(upperAirSignal.summary_en || "").trim()
|
||||||
|
: String(upperAirSignal.summary_zh || "").trim(),
|
||||||
precipMax: 0,
|
precipMax: 0,
|
||||||
score: 0,
|
score: 0,
|
||||||
summary:
|
summary:
|
||||||
@@ -1083,6 +1203,10 @@ export function computeFrontTrendSignal(
|
|||||||
confidence,
|
confidence,
|
||||||
label,
|
label,
|
||||||
metrics,
|
metrics,
|
||||||
|
upperAirMetrics,
|
||||||
|
upperAirSummary: isEnglish(locale)
|
||||||
|
? String(upperAirSignal.summary_en || "").trim()
|
||||||
|
: String(upperAirSignal.summary_zh || "").trim(),
|
||||||
precipMax,
|
precipMax,
|
||||||
score,
|
score,
|
||||||
summary: backendSummary || summary,
|
summary: backendSummary || summary,
|
||||||
|
|||||||
@@ -187,7 +187,12 @@ class NwsOpenMeteoSourceMixin:
|
|||||||
"latitude": lat,
|
"latitude": lat,
|
||||||
"longitude": lon,
|
"longitude": lon,
|
||||||
"current_weather": "true",
|
"current_weather": "true",
|
||||||
"hourly": "temperature_2m,shortwave_radiation,dew_point_2m,pressure_msl,wind_speed_10m,wind_direction_10m,precipitation_probability,cloud_cover",
|
"hourly": (
|
||||||
|
"temperature_2m,shortwave_radiation,dew_point_2m,pressure_msl,"
|
||||||
|
"wind_speed_10m,wind_direction_10m,wind_speed_180m,wind_direction_180m,"
|
||||||
|
"precipitation_probability,cloud_cover,cape,convective_inhibition,"
|
||||||
|
"lifted_index,boundary_layer_height"
|
||||||
|
),
|
||||||
"daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration",
|
"daily": "temperature_2m_max,apparent_temperature_max,sunrise,sunset,sunshine_duration",
|
||||||
"timezone": "auto",
|
"timezone": "auto",
|
||||||
"forecast_days": forecast_days,
|
"forecast_days": forecast_days,
|
||||||
|
|||||||
@@ -24,6 +24,167 @@ from src.analysis.settlement_rounding import apply_city_settlement
|
|||||||
from src.analysis.metar_narrator import describe_metar_report
|
from src.analysis.metar_narrator import describe_metar_report
|
||||||
from src.data_collection.city_registry import ALIASES
|
from src.data_collection.city_registry import ALIASES
|
||||||
|
|
||||||
|
def _wind_components(speed: Optional[float], direction: Optional[float]) -> tuple[Optional[float], Optional[float]]:
|
||||||
|
if speed is None or direction is None:
|
||||||
|
return None, None
|
||||||
|
try:
|
||||||
|
import math
|
||||||
|
|
||||||
|
rad = math.radians(float(direction))
|
||||||
|
spd = float(speed)
|
||||||
|
u = -spd * math.sin(rad)
|
||||||
|
v = -spd * math.cos(rad)
|
||||||
|
return u, v
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_vertical_profile_signal(
|
||||||
|
hourly_next_48h: Dict[str, list],
|
||||||
|
local_date: str,
|
||||||
|
local_hour: int,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
times = hourly_next_48h.get("times") or []
|
||||||
|
if not times:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
preferred_start = max(local_hour, 12)
|
||||||
|
preferred_end = 19
|
||||||
|
candidate_indexes = [
|
||||||
|
index
|
||||||
|
for index, ts in enumerate(times)
|
||||||
|
if str(ts).startswith(local_date)
|
||||||
|
and preferred_start <= int(str(ts).split("T")[1][:2]) <= preferred_end
|
||||||
|
]
|
||||||
|
if not candidate_indexes:
|
||||||
|
candidate_indexes = [
|
||||||
|
index
|
||||||
|
for index, ts in enumerate(times)
|
||||||
|
if str(ts).startswith(local_date)
|
||||||
|
]
|
||||||
|
if not candidate_indexes:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _series(name: str) -> list:
|
||||||
|
values = hourly_next_48h.get(name) or []
|
||||||
|
return [values[idx] if idx < len(values) else None for idx in candidate_indexes]
|
||||||
|
|
||||||
|
def _max_numeric(values: list) -> Optional[float]:
|
||||||
|
valid = [_sf(value) for value in values if _sf(value) is not None]
|
||||||
|
return max(valid) if valid else None
|
||||||
|
|
||||||
|
def _min_numeric(values: list) -> Optional[float]:
|
||||||
|
valid = [_sf(value) for value in values if _sf(value) is not None]
|
||||||
|
return min(valid) if valid else None
|
||||||
|
|
||||||
|
cape_max = _max_numeric(_series("cape"))
|
||||||
|
cin_min = _min_numeric(_series("convective_inhibition"))
|
||||||
|
lifted_index_min = _min_numeric(_series("lifted_index"))
|
||||||
|
boundary_layer_height_max = _max_numeric(_series("boundary_layer_height"))
|
||||||
|
|
||||||
|
shear_values: list[float] = []
|
||||||
|
speed_10m = hourly_next_48h.get("wind_speed_10m") or []
|
||||||
|
direction_10m = hourly_next_48h.get("wind_direction_10m") or []
|
||||||
|
speed_180m = hourly_next_48h.get("wind_speed_180m") or []
|
||||||
|
direction_180m = hourly_next_48h.get("wind_direction_180m") or []
|
||||||
|
for idx in candidate_indexes:
|
||||||
|
s10 = _sf(speed_10m[idx]) if idx < len(speed_10m) else None
|
||||||
|
d10 = _sf(direction_10m[idx]) if idx < len(direction_10m) else None
|
||||||
|
s180 = _sf(speed_180m[idx]) if idx < len(speed_180m) else None
|
||||||
|
d180 = _sf(direction_180m[idx]) if idx < len(direction_180m) else None
|
||||||
|
u10, v10 = _wind_components(s10, d10)
|
||||||
|
u180, v180 = _wind_components(s180, d180)
|
||||||
|
if None in (u10, v10, u180, v180):
|
||||||
|
continue
|
||||||
|
import math
|
||||||
|
|
||||||
|
shear_values.append(math.sqrt((u180 - u10) ** 2 + (v180 - v10) ** 2))
|
||||||
|
shear_10m_180m_max = max(shear_values) if shear_values else None
|
||||||
|
|
||||||
|
suppression_risk = "low"
|
||||||
|
if (cape_max is not None and cape_max >= 900) or (
|
||||||
|
cin_min is not None and cin_min <= -60
|
||||||
|
):
|
||||||
|
suppression_risk = "high"
|
||||||
|
elif (cape_max is not None and cape_max >= 300) or (
|
||||||
|
cin_min is not None and cin_min <= -20
|
||||||
|
):
|
||||||
|
suppression_risk = "medium"
|
||||||
|
|
||||||
|
trigger_risk = "low"
|
||||||
|
if (
|
||||||
|
cape_max is not None
|
||||||
|
and cape_max >= 800
|
||||||
|
and lifted_index_min is not None
|
||||||
|
and lifted_index_min <= -2
|
||||||
|
):
|
||||||
|
trigger_risk = "high"
|
||||||
|
elif (
|
||||||
|
cape_max is not None
|
||||||
|
and cape_max >= 250
|
||||||
|
and lifted_index_min is not None
|
||||||
|
and lifted_index_min <= 0
|
||||||
|
):
|
||||||
|
trigger_risk = "medium"
|
||||||
|
|
||||||
|
mixing_strength = "weak"
|
||||||
|
if boundary_layer_height_max is not None and boundary_layer_height_max >= 1800:
|
||||||
|
mixing_strength = "strong"
|
||||||
|
elif boundary_layer_height_max is not None and boundary_layer_height_max >= 1000:
|
||||||
|
mixing_strength = "medium"
|
||||||
|
|
||||||
|
shear_risk = "low"
|
||||||
|
if shear_10m_180m_max is not None and shear_10m_180m_max >= 12:
|
||||||
|
shear_risk = "high"
|
||||||
|
elif shear_10m_180m_max is not None and shear_10m_180m_max >= 6:
|
||||||
|
shear_risk = "medium"
|
||||||
|
|
||||||
|
zh_parts = []
|
||||||
|
en_parts = []
|
||||||
|
if suppression_risk == "high":
|
||||||
|
zh_parts.append("午后对流压温风险偏高。")
|
||||||
|
en_parts.append("Afternoon convective suppression risk is elevated.")
|
||||||
|
elif suppression_risk == "medium":
|
||||||
|
zh_parts.append("存在一定云雨压温风险。")
|
||||||
|
en_parts.append("There is some cloud and shower suppression risk.")
|
||||||
|
if mixing_strength == "strong":
|
||||||
|
zh_parts.append("边界层混合较深,若无云雨打断仍有冲高空间。")
|
||||||
|
en_parts.append("Deep boundary-layer mixing still supports additional warming if convection stays limited.")
|
||||||
|
elif mixing_strength == "medium":
|
||||||
|
zh_parts.append("白天混合条件中等。")
|
||||||
|
en_parts.append("Daytime mixing potential is moderate.")
|
||||||
|
if shear_risk == "high":
|
||||||
|
zh_parts.append("高空风切变较强,午后结构波动可能加大。")
|
||||||
|
en_parts.append("Upper-level shear is relatively strong and may increase afternoon volatility.")
|
||||||
|
if trigger_risk == "high":
|
||||||
|
zh_parts.append("抬升触发条件较好,需警惕午后云团发展。")
|
||||||
|
en_parts.append("Trigger conditions are favorable enough to watch for afternoon convective development.")
|
||||||
|
elif trigger_risk == "medium":
|
||||||
|
zh_parts.append("午后具备一定触发条件。")
|
||||||
|
en_parts.append("There is some afternoon trigger potential.")
|
||||||
|
if not zh_parts:
|
||||||
|
zh_parts.append("高空结构整体平稳,暂未看到明显压温信号。")
|
||||||
|
if not en_parts:
|
||||||
|
en_parts.append("The upper-air structure looks fairly stable, without a strong suppression signal yet.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"source": "open-meteo-gfs",
|
||||||
|
"window_start": times[candidate_indexes[0]] if candidate_indexes else None,
|
||||||
|
"window_end": times[candidate_indexes[-1]] if candidate_indexes else None,
|
||||||
|
"cape_max": cape_max,
|
||||||
|
"cin_min": cin_min,
|
||||||
|
"lifted_index_min": lifted_index_min,
|
||||||
|
"boundary_layer_height_max": boundary_layer_height_max,
|
||||||
|
"shear_10m_180m_max": shear_10m_180m_max,
|
||||||
|
"suppression_risk": suppression_risk,
|
||||||
|
"trigger_risk": trigger_risk,
|
||||||
|
"mixing_strength": mixing_strength,
|
||||||
|
"shear_risk": shear_risk,
|
||||||
|
"summary_zh": "".join(zh_parts),
|
||||||
|
"summary_en": " ".join(en_parts),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
"""Fetch, analyse, and return structured weather data for one city."""
|
"""Fetch, analyse, and return structured weather data for one city."""
|
||||||
# Check cache
|
# Check cache
|
||||||
@@ -301,8 +462,14 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
h_pressure = hourly.get("pressure_msl", [])
|
h_pressure = hourly.get("pressure_msl", [])
|
||||||
h_wspd = hourly.get("wind_speed_10m", [])
|
h_wspd = hourly.get("wind_speed_10m", [])
|
||||||
h_wdir = hourly.get("wind_direction_10m", [])
|
h_wdir = hourly.get("wind_direction_10m", [])
|
||||||
|
h_wspd_180m = hourly.get("wind_speed_180m", [])
|
||||||
|
h_wdir_180m = hourly.get("wind_direction_180m", [])
|
||||||
h_precip_prob = hourly.get("precipitation_probability", [])
|
h_precip_prob = hourly.get("precipitation_probability", [])
|
||||||
h_cloud_cover = hourly.get("cloud_cover", [])
|
h_cloud_cover = hourly.get("cloud_cover", [])
|
||||||
|
h_cape = hourly.get("cape", [])
|
||||||
|
h_cin = hourly.get("convective_inhibition", [])
|
||||||
|
h_lifted_index = hourly.get("lifted_index", [])
|
||||||
|
h_boundary_layer_height = hourly.get("boundary_layer_height", [])
|
||||||
if (not h_times or not h_temps) and metar:
|
if (not h_times or not h_temps) and metar:
|
||||||
metar_today_obs = metar.get("today_obs", []) or []
|
metar_today_obs = metar.get("today_obs", []) or []
|
||||||
parsed_obs = []
|
parsed_obs = []
|
||||||
@@ -324,8 +491,14 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
h_pressure = [None for _ in parsed_obs]
|
h_pressure = [None for _ in parsed_obs]
|
||||||
h_wspd = [None for _ in parsed_obs]
|
h_wspd = [None for _ in parsed_obs]
|
||||||
h_wdir = [None for _ in parsed_obs]
|
h_wdir = [None for _ in parsed_obs]
|
||||||
|
h_wspd_180m = [None for _ in parsed_obs]
|
||||||
|
h_wdir_180m = [None for _ in parsed_obs]
|
||||||
h_precip_prob = [None for _ in parsed_obs]
|
h_precip_prob = [None for _ in parsed_obs]
|
||||||
h_cloud_cover = [None for _ in parsed_obs]
|
h_cloud_cover = [None for _ in parsed_obs]
|
||||||
|
h_cape = [None for _ in parsed_obs]
|
||||||
|
h_cin = [None for _ in parsed_obs]
|
||||||
|
h_lifted_index = [None for _ in parsed_obs]
|
||||||
|
h_boundary_layer_height = [None for _ in parsed_obs]
|
||||||
|
|
||||||
peak_hours = []
|
peak_hours = []
|
||||||
if h_times and h_temps and om_today is not None:
|
if h_times and h_temps and om_today is not None:
|
||||||
@@ -422,8 +595,14 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
"pressure_msl": [],
|
"pressure_msl": [],
|
||||||
"wind_speed_10m": [],
|
"wind_speed_10m": [],
|
||||||
"wind_direction_10m": [],
|
"wind_direction_10m": [],
|
||||||
|
"wind_speed_180m": [],
|
||||||
|
"wind_direction_180m": [],
|
||||||
"precipitation_probability": [],
|
"precipitation_probability": [],
|
||||||
"cloud_cover": [],
|
"cloud_cover": [],
|
||||||
|
"cape": [],
|
||||||
|
"convective_inhibition": [],
|
||||||
|
"lifted_index": [],
|
||||||
|
"boundary_layer_height": [],
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
local_anchor = datetime.strptime(
|
local_anchor = datetime.strptime(
|
||||||
@@ -454,12 +633,36 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
next_48h_hourly["wind_direction_10m"].append(
|
next_48h_hourly["wind_direction_10m"].append(
|
||||||
h_wdir[i] if i < len(h_wdir) else None
|
h_wdir[i] if i < len(h_wdir) else None
|
||||||
)
|
)
|
||||||
|
next_48h_hourly["wind_speed_180m"].append(
|
||||||
|
h_wspd_180m[i] if i < len(h_wspd_180m) else None
|
||||||
|
)
|
||||||
|
next_48h_hourly["wind_direction_180m"].append(
|
||||||
|
h_wdir_180m[i] if i < len(h_wdir_180m) else None
|
||||||
|
)
|
||||||
next_48h_hourly["precipitation_probability"].append(
|
next_48h_hourly["precipitation_probability"].append(
|
||||||
h_precip_prob[i] if i < len(h_precip_prob) else None
|
h_precip_prob[i] if i < len(h_precip_prob) else None
|
||||||
)
|
)
|
||||||
next_48h_hourly["cloud_cover"].append(
|
next_48h_hourly["cloud_cover"].append(
|
||||||
h_cloud_cover[i] if i < len(h_cloud_cover) else None
|
h_cloud_cover[i] if i < len(h_cloud_cover) else None
|
||||||
)
|
)
|
||||||
|
next_48h_hourly["cape"].append(
|
||||||
|
h_cape[i] if i < len(h_cape) else None
|
||||||
|
)
|
||||||
|
next_48h_hourly["convective_inhibition"].append(
|
||||||
|
h_cin[i] if i < len(h_cin) else None
|
||||||
|
)
|
||||||
|
next_48h_hourly["lifted_index"].append(
|
||||||
|
h_lifted_index[i] if i < len(h_lifted_index) else None
|
||||||
|
)
|
||||||
|
next_48h_hourly["boundary_layer_height"].append(
|
||||||
|
h_boundary_layer_height[i] if i < len(h_boundary_layer_height) else None
|
||||||
|
)
|
||||||
|
|
||||||
|
vertical_profile_signal = _build_vertical_profile_signal(
|
||||||
|
next_48h_hourly,
|
||||||
|
local_date_str,
|
||||||
|
local_hour,
|
||||||
|
)
|
||||||
|
|
||||||
# ── 13. Cloud description (METAR primary, MGM fallback) ──
|
# ── 13. Cloud description (METAR primary, MGM fallback) ──
|
||||||
clouds = mc.get("clouds", [])
|
clouds = mc.get("clouds", [])
|
||||||
@@ -683,6 +886,7 @@ def _analyze(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
|||||||
"dynamic_commentary": dynamic_commentary,
|
"dynamic_commentary": dynamic_commentary,
|
||||||
"hourly": today_hourly,
|
"hourly": today_hourly,
|
||||||
"hourly_next_48h": next_48h_hourly,
|
"hourly_next_48h": next_48h_hourly,
|
||||||
|
"vertical_profile_signal": vertical_profile_signal,
|
||||||
"metar_today_obs": metar_today_obs_payload,
|
"metar_today_obs": metar_today_obs_payload,
|
||||||
"metar_recent_obs": metar_recent_obs_payload,
|
"metar_recent_obs": metar_recent_obs_payload,
|
||||||
"settlement_today_obs": settlement_today_obs,
|
"settlement_today_obs": settlement_today_obs,
|
||||||
@@ -866,6 +1070,7 @@ def _build_city_detail_payload(
|
|||||||
},
|
},
|
||||||
"probabilities": data.get("probabilities") or {"mu": None, "distribution": []},
|
"probabilities": data.get("probabilities") or {"mu": None, "distribution": []},
|
||||||
"dynamic_commentary": data.get("dynamic_commentary") or {"summary": "", "notes": []},
|
"dynamic_commentary": data.get("dynamic_commentary") or {"summary": "", "notes": []},
|
||||||
|
"vertical_profile_signal": data.get("vertical_profile_signal") or {},
|
||||||
"market_scan": market_scan,
|
"market_scan": market_scan,
|
||||||
"risk": data.get("risk"),
|
"risk": data.get("risk"),
|
||||||
"nearby_source": data.get("nearby_source") or ("mgm" if data.get("name") == "ankara" else "metar_cluster"),
|
"nearby_source": data.get("nearby_source") or ("mgm" if data.get("name") == "ankara" else "metar_cluster"),
|
||||||
|
|||||||
Reference in New Issue
Block a user