Load nearby stations before opening intraday analysis
This commit is contained in:
@@ -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 &&
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user