重构跑道观测系统:全跑道展示、结算跑道标注、热力模型、风场分析
This commit is contained in:
@@ -62,6 +62,25 @@ def _amsc_split_runway_pair(label: str) -> tuple[str, str]:
|
||||
return runway, runway
|
||||
|
||||
|
||||
def _amsc_wind_dir(*candidates: Any) -> Optional[int]:
|
||||
"""Parse wind direction from AMSC fields (degrees)."""
|
||||
for value in candidates:
|
||||
parsed = _amsc_safe_float(value)
|
||||
if parsed is not None and 0 <= parsed <= 360:
|
||||
return int(round(parsed))
|
||||
return None
|
||||
|
||||
|
||||
def _amsc_parse_rvr(value: Any) -> Optional[int]:
|
||||
"""Parse RVR field like 'P2000' → 2000 (meters)."""
|
||||
text = str(value or "").strip().upper()
|
||||
if not text or text in {"--", "-", "NULL", "NONE"}:
|
||||
return None
|
||||
text = text.lstrip("PM")
|
||||
parsed = _amsc_safe_float(text)
|
||||
return int(round(parsed)) if parsed is not None else None
|
||||
|
||||
|
||||
def _amsc_parse_utc_time(value: Any) -> tuple[Optional[str], Optional[str]]:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
@@ -124,12 +143,44 @@ def _amsc_parse_wind_plate_payload(
|
||||
best_temp = tdz if tdz is not None else max(points)
|
||||
runway_temps.append((best_temp, None))
|
||||
valid_values.extend(points)
|
||||
|
||||
# Wind: prefer TDZ point, fallback to END
|
||||
wind_dir = _amsc_wind_dir(
|
||||
raw_row.get("TDZ_WIND_D10"),
|
||||
raw_row.get("END_WIND_D10"),
|
||||
)
|
||||
wind_speed = _amsc_safe_float(
|
||||
raw_row.get("TDZ_WIND_F10") or raw_row.get("END_WIND_F10")
|
||||
)
|
||||
# RVR / MOR: prefer 1-min average
|
||||
raw_rvr = (
|
||||
raw_row.get("TDZ_RVR_1A") or raw_row.get("END_RVR_1A")
|
||||
or raw_row.get("TDZ_RVR_10A") or raw_row.get("END_RVR_10A")
|
||||
)
|
||||
rvr = _amsc_parse_rvr(raw_rvr)
|
||||
raw_mor = (
|
||||
raw_row.get("TDZ_MOR_1A") or raw_row.get("END_MOR_1A")
|
||||
or raw_row.get("TDZ_MOR_10A") or raw_row.get("END_MOR_10A")
|
||||
)
|
||||
mor = _amsc_safe_float(raw_mor)
|
||||
# Humidity: prefer TDZ, fallback to END, then MID
|
||||
humidity = _amsc_safe_float(
|
||||
raw_row.get("TDZ_HUMID") or raw_row.get("END_HUMID") or raw_row.get("MID_HUMID")
|
||||
)
|
||||
|
||||
target_max = max(points)
|
||||
point_temperatures.append(
|
||||
{
|
||||
"runway": runway_label,
|
||||
"tdz_temp": tdz,
|
||||
"mid_temp": mid,
|
||||
"end_temp": end,
|
||||
"target_runway_max": target_max,
|
||||
"wind_dir": wind_dir,
|
||||
"wind_speed": wind_speed,
|
||||
"rvr": rvr,
|
||||
"mor": mor,
|
||||
"humidity": humidity,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -155,6 +206,8 @@ def _amsc_parse_wind_plate_payload(
|
||||
"station_label": airport_meta.get("label") or icao,
|
||||
"raw_metar": raw_metar,
|
||||
"raw_taf": None,
|
||||
"wind_dir": point_temperatures[0].get("wind_dir") if point_temperatures else None,
|
||||
"wind_speed": point_temperatures[0].get("wind_speed") if point_temperatures else None,
|
||||
"runway_obs": {
|
||||
"runway_pairs": runway_pairs,
|
||||
"temperatures": runway_temps,
|
||||
|
||||
@@ -1090,26 +1090,47 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour
|
||||
# Reuse the existing `amos` detail shape consumed by dashboard runway panels.
|
||||
results["amos"] = amsc_data
|
||||
try:
|
||||
icao = amsc_data.get("icao") or ""
|
||||
obs_time = amsc_data.get("observation_time") or datetime.now().isoformat()
|
||||
DBManager().append_airport_obs(
|
||||
icao=amsc_data.get("icao") or "",
|
||||
icao=icao,
|
||||
city=city_lower,
|
||||
temp_c=amsc_data.get("temp_c"),
|
||||
wind_kt=amsc_data.get("wind_kt"),
|
||||
wind_kt=amsc_data.get("wind_speed"),
|
||||
pressure_hpa=amsc_data.get("pressure_hpa"),
|
||||
obs_time=amsc_data.get("observation_time") or datetime.now().isoformat(),
|
||||
obs_time=obs_time,
|
||||
)
|
||||
runway_obs = amsc_data.get("runway_obs") or {}
|
||||
rw_pairs = runway_obs.get("runway_pairs") or []
|
||||
rw_temps = runway_obs.get("temperatures") or []
|
||||
point_temps = runway_obs.get("point_temperatures") or []
|
||||
for i, (pair, temp_pair) in enumerate(zip(rw_pairs, rw_temps)):
|
||||
t = temp_pair[0] if temp_pair else None
|
||||
if t is not None and i < 6:
|
||||
pair_label = "/".join(pair) if isinstance(pair, (list, tuple)) else str(pair)
|
||||
# Legacy airport_obs_log (keep backward compat)
|
||||
DBManager().append_airport_obs(
|
||||
icao=f"{amsc_data.get('icao', '')}_RWY_{i}",
|
||||
icao=f"{icao}_RWY_{i}",
|
||||
city=city_lower,
|
||||
temp_c=t,
|
||||
obs_time=amsc_data.get("observation_time") or datetime.now().isoformat(),
|
||||
obs_time=obs_time,
|
||||
)
|
||||
# New runway_obs_log with full detail
|
||||
pt = point_temps[i] if i < len(point_temps) else {}
|
||||
DBManager().append_runway_obs(
|
||||
icao=icao,
|
||||
city=city_lower,
|
||||
runway=pair_label,
|
||||
tdz_temp=pt.get("tdz_temp"),
|
||||
mid_temp=pt.get("mid_temp"),
|
||||
end_temp=pt.get("end_temp"),
|
||||
target_runway_max=pt.get("target_runway_max"),
|
||||
wind_dir=pt.get("wind_dir"),
|
||||
wind_speed=pt.get("wind_speed"),
|
||||
rvr=pt.get("rvr"),
|
||||
mor=pt.get("mor"),
|
||||
humidity=pt.get("humidity"),
|
||||
otime_utc=obs_time,
|
||||
)
|
||||
logger.debug("AMSC AWOS stored runway row city={} runway={} temp={}", city_lower, pair_label, t)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user