Load nearby stations before opening intraday analysis

This commit is contained in:
2569718930@qq.com
2026-04-12 20:49:51 +08:00
parent a0adf05d1e
commit a27ae564fb
6 changed files with 64 additions and 24 deletions
+16 -4
View File
@@ -33,7 +33,7 @@ interface DashboardStoreValue extends DashboardState {
ensureCityDetail: ( ensureCityDetail: (
cityName: string, cityName: string,
force?: boolean, force?: boolean,
depth?: "panel" | "full", depth?: "panel" | "nearby" | "full",
) => Promise<CityDetail>; ) => Promise<CityDetail>;
futureModalDate: string | null; futureModalDate: string | null;
loadCities: () => Promise<void>; loadCities: () => Promise<void>;
@@ -125,7 +125,7 @@ const EAGER_SUMMARY_PRIORITY_CITY_ORDER = [
"milan", "milan",
"madrid", "madrid",
] as const; ] as const;
type CityDetailDepth = "panel" | "full"; type CityDetailDepth = "panel" | "nearby" | "full";
function countAvailableModels( function countAvailableModels(
detail?: CityDetail | null, detail?: CityDetail | null,
@@ -165,7 +165,9 @@ function hasSparseDetailCoverage(
} }
function normalizeDetailDepth(detail?: CityDetail | null): CityDetailDepth { function normalizeDetailDepth(detail?: CityDetail | null): CityDetailDepth {
return detail?.detail_depth === "panel" ? "panel" : "full"; if (detail?.detail_depth === "nearby") return "nearby";
if (detail?.detail_depth === "panel") return "panel";
return "full";
} }
function detailSatisfiesDepth( function detailSatisfiesDepth(
@@ -174,9 +176,17 @@ function detailSatisfiesDepth(
) { ) {
if (!detail) return false; if (!detail) return false;
if (depth === "panel") return true; if (depth === "panel") return true;
if (depth === "nearby") {
const normalized = normalizeDetailDepth(detail);
return normalized === "nearby" || normalized === "full";
}
return normalizeDetailDepth(detail) === "full"; return normalizeDetailDepth(detail) === "full";
} }
function shouldCheckSparseCoverageForDepth(depth: CityDetailDepth) {
return depth === "panel" || depth === "full";
}
function getStoredSelectedCityName(cities: CityListItem[]) { function getStoredSelectedCityName(cities: CityListItem[]) {
if (typeof window === "undefined") return null; if (typeof window === "undefined") return null;
const stored = String( const stored = String(
@@ -413,7 +423,9 @@ export function DashboardStoreProvider({
const cached = cityDetailsByName[cityName]; const cached = cityDetailsByName[cityName];
const cachedMeta = cityDetailMetaByName[cityName]; const cachedMeta = cityDetailMetaByName[cityName];
const hasRequestedDepth = detailSatisfiesDepth(cached, depth); const hasRequestedDepth = detailSatisfiesDepth(cached, depth);
const cachedIsSparse = hasSparseDetailCoverage(cached, cached?.local_date); const cachedIsSparse =
shouldCheckSparseCoverageForDepth(depth) &&
hasSparseDetailCoverage(cached, cached?.local_date);
if ( if (
!force && !force &&
cached && cached &&
+14 -6
View File
@@ -18,6 +18,7 @@ interface UseLeafletMapArgs {
onEnsureCityDetail: ( onEnsureCityDetail: (
cityName: string, cityName: string,
force?: boolean, force?: boolean,
depth?: "panel" | "nearby" | "full",
) => Promise<CityDetail>; ) => Promise<CityDetail>;
onMapInteractionChange: (active: boolean) => void; onMapInteractionChange: (active: boolean) => void;
onRegisterStopMotion: (stopMotion: () => void) => void; onRegisterStopMotion: (stopMotion: () => void) => void;
@@ -512,9 +513,12 @@ export function useLeafletMap({
handlingAutoNearbyRef.current = true; handlingAutoNearbyRef.current = true;
try { try {
if (selectedDetail) { if (selectedDetail) {
// Just render stations, no camera move from here const selectedNearbyStations = pickMapNearbyStations(selectedDetail);
renderNearbyStations(selectedDetail, true); if (selectedNearbyStations.length) {
return; // Just render stations, no camera move from here
renderNearbyStations(selectedDetail, true);
return;
}
} }
if (suspendMotion) { if (suspendMotion) {
@@ -543,7 +547,7 @@ export function useLeafletMap({
} }
} }
const targetCity = best?.cityName || null; const targetCity = selectedCity || best?.cityName || null;
if (!targetCity) { if (!targetCity) {
autoNearbyCityRef.current = null; autoNearbyCityRef.current = null;
layer.clearLayers(); layer.clearLayers();
@@ -559,7 +563,7 @@ export function useLeafletMap({
autoNearbyCityRef.current = targetCity; autoNearbyCityRef.current = targetCity;
const cachedDetail = cityDetailsByName[targetCity]; const cachedDetail = cityDetailsByName[targetCity];
if (cachedDetail) { if (cachedDetail && pickMapNearbyStations(cachedDetail).length) {
renderNearbyStations(cachedDetail, true); renderNearbyStations(cachedDetail, true);
return; return;
} }
@@ -567,7 +571,11 @@ export function useLeafletMap({
if (loadingAutoNearbyRef.current) return; if (loadingAutoNearbyRef.current) return;
loadingAutoNearbyRef.current = true; loadingAutoNearbyRef.current = true;
try { try {
const detail = await onEnsureCityDetailRef.current(targetCity, false); const detail = await onEnsureCityDetailRef.current(
targetCity,
false,
"nearby",
);
renderNearbyStations(detail, true); renderNearbyStations(detail, true);
} catch { } catch {
} finally { } finally {
+5 -3
View File
@@ -29,8 +29,10 @@ function normalizeCityName(cityName: string) {
return encodeURIComponent(String(cityName).replace(/\s/g, "-")); return encodeURIComponent(String(cityName).replace(/\s/g, "-"));
} }
function normalizeDetailDepth(depth?: "panel" | "full") { function normalizeDetailDepth(depth?: "panel" | "nearby" | "full") {
return depth === "full" ? "full" : "panel"; if (depth === "full") return "full";
if (depth === "nearby") return "nearby";
return "panel";
} }
async function fetchJson<T>(url: string): Promise<T> { async function fetchJson<T>(url: string): Promise<T> {
@@ -161,7 +163,7 @@ export const dashboardClient = {
async getCityDetail( async getCityDetail(
cityName: string, cityName: string,
options?: { force?: boolean; depth?: "panel" | "full" }, options?: { force?: boolean; depth?: "panel" | "nearby" | "full" },
) { ) {
const force = options?.force ?? false; const force = options?.force ?? false;
const depth = normalizeDetailDepth(options?.depth); const depth = normalizeDetailDepth(options?.depth);
+1 -1
View File
@@ -327,7 +327,7 @@ export interface AiAnalysisStructured {
export interface CityDetail { export interface CityDetail {
name: string; name: string;
display_name: string; display_name: string;
detail_depth?: "panel" | "full"; detail_depth?: "panel" | "nearby" | "full";
lat: number; lat: number;
lon: number; lon: number;
temp_symbol: string; temp_symbol: string;
+22 -9
View File
@@ -85,14 +85,20 @@ def _analysis_ttl_for_city(city: str) -> int:
def _analysis_cache_key(city: str, detail_mode: str = "full") -> str: def _analysis_cache_key(city: str, detail_mode: str = "full") -> str:
normalized_mode = "panel" if str(detail_mode or "").strip().lower() == "panel" else "full" normalized_raw = str(detail_mode or "").strip().lower()
if normalized_raw == "panel":
normalized_mode = "panel"
elif normalized_raw == "nearby":
normalized_mode = "nearby"
else:
normalized_mode = "full"
return f"{city}::{normalized_mode}" return f"{city}::{normalized_mode}"
def _get_cached_analysis( def _get_cached_analysis(
city: str, city: str,
ttl: int, ttl: int,
detail_modes: tuple[str, ...] = ("panel", "full"), detail_modes: tuple[str, ...] = ("panel", "nearby", "full"),
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
now_ts = _time.time() now_ts = _time.time()
freshest_payload: Optional[Dict[str, Any]] = None freshest_payload: Optional[Dict[str, Any]] = None
@@ -1085,7 +1091,13 @@ def _analyze(
"""Fetch, analyse, and return structured weather data for one city.""" """Fetch, analyse, and return structured weather data for one city."""
# Check cache # Check cache
ttl = CACHE_TTL_ANKARA if city.lower() in TURKISH_MGM_CITIES else CACHE_TTL ttl = CACHE_TTL_ANKARA if city.lower() in TURKISH_MGM_CITIES else CACHE_TTL
normalized_detail_mode = "panel" if str(detail_mode or "full").strip().lower() == "panel" else "full" normalized_detail_mode_raw = str(detail_mode or "full").strip().lower()
if normalized_detail_mode_raw == "panel":
normalized_detail_mode = "panel"
elif normalized_detail_mode_raw == "nearby":
normalized_detail_mode = "nearby"
else:
normalized_detail_mode = "full"
cache_key = _analysis_cache_key(city, normalized_detail_mode) cache_key = _analysis_cache_key(city, normalized_detail_mode)
if not force_refresh: if not force_refresh:
@@ -1114,16 +1126,17 @@ def _analyze(
# ── 1. Fetch raw data ── # ── 1. Fetch raw data ──
is_panel_mode = normalized_detail_mode == "panel" is_panel_mode = normalized_detail_mode == "panel"
is_nearby_mode = normalized_detail_mode == "nearby"
raw = _weather.fetch_all_sources( raw = _weather.fetch_all_sources(
city, city,
lat=lat, lat=lat,
lon=lon, lon=lon,
force_refresh=force_refresh, force_refresh=force_refresh,
include_taf=not is_panel_mode, include_taf=not is_panel_mode and not is_nearby_mode,
include_nearby=not is_panel_mode, include_nearby=not is_panel_mode,
include_ensemble=not is_panel_mode, include_ensemble=not is_panel_mode and not is_nearby_mode,
include_multi_model=not is_panel_mode, include_multi_model=not is_panel_mode and not is_nearby_mode,
) )
om = raw.get("open-meteo", {}) om = raw.get("open-meteo", {})
metar = raw.get("metar", {}) metar = raw.get("metar", {})
@@ -1615,7 +1628,7 @@ def _analyze(
first_peak_h, first_peak_h,
last_peak_h, last_peak_h,
) )
if not is_panel_mode if not is_panel_mode and not is_nearby_mode
else {} else {}
) )
taf_signal = ( taf_signal = (
@@ -1627,7 +1640,7 @@ def _analyze(
first_peak_h, first_peak_h,
last_peak_h, last_peak_h,
) )
if not is_panel_mode if not is_panel_mode and not is_nearby_mode
else {"available": False} else {"available": False}
) )
@@ -1777,7 +1790,7 @@ def _analyze(
# ── Assemble result ── # ── Assemble result ──
city_meta = CITIES.get(city, {}) or {} city_meta = CITIES.get(city, {}) or {}
result = { result = {
"detail_depth": "panel" if is_panel_mode else "full", "detail_depth": "panel" if is_panel_mode else "nearby" if is_nearby_mode else "full",
"name": city, "name": city,
"display_name": str(city_meta.get("display_name") or city_meta.get("name") or city.title()), "display_name": str(city_meta.get("display_name") or city_meta.get("name") or city.title()),
"lat": lat, "lat": lat,
+6 -1
View File
@@ -435,7 +435,12 @@ async def city_detail(
_assert_entitlement(request) _assert_entitlement(request)
city = _normalize_city_or_404(name) city = _normalize_city_or_404(name)
normalized_depth = str(depth or "panel").strip().lower() normalized_depth = str(depth or "panel").strip().lower()
detail_mode = "full" if normalized_depth == "full" else "panel" if normalized_depth == "full":
detail_mode = "full"
elif normalized_depth == "nearby":
detail_mode = "nearby"
else:
detail_mode = "panel"
return await run_in_threadpool(_analyze, city, force_refresh, False, detail_mode) return await run_in_threadpool(_analyze, city, force_refresh, False, detail_mode)