Restore observed temperatures on map and trend charts
This commit is contained in:
@@ -52,6 +52,26 @@ function getMarkerDisplayOffset(cityName: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function pickMarkerTemperature(
|
||||
snapshot?: Pick<CityDetail, "current" | "temp_symbol"> | CitySummary,
|
||||
) {
|
||||
if (!snapshot) return null;
|
||||
const detail = snapshot as Partial<CityDetail>;
|
||||
const candidates = [
|
||||
snapshot.current?.temp,
|
||||
detail.airport_primary?.temp,
|
||||
detail.airport_current?.temp,
|
||||
detail.center_station_candidate?.temp,
|
||||
detail.official_nearby?.[0]?.temp,
|
||||
detail.mgm_nearby?.[0]?.temp,
|
||||
];
|
||||
for (const value of candidates) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric)) return numeric;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createMarkerIcon(
|
||||
city: CityListItem,
|
||||
snapshot?: Pick<CityDetail, "current" | "temp_symbol"> | CitySummary,
|
||||
@@ -60,8 +80,8 @@ function createMarkerIcon(
|
||||
const label = city.display_name;
|
||||
const unit = city.temp_unit === "fahrenheit" ? "°F" : "°C";
|
||||
const shortName = label.length > 10 ? `${label.substring(0, 8)}...` : label;
|
||||
const tempText =
|
||||
snapshot?.current?.temp != null ? `${snapshot.current.temp}${unit}` : "--";
|
||||
const markerTemp = pickMarkerTemperature(snapshot);
|
||||
const tempText = markerTemp != null ? `${markerTemp}${unit}` : "--";
|
||||
const offset = getMarkerDisplayOffset(city.name);
|
||||
const styleAttr =
|
||||
offset.x || offset.y
|
||||
@@ -91,7 +111,7 @@ function getMarkerSignature(
|
||||
city.temp_unit,
|
||||
city.lat,
|
||||
city.lon,
|
||||
snapshot?.current?.temp ?? "",
|
||||
pickMarkerTemperature(snapshot) ?? "",
|
||||
].join("|");
|
||||
}
|
||||
|
||||
|
||||
@@ -323,6 +323,78 @@ function sortObservationItemsByTime<T extends { time?: string | null }>(items: T
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeObservationTimeForChart(
|
||||
value: unknown,
|
||||
detail: CityDetail,
|
||||
) {
|
||||
const raw = String(value || "").trim();
|
||||
if (raw && !raw.includes("T")) {
|
||||
return normalizeHm(raw) || raw;
|
||||
}
|
||||
return normalizeHm(detail.local_time) || normalizeHm(raw) || raw;
|
||||
}
|
||||
|
||||
function buildCurrentObservationFallback(
|
||||
detail: CityDetail,
|
||||
): Array<{ time?: string; temp?: number | null; sourceLabel?: string | null }> {
|
||||
const candidates: Array<{
|
||||
sourceLabel?: string | null;
|
||||
temp?: number | null;
|
||||
time?: string | null;
|
||||
}> = [
|
||||
{
|
||||
sourceLabel: detail.current?.settlement_source_label,
|
||||
temp: detail.current?.temp,
|
||||
time: detail.current?.obs_time || detail.current?.report_time,
|
||||
},
|
||||
{
|
||||
sourceLabel: detail.airport_primary?.source_label || "METAR",
|
||||
temp: detail.airport_primary?.temp,
|
||||
time: detail.airport_primary?.obs_time || detail.airport_primary?.report_time,
|
||||
},
|
||||
{
|
||||
sourceLabel: detail.airport_current?.source_label || "METAR",
|
||||
temp: detail.airport_current?.temp,
|
||||
time: detail.airport_current?.obs_time || detail.airport_current?.report_time,
|
||||
},
|
||||
{
|
||||
sourceLabel:
|
||||
detail.center_station_candidate?.source_label ||
|
||||
detail.center_station_candidate?.source_code,
|
||||
temp: detail.center_station_candidate?.temp,
|
||||
time: String((detail.center_station_candidate as Record<string, unknown> | null | undefined)?.obs_time || ""),
|
||||
},
|
||||
{
|
||||
sourceLabel:
|
||||
detail.official_nearby?.[0]?.source_label ||
|
||||
detail.official_nearby?.[0]?.source_code,
|
||||
temp: detail.official_nearby?.[0]?.temp,
|
||||
time: String((detail.official_nearby?.[0] as Record<string, unknown> | null | undefined)?.obs_time || ""),
|
||||
},
|
||||
{
|
||||
sourceLabel:
|
||||
detail.mgm_nearby?.[0]?.source_label ||
|
||||
detail.mgm_nearby?.[0]?.source_code,
|
||||
temp: detail.mgm_nearby?.[0]?.temp,
|
||||
time: String((detail.mgm_nearby?.[0] as Record<string, unknown> | null | undefined)?.obs_time || ""),
|
||||
},
|
||||
];
|
||||
|
||||
const first = candidates.find((item) => {
|
||||
const numeric = Number(item.temp);
|
||||
return Number.isFinite(numeric);
|
||||
});
|
||||
if (!first) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
sourceLabel: first.sourceLabel,
|
||||
temp: Number(first.temp),
|
||||
time: normalizeObservationTimeForChart(first.time, detail),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function interpolateSeriesAtMinutes(
|
||||
times: string[],
|
||||
values: Array<number | null | undefined>,
|
||||
@@ -623,9 +695,19 @@ export function getTemperatureChartData(
|
||||
? [{ time: detail.current.obs_time, temp: detail.current.temp }]
|
||||
: []
|
||||
: [];
|
||||
const currentObservationFallback = buildCurrentObservationFallback(detail);
|
||||
const metarObservationSource = detail.metar_today_obs?.length
|
||||
? detail.metar_today_obs
|
||||
: detail.trend?.recent || [];
|
||||
: detail.trend?.recent?.length
|
||||
? detail.trend.recent
|
||||
: currentObservationFallback;
|
||||
const usingCurrentObservationFallback =
|
||||
!detail.metar_today_obs?.length &&
|
||||
!detail.trend?.recent?.length &&
|
||||
currentObservationFallback.length > 0;
|
||||
const currentFallbackTag =
|
||||
currentObservationFallback[0]?.sourceLabel ||
|
||||
getObservationSourceTag(detail);
|
||||
const allowMetarFallback = settlementSource && observationCode !== "hko";
|
||||
const shouldUseMetarFallback =
|
||||
allowMetarFallback &&
|
||||
@@ -644,7 +726,9 @@ export function getTemperatureChartData(
|
||||
return `${icao} METAR`;
|
||||
})();
|
||||
const observationDisplayTag =
|
||||
observationCode === "wunderground"
|
||||
usingCurrentObservationFallback
|
||||
? String(currentFallbackTag).toUpperCase()
|
||||
: observationCode === "wunderground"
|
||||
? metarFallbackTag
|
||||
: useSettlementObservationSource && shouldUseMetarFallback
|
||||
? metarFallbackTag
|
||||
|
||||
+82
-6
@@ -30,6 +30,7 @@ from src.analysis.deb_algorithm import calculate_dynamic_weights
|
||||
from src.analysis.settlement_rounding import apply_city_settlement
|
||||
from src.data_collection.country_networks import build_country_network_snapshot
|
||||
from src.data_collection.city_registry import ALIASES, CITY_REGISTRY
|
||||
from src.data_collection.nmc_sources import NMC_CITY_REFERENCES
|
||||
from src.models.lgbm_daily_high import predict_lgbm_daily_high
|
||||
|
||||
TURKISH_MGM_CITIES = {"ankara", "istanbul"}
|
||||
@@ -52,6 +53,39 @@ _GROQ_COMMENTARY_CACHE_TTL_SEC = int(
|
||||
)
|
||||
|
||||
|
||||
def _format_observation_time_local(value: Any, utc_offset: int) -> str:
|
||||
raw = str(value or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if "T" in raw:
|
||||
try:
|
||||
dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone(timedelta(seconds=utc_offset))).strftime("%H:%M")
|
||||
except Exception:
|
||||
pass
|
||||
match = re.search(r"(\d{1,2}):(\d{2})", raw)
|
||||
if match:
|
||||
return f"{int(match.group(1)):02d}:{match.group(2)}"
|
||||
return raw[:16]
|
||||
|
||||
|
||||
def _fetch_nmc_current_fallback(city: str, *, use_fahrenheit: bool) -> Dict[str, Any]:
|
||||
city_key = str(city or "").strip().lower()
|
||||
if city_key not in NMC_CITY_REFERENCES:
|
||||
return {}
|
||||
try:
|
||||
payload = _weather.fetch_nmc_region_current(
|
||||
city_key,
|
||||
use_fahrenheit=use_fahrenheit,
|
||||
)
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception as exc:
|
||||
logger.debug("NMC current fallback failed city={}: {}", city_key, exc)
|
||||
return {}
|
||||
|
||||
|
||||
def _record_analysis_cache_event(*, city: str, hit: bool, force_refresh: bool) -> None:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
with _ANALYSIS_CACHE_STATS_LOCK:
|
||||
@@ -1176,17 +1210,33 @@ def _analyze(
|
||||
sc_cur = settlement_current.get("current", {}) if settlement_current else {}
|
||||
use_settlement_current = settlement_source in {"hko", "cwa", "noaa", "wunderground"} and bool(sc_cur)
|
||||
primary_current = sc_cur if use_settlement_current else mc
|
||||
current_source = settlement_source
|
||||
current_source_label = settlement_source_label
|
||||
current_station_code = settlement_current.get("station_code")
|
||||
current_station_name = settlement_current.get("station_name")
|
||||
cur_temp = _sf(primary_current.get("temp"))
|
||||
if cur_temp is None:
|
||||
cur_temp = _sf(mc.get("temp"))
|
||||
if cur_temp is None:
|
||||
cur_temp = _sf(mg_cur.get("temp"))
|
||||
if cur_temp is None:
|
||||
nmc_fallback = _fetch_nmc_current_fallback(city, use_fahrenheit=is_f)
|
||||
nmc_cur = nmc_fallback.get("current") or {}
|
||||
nmc_temp = _sf(nmc_cur.get("temp"))
|
||||
if nmc_temp is not None:
|
||||
cur_temp = nmc_temp
|
||||
current_source = "nmc"
|
||||
current_source_label = "NMC"
|
||||
current_station_code = nmc_fallback.get("station_code")
|
||||
current_station_name = nmc_fallback.get("station_name")
|
||||
|
||||
max_so_far = _sf(primary_current.get("max_temp_so_far"))
|
||||
if max_so_far is None:
|
||||
max_so_far = _sf(mc.get("max_temp_so_far"))
|
||||
if max_so_far is None:
|
||||
max_so_far = _sf(mg_cur.get("mgm_max_temp"))
|
||||
if max_so_far is None:
|
||||
max_so_far = cur_temp
|
||||
|
||||
max_temp_time = primary_current.get("max_temp_time")
|
||||
if not max_temp_time and not use_settlement_current:
|
||||
@@ -1237,6 +1287,12 @@ def _analyze(
|
||||
)
|
||||
except Exception:
|
||||
obs_time_str = str(obs_t)[:16]
|
||||
if not obs_time_str and current_source == "nmc":
|
||||
nmc_fallback = _fetch_nmc_current_fallback(city, use_fahrenheit=is_f)
|
||||
obs_time_str = _format_observation_time_local(
|
||||
nmc_fallback.get("publish_time") or nmc_fallback.get("timestamp"),
|
||||
int(utc_offset or 0),
|
||||
)
|
||||
|
||||
settlement_today_obs = []
|
||||
if use_settlement_current:
|
||||
@@ -1826,10 +1882,10 @@ def _analyze(
|
||||
"max_temp_time": max_temp_time,
|
||||
"raw_max_so_far": raw_settlement_max,
|
||||
"wu_settlement": wu_settle,
|
||||
"settlement_source": settlement_source,
|
||||
"settlement_source_label": settlement_source_label,
|
||||
"station_code": settlement_current.get("station_code"),
|
||||
"station_name": settlement_current.get("station_name"),
|
||||
"settlement_source": current_source,
|
||||
"settlement_source_label": current_source_label,
|
||||
"station_code": current_station_code,
|
||||
"station_name": current_station_name,
|
||||
"obs_time": obs_time_str,
|
||||
"obs_age_min": None if use_settlement_current else metar_age_min,
|
||||
"report_time": primary_current.get("report_time"),
|
||||
@@ -2033,17 +2089,30 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
use_settlement_current = settlement_source in {"hko", "cwa", "noaa", "wunderground"} and bool(sc_cur)
|
||||
primary_current = sc_cur if use_settlement_current else mc
|
||||
|
||||
current_source = settlement_source
|
||||
current_source_label = settlement_source_label
|
||||
nmc_fallback: Dict[str, Any] = {}
|
||||
cur_temp = _sf(primary_current.get("temp"))
|
||||
if cur_temp is None:
|
||||
cur_temp = _sf(mc.get("temp"))
|
||||
if cur_temp is None:
|
||||
cur_temp = _sf(mg_cur.get("temp"))
|
||||
if cur_temp is None:
|
||||
nmc_fallback = _fetch_nmc_current_fallback(city, use_fahrenheit=is_f)
|
||||
nmc_cur = nmc_fallback.get("current") or {}
|
||||
nmc_temp = _sf(nmc_cur.get("temp"))
|
||||
if nmc_temp is not None:
|
||||
cur_temp = nmc_temp
|
||||
current_source = "nmc"
|
||||
current_source_label = "NMC"
|
||||
|
||||
max_so_far = _sf(primary_current.get("max_temp_so_far"))
|
||||
if max_so_far is None:
|
||||
max_so_far = _sf(mc.get("max_temp_so_far"))
|
||||
if max_so_far is None:
|
||||
max_so_far = _sf(mg_cur.get("mgm_max_temp"))
|
||||
if max_so_far is None:
|
||||
max_so_far = cur_temp
|
||||
|
||||
max_temp_time = primary_current.get("max_temp_time")
|
||||
if not max_temp_time and not use_settlement_current:
|
||||
@@ -2084,6 +2153,13 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
)
|
||||
except Exception:
|
||||
obs_time_str = str(obs_t)[:16]
|
||||
if not obs_time_str and current_source == "nmc":
|
||||
if not nmc_fallback:
|
||||
nmc_fallback = _fetch_nmc_current_fallback(city, use_fahrenheit=is_f)
|
||||
obs_time_str = _format_observation_time_local(
|
||||
nmc_fallback.get("publish_time") or nmc_fallback.get("timestamp"),
|
||||
int(utc_offset or 0),
|
||||
)
|
||||
|
||||
om_daily = (open_meteo.get("daily") or {}) if isinstance(open_meteo, dict) else {}
|
||||
om_hourly = (open_meteo.get("hourly") or {}) if isinstance(open_meteo, dict) else {}
|
||||
@@ -2200,8 +2276,8 @@ def _analyze_summary(city: str, force_refresh: bool = False) -> Dict[str, Any]:
|
||||
"max_so_far": _sf(display_settlement_max),
|
||||
"max_temp_time": max_temp_time,
|
||||
"wu_settlement": _sf(wu_settle),
|
||||
"settlement_source": settlement_source,
|
||||
"settlement_source_label": settlement_source_label,
|
||||
"settlement_source": current_source,
|
||||
"settlement_source_label": current_source_label,
|
||||
"obs_time": obs_time_str or None,
|
||||
"obs_age_min": obs_age_min,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user