Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

199 lines
6.2 KiB
TypeScript

/**
* 天气数据库读写层。
*
* - 所有天气查询先查库,命中则 materialize(动态填充日期)后返回标准类型
* - 未命中时由调用方走真实 API,API 返回后回填到库
* - 库中日期字段无意义(daily[0] = 今天,[1] = 明天 …),读取时按 TimeService.now() 重写
*/
import type {
WeatherBundle,
WeatherDaily,
AirQualityForecastDay,
WeatherLibraryEntry,
} from '../types';
import * as TimeService from '../../../os/TimeService';
import { toLocalDateKey } from './dateFormat';
import { useWeatherStore } from '../state';
const DAY_MS = 86_400_000;
const HOUR_MS = 3_600_000;
// ---------------------------------------------------------------------------
// Date materialisation
// ---------------------------------------------------------------------------
function nowIso(): string {
return TimeService.getDate().toISOString();
}
function hourBaseTs(): number {
const d = TimeService.getDate();
d.setMinutes(0, 0, 0);
return d.getTime();
}
export function materializeBundle(raw: WeatherBundle): WeatherBundle {
const now = TimeService.now();
const today = toLocalDateKey(now);
const nowStr = nowIso();
const hBase = hourBaseTs();
return {
now: { ...raw.now, obsTime: nowStr },
daily: raw.daily.map((day, i) => ({
...day,
fxDate: toLocalDateKey(now + i * DAY_MS),
})),
hourly: raw.hourly.map((hour, i) => ({
...hour,
fxTime: TimeService.fromTimestamp(hBase + i * HOUR_MS).toISOString(),
})),
indices: raw.indices.map((idx) => ({ ...idx, date: today })),
warnings: raw.warnings.map((w) => ({
...w,
pubTime: nowStr,
startTime: w.startTime ? nowStr : '',
endTime: w.endTime
? TimeService.fromTimestamp(now + DAY_MS).toISOString()
: '',
})),
airQuality: raw.airQuality
? { ...raw.airQuality, pubTime: nowStr }
: null,
minutely: raw.minutely
? {
summary: raw.minutely.summary,
minutely: raw.minutely.minutely.map((m, i) => ({
...m,
fxTime: TimeService.fromTimestamp(now + i * 5 * 60_000).toISOString(),
})),
}
: null,
};
}
export function materializeHistorical(
raw: WeatherDaily | null | undefined,
): WeatherDaily | null {
if (!raw) return null;
return { ...raw, fxDate: toLocalDateKey(TimeService.now() - DAY_MS) };
}
export function materializeAirForecast(
raw: AirQualityForecastDay[],
): AirQualityForecastDay[] {
const now = TimeService.now();
return raw.map((day, i) => {
const dateKey = toLocalDateKey(now + i * DAY_MS);
return {
...day,
forecastStartTime: `${dateKey}T00:00:00`,
forecastEndTime: `${dateKey}T23:59:59`,
};
});
}
// ---------------------------------------------------------------------------
// Coordinate matching
// ---------------------------------------------------------------------------
function coordsClose(a: string, b: string): boolean {
const [aLon, aLat] = a.split(',').map(Number);
const [bLon, bLat] = b.split(',').map(Number);
if (!Number.isFinite(aLon) || !Number.isFinite(bLon)) return false;
return Math.abs(aLon - bLon) < 0.01 && Math.abs(aLat - bLat) < 0.01;
}
function normalizeLibraryCityId(cityId?: string): string | undefined {
const id = String(cityId ?? '').trim();
return id && id !== 'located' ? id : undefined;
}
type EntryPredicate = (entry: WeatherLibraryEntry) => boolean;
function findCoordinateKey(
library: Record<string, WeatherLibraryEntry>,
lonLat: string,
predicate?: EntryPredicate,
): string | null {
for (const [key, entry] of Object.entries(library)) {
if (
(entry.lonLat === lonLat || coordsClose(entry.lonLat, lonLat)) &&
(!predicate || predicate(entry))
) {
return key;
}
}
return null;
}
function findRawEntry(
lonLat: string,
cityId?: string,
predicate?: EntryPredicate,
): { key: string; entry: WeatherLibraryEntry } | null {
const library = useWeatherStore.getState().weatherLibrary ?? {};
const normalizedCityId = normalizeLibraryCityId(cityId);
if (normalizedCityId) {
const entry = library[normalizedCityId];
if (entry && (!predicate || predicate(entry))) {
return { key: normalizedCityId, entry };
}
}
const key = findCoordinateKey(library, lonLat, predicate);
if (key) return { key, entry: library[key] };
return null;
}
// ---------------------------------------------------------------------------
// Public read API — returns materialised (date-filled) data
// ---------------------------------------------------------------------------
export function findLibraryBundle(lonLat: string, cityId?: string): WeatherBundle | null {
const found = findRawEntry(lonLat, cityId, (entry) => !!entry.bundle);
return found?.entry.bundle ? materializeBundle(found.entry.bundle) : null;
}
export function findLibraryHistorical(lonLat: string, cityId?: string): WeatherDaily | null {
const found = findRawEntry(lonLat, cityId, (entry) => entry.historicalYesterday !== undefined);
return found ? materializeHistorical(found.entry.historicalYesterday) : null;
}
export function findLibraryAirForecast(
lonLat: string,
cityId?: string,
): AirQualityForecastDay[] | null {
const found = findRawEntry(lonLat, cityId, (entry) => !!entry.airQualityForecast);
return found?.entry.airQualityForecast
? materializeAirForecast(found.entry.airQualityForecast)
: null;
}
export function findLibraryLocationName(lonLat: string): string | null {
const found = findRawEntry(lonLat);
return found?.entry.locationName ?? null;
}
// ---------------------------------------------------------------------------
// Public write API — saves raw (un-materialised) data to the store
// ---------------------------------------------------------------------------
export function saveToLibrary(
lonLat: string,
data: Partial<Omit<WeatherLibraryEntry, 'lonLat'>>,
cityId?: string,
): void {
useWeatherStore.setState((prev) => {
const library = prev.weatherLibrary ?? {};
const key = normalizeLibraryCityId(cityId) ?? findCoordinateKey(library, lonLat) ?? lonLat;
const existing = library[key] ?? { lonLat };
return {
...prev,
weatherLibrary: {
...library,
[key]: { ...existing, lonLat, ...data },
},
};
}, true);
}