feat: implement utility modules and AI-pinned city dashboard components for temperature forecasting
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import type { AirportCurrentConditions, CityDetail } from "@/lib/dashboard-types";
|
||||
|
||||
function normalizeCityKey(value?: string | null) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_-]+/g, "");
|
||||
}
|
||||
|
||||
const KOREAN_RUNWAY_OBSERVATION_CITY_KEYS = new Set(["seoul", "busan"]);
|
||||
|
||||
function hasRunwayObservationPayload(detail: CityDetail) {
|
||||
return Boolean(
|
||||
detail.amos?.runway_temp_range ||
|
||||
(detail.amos?.runway_temps?.length || 0) > 0 ||
|
||||
(detail.amos?.runway_obs?.temperatures?.length || 0) > 0 ||
|
||||
(detail.amos?.runway_obs?.point_temperatures?.length || 0) > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function isRunwayObservationSource(
|
||||
airportPrimary?: AirportCurrentConditions | null,
|
||||
detail?: CityDetail | null,
|
||||
) {
|
||||
const sourceText = [
|
||||
airportPrimary?.source_code,
|
||||
airportPrimary?.source_label,
|
||||
airportPrimary?.station_label,
|
||||
detail?.amos?.source,
|
||||
detail?.amos?.source_label,
|
||||
detail?.amos?.temp_source,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
return (
|
||||
sourceText.includes("amos") ||
|
||||
sourceText.includes("amsc") ||
|
||||
sourceText.includes("awos") ||
|
||||
sourceText.includes("runway") ||
|
||||
sourceText.includes("跑道")
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldSuppressKoreanRunwayObservation(detail?: CityDetail | null) {
|
||||
if (!detail) return false;
|
||||
const cityKey = normalizeCityKey(detail.name) || normalizeCityKey(detail.display_name);
|
||||
if (!KOREAN_RUNWAY_OBSERVATION_CITY_KEYS.has(cityKey)) return false;
|
||||
return isRunwayObservationSource(detail.airport_primary, detail) || hasRunwayObservationPayload(detail);
|
||||
}
|
||||
|
||||
export function getDisplayAirportPrimary(detail?: CityDetail | null) {
|
||||
if (!detail || shouldSuppressKoreanRunwayObservation(detail)) return undefined;
|
||||
return detail.airport_primary;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CityDetail } from "@/lib/dashboard-types";
|
||||
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
import {
|
||||
getNoaaStationCode,
|
||||
@@ -326,6 +327,7 @@ function normalizeObservationTimeForChart(
|
||||
function buildCurrentObservationFallback(
|
||||
detail: CityDetail,
|
||||
): Array<{ time?: string; temp?: number | null; sourceLabel?: string | null }> {
|
||||
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||
const candidates: Array<{
|
||||
sourceLabel?: string | null;
|
||||
temp?: number | null;
|
||||
@@ -337,9 +339,9 @@ function buildCurrentObservationFallback(
|
||||
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: displayAirportPrimary?.source_label || "METAR",
|
||||
temp: displayAirportPrimary?.temp,
|
||||
time: displayAirportPrimary?.obs_time || displayAirportPrimary?.report_time,
|
||||
},
|
||||
{
|
||||
sourceLabel: detail.airport_current?.source_label || "METAR",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { formatTafMarkerType } from "@/lib/taf-utils";
|
||||
import { normalizeHm } from "@/lib/time-utils";
|
||||
import { getWeatherSummary } from "@/lib/weather-summary-utils";
|
||||
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||
|
||||
export { getTemperatureChartData } from "@/lib/chart-utils";
|
||||
export { getModelView, getProbabilityView } from "@/lib/model-utils";
|
||||
@@ -1932,6 +1933,7 @@ function getOfficialObservationCandidates(detail: CityDetail) {
|
||||
|
||||
function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
||||
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
||||
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||
const currentSource = String(
|
||||
detail.current?.settlement_source ||
|
||||
detail.current?.settlement_source_label ||
|
||||
@@ -1940,7 +1942,7 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
||||
const isNmcAirport = String(detail.airport_primary?.source_label || "")
|
||||
const isNmcAirport = String(displayAirportPrimary?.source_label || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.includes("nmc");
|
||||
@@ -1950,8 +1952,8 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
||||
temp: detail.airport_current?.temp,
|
||||
},
|
||||
{
|
||||
sourceLabel: detail.airport_primary?.source_label || "METAR",
|
||||
temp: isNmcAirport ? null : detail.airport_primary?.temp,
|
||||
sourceLabel: displayAirportPrimary?.source_label || "METAR",
|
||||
temp: isNmcAirport ? null : displayAirportPrimary?.temp,
|
||||
},
|
||||
{
|
||||
sourceLabel: detail.current?.settlement_source_label,
|
||||
@@ -1988,6 +1990,7 @@ function getObservedTemperatureProfile(detail: CityDetail, locale: Locale) {
|
||||
function getObservationUpdateProfile(detail: CityDetail, locale: Locale) {
|
||||
const { mgmNearby } = getOfficialObservationCandidates(detail);
|
||||
const mgmFirstRecord = asRecord(mgmNearby[0]);
|
||||
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||
const currentSource = String(
|
||||
detail.current?.settlement_source ||
|
||||
detail.current?.settlement_source_label ||
|
||||
@@ -1998,11 +2001,11 @@ function getObservationUpdateProfile(detail: CityDetail, locale: Locale) {
|
||||
const isNmcCurrent = currentSource === "nmc" || currentSource.includes("nmc");
|
||||
const rawValue = firstNonEmptyString([
|
||||
isNmcCurrent ? "" : detail.current?.obs_time,
|
||||
localObservationTimeCandidate(detail.airport_primary?.obs_time),
|
||||
localObservationTimeCandidate(displayAirportPrimary?.obs_time),
|
||||
localObservationTimeCandidate(detail.airport_current?.obs_time),
|
||||
localObservationTimeCandidate(mgmFirstRecord?.obs_time),
|
||||
localObservationTimeCandidate(mgmFirstRecord?.time),
|
||||
detail.airport_primary?.report_time,
|
||||
displayAirportPrimary?.report_time,
|
||||
detail.airport_current?.report_time,
|
||||
isNmcCurrent ? "" : detail.current?.report_time,
|
||||
mgmFirstRecord?.report_time,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CityDetail } from "@/lib/dashboard-types";
|
||||
import { getDisplayAirportPrimary } from "@/lib/airport-observation-display";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
import {
|
||||
hmToMinutes,
|
||||
@@ -18,10 +19,11 @@ export function getTodayPaceView(
|
||||
const times = hourly.times || [];
|
||||
const temps = hourly.temps || [];
|
||||
if (!times.length || !temps.length) return null;
|
||||
const displayAirportPrimary = getDisplayAirportPrimary(detail);
|
||||
|
||||
const currentMinutes =
|
||||
hmToMinutes(detail.local_time) ??
|
||||
hmToMinutes(detail.airport_primary?.obs_time) ??
|
||||
hmToMinutes(displayAirportPrimary?.obs_time) ??
|
||||
hmToMinutes(detail.airport_current?.obs_time) ??
|
||||
hmToMinutes(detail.current?.obs_time);
|
||||
if (currentMinutes == null) return null;
|
||||
@@ -39,7 +41,7 @@ export function getTodayPaceView(
|
||||
if (expectedNow == null) return null;
|
||||
|
||||
const observedNowCandidate = [
|
||||
detail.airport_primary?.temp,
|
||||
displayAirportPrimary?.temp,
|
||||
detail.airport_current?.temp,
|
||||
detail.current?.temp,
|
||||
]
|
||||
@@ -75,7 +77,7 @@ export function getTodayPaceView(
|
||||
: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`;
|
||||
|
||||
const topObservedCandidate = [
|
||||
detail.airport_primary?.max_so_far,
|
||||
displayAirportPrimary?.max_so_far,
|
||||
detail.airport_current?.max_so_far,
|
||||
detail.current?.max_so_far,
|
||||
observedNow,
|
||||
@@ -105,7 +107,7 @@ export function getTodayPaceView(
|
||||
).padStart(2, "0")}:00`
|
||||
: "--";
|
||||
const observedLabel =
|
||||
detail.airport_primary?.temp != null || detail.airport_current?.temp != null
|
||||
displayAirportPrimary?.temp != null || detail.airport_current?.temp != null
|
||||
? isEnglish(locale)
|
||||
? "Airport obs"
|
||||
: "机场实测"
|
||||
|
||||
Reference in New Issue
Block a user