Update intraday TAF summaries for active and peak windows
This commit is contained in:
+206
-111
@@ -795,14 +795,206 @@ export function computeFrontTrendSignal(
|
|||||||
? String(tafSignal.summary_en || "").trim()
|
? String(tafSignal.summary_en || "").trim()
|
||||||
: String(tafSignal.summary_zh || "").trim()
|
: String(tafSignal.summary_zh || "").trim()
|
||||||
: "";
|
: "";
|
||||||
const upperAirSummary = [baseUpperAirSummary, tafSummary]
|
let upperAirSummary = baseUpperAirSummary;
|
||||||
|
let tafMetric:
|
||||||
|
| {
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
tone?: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
|
let upperAirMetrics: Array<{
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
tone?: string;
|
||||||
|
value: string;
|
||||||
|
}> = [];
|
||||||
|
const rawBackendSummary =
|
||||||
|
dateStr === detail.local_date
|
||||||
|
? String(detail.dynamic_commentary?.summary || "").trim()
|
||||||
|
: "";
|
||||||
|
const backendSummary =
|
||||||
|
rawBackendSummary &&
|
||||||
|
(!isEnglish(locale) || !containsCjk(rawBackendSummary))
|
||||||
|
? rawBackendSummary
|
||||||
|
: "";
|
||||||
|
const rawBackendNotes = Array.isArray(detail.dynamic_commentary?.notes)
|
||||||
|
? detail.dynamic_commentary?.notes
|
||||||
|
?.map((item) => String(item || "").trim())
|
||||||
|
.filter(Boolean) || []
|
||||||
|
: [];
|
||||||
|
const backendNotes = rawBackendNotes.filter(
|
||||||
|
(note) => !isEnglish(locale) || !containsCjk(note),
|
||||||
|
);
|
||||||
|
const slice = getFutureSlice(detail, dateStr);
|
||||||
|
const currentTemp = Number(detail.current?.temp);
|
||||||
|
const currentDew = Number(detail.current?.dewpoint);
|
||||||
|
|
||||||
|
if (!slice.length) {
|
||||||
|
return {
|
||||||
|
confidence: "low",
|
||||||
|
label: isEnglish(locale) ? "Monitoring" : "监控中",
|
||||||
|
metrics: [] as Array<{
|
||||||
|
label: string;
|
||||||
|
note: string;
|
||||||
|
tone?: string;
|
||||||
|
value: string;
|
||||||
|
}>,
|
||||||
|
upperAirMetrics,
|
||||||
|
upperAirSummary,
|
||||||
|
precipMax: 0,
|
||||||
|
score: 0,
|
||||||
|
summary:
|
||||||
|
backendSummary ||
|
||||||
|
(isEnglish(locale)
|
||||||
|
? "Insufficient intraday structured data. Keep baseline monitoring."
|
||||||
|
: "当日日内结构化数据不足,暂时只保留基础监控。"),
|
||||||
|
weatherGovPeriods: [] as ReturnType<typeof getForecastTextForDate>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeHm = (value: unknown): string | null => {
|
||||||
|
const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
|
||||||
|
if (!match) return null;
|
||||||
|
const hour = Number.parseInt(match[1], 10);
|
||||||
|
const minute = Number.parseInt(match[2], 10);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(hour) ||
|
||||||
|
!Number.isFinite(minute) ||
|
||||||
|
hour < 0 ||
|
||||||
|
hour > 23 ||
|
||||||
|
minute < 0 ||
|
||||||
|
minute > 59
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
const hmToMinutes = (value: string | null) => {
|
||||||
|
if (!value) return null;
|
||||||
|
const [hourPart, minutePart] = value.split(":");
|
||||||
|
const hour = Number.parseInt(hourPart || "", 10);
|
||||||
|
const minute = Number.parseInt(minutePart || "", 10);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(hour) ||
|
||||||
|
!Number.isFinite(minute) ||
|
||||||
|
hour < 0 ||
|
||||||
|
hour > 23 ||
|
||||||
|
minute < 0 ||
|
||||||
|
minute > 59
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return hour * 60 + minute;
|
||||||
|
};
|
||||||
|
const pointMinutes = (point: { label?: string }) =>
|
||||||
|
hmToMinutes(normalizeHm(point.label));
|
||||||
|
|
||||||
|
const isTargetToday = dateStr === detail.local_date;
|
||||||
|
const currentHm = normalizeHm(detail.local_time);
|
||||||
|
const sunsetHm = normalizeHm(detail.forecast?.sunset);
|
||||||
|
const peakFirstHour = Number(detail.peak?.first_h);
|
||||||
|
const peakLastHour = Number(detail.peak?.last_h);
|
||||||
|
const hasPeakWindow =
|
||||||
|
Number.isFinite(peakFirstHour) &&
|
||||||
|
Number.isFinite(peakLastHour) &&
|
||||||
|
peakFirstHour >= 0 &&
|
||||||
|
peakLastHour >= peakFirstHour;
|
||||||
|
const currentMinutes = hmToMinutes(currentHm);
|
||||||
|
const sunsetMinutes = hmToMinutes(sunsetHm);
|
||||||
|
const peakWindowStartMinutes = hasPeakWindow
|
||||||
|
? Math.max(0, (peakFirstHour - 2) * 60)
|
||||||
|
: null;
|
||||||
|
const peakWindowEndMinutes = hasPeakWindow
|
||||||
|
? Math.min(23 * 60 + 59, (peakLastHour + 1) * 60)
|
||||||
|
: null;
|
||||||
|
const canUseSunsetWindow =
|
||||||
|
isTargetToday &&
|
||||||
|
currentMinutes !== null &&
|
||||||
|
sunsetMinutes !== null &&
|
||||||
|
sunsetMinutes > currentMinutes;
|
||||||
|
const tafMarkers = Array.isArray(tafSignal.markers) ? tafSignal.markers : [];
|
||||||
|
const markerSummary = (
|
||||||
|
marker:
|
||||||
|
| {
|
||||||
|
summary_zh?: string | null;
|
||||||
|
summary_en?: string | null;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
|
) =>
|
||||||
|
!marker
|
||||||
|
? ""
|
||||||
|
: isEnglish(locale)
|
||||||
|
? String(marker.summary_en || "").trim()
|
||||||
|
: String(marker.summary_zh || "").trim();
|
||||||
|
const currentTafMarker =
|
||||||
|
tafSignal.available && currentMinutes !== null
|
||||||
|
? tafMarkers.find((marker) => {
|
||||||
|
const start = hmToMinutes(normalizeHm(marker?.start_local));
|
||||||
|
const end = hmToMinutes(normalizeHm(marker?.end_local));
|
||||||
|
if (start === null || end === null) return false;
|
||||||
|
return currentMinutes >= start && currentMinutes <= end;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const nextTafMarker =
|
||||||
|
tafSignal.available && currentMinutes !== null && !currentTafMarker
|
||||||
|
? tafMarkers.find((marker) => {
|
||||||
|
const start = hmToMinutes(normalizeHm(marker?.start_local));
|
||||||
|
return start !== null && start > currentMinutes;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const peakWindowTafMarker =
|
||||||
|
tafSignal.available &&
|
||||||
|
peakWindowStartMinutes !== null &&
|
||||||
|
peakWindowEndMinutes !== null
|
||||||
|
? tafMarkers.find((marker) => {
|
||||||
|
const start = hmToMinutes(normalizeHm(marker?.start_local));
|
||||||
|
const end = hmToMinutes(normalizeHm(marker?.end_local));
|
||||||
|
if (start === null || end === null) return false;
|
||||||
|
return start <= peakWindowEndMinutes && end >= peakWindowStartMinutes;
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const currentTafSummary = markerSummary(currentTafMarker);
|
||||||
|
const peakTafSummary = markerSummary(peakWindowTafMarker);
|
||||||
|
const nextTafSummary = markerSummary(nextTafMarker);
|
||||||
|
const tafTimelineSummary =
|
||||||
|
tafSignal.available && dateStr === detail.local_date
|
||||||
|
? (() => {
|
||||||
|
if (currentTafMarker && currentTafSummary) {
|
||||||
|
const range = `${normalizeHm(currentTafMarker.start_local) || "--:--"}-${normalizeHm(currentTafMarker.end_local) || "--:--"}`;
|
||||||
|
return isEnglish(locale)
|
||||||
|
? `Current active TAF segment (${range}): ${currentTafSummary}`
|
||||||
|
: `当前生效的 TAF 时段(${range}):${currentTafSummary}`;
|
||||||
|
}
|
||||||
|
if (nextTafMarker && nextTafSummary) {
|
||||||
|
const range = `${normalizeHm(nextTafMarker.start_local) || "--:--"}-${normalizeHm(nextTafMarker.end_local) || "--:--"}`;
|
||||||
|
return isEnglish(locale)
|
||||||
|
? `Next TAF segment (${range}): ${nextTafSummary}`
|
||||||
|
: `下一段 TAF 时段(${range}):${nextTafSummary}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})()
|
||||||
|
: "";
|
||||||
|
const effectivePeakTafSummary =
|
||||||
|
peakTafSummary && peakTafSummary !== currentTafSummary ? peakTafSummary : "";
|
||||||
|
upperAirSummary = [
|
||||||
|
baseUpperAirSummary,
|
||||||
|
tafTimelineSummary,
|
||||||
|
effectivePeakTafSummary,
|
||||||
|
tafSummary,
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isEnglish(locale) ? " " : "");
|
.join(isEnglish(locale) ? " " : "");
|
||||||
const tafMetric =
|
tafMetric =
|
||||||
tafSignal.available && dateStr === detail.local_date
|
tafSignal.available && dateStr === detail.local_date
|
||||||
? {
|
? {
|
||||||
label: isEnglish(locale) ? "Airport TAF" : "机场预报",
|
label: isEnglish(locale) ? "Airport TAF" : "机场预报",
|
||||||
note: tafSummary ||
|
note:
|
||||||
|
tafTimelineSummary ||
|
||||||
|
effectivePeakTafSummary ||
|
||||||
|
tafSummary ||
|
||||||
(isEnglish(locale)
|
(isEnglish(locale)
|
||||||
? "Airport TAF is available for the current peak window."
|
? "Airport TAF is available for the current peak window."
|
||||||
: "当前峰值窗口已接入机场 TAF 预报。"),
|
: "当前峰值窗口已接入机场 TAF 预报。"),
|
||||||
@@ -826,7 +1018,7 @@ export function computeFrontTrendSignal(
|
|||||||
: "暂稳",
|
: "暂稳",
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
const upperAirMetrics = upperAirSignal.source
|
upperAirMetrics = upperAirSignal.source
|
||||||
? [
|
? [
|
||||||
...(upperAirTradeCue ? [upperAirTradeCue] : []),
|
...(upperAirTradeCue ? [upperAirTradeCue] : []),
|
||||||
{
|
{
|
||||||
@@ -946,118 +1138,14 @@ export function computeFrontTrendSignal(
|
|||||||
? "Medium"
|
? "Medium"
|
||||||
: "中"
|
: "中"
|
||||||
: isEnglish(locale)
|
: isEnglish(locale)
|
||||||
? "Weak"
|
? "Weak"
|
||||||
: "弱",
|
: "弱",
|
||||||
},
|
},
|
||||||
...(tafMetric ? [tafMetric] : []),
|
...(tafMetric ? [tafMetric] : []),
|
||||||
]
|
]
|
||||||
: tafMetric
|
: tafMetric
|
||||||
? [tafMetric]
|
? [tafMetric]
|
||||||
: [];
|
: [];
|
||||||
const rawBackendSummary =
|
|
||||||
dateStr === detail.local_date
|
|
||||||
? String(detail.dynamic_commentary?.summary || "").trim()
|
|
||||||
: "";
|
|
||||||
const backendSummary =
|
|
||||||
rawBackendSummary &&
|
|
||||||
(!isEnglish(locale) || !containsCjk(rawBackendSummary))
|
|
||||||
? rawBackendSummary
|
|
||||||
: "";
|
|
||||||
const rawBackendNotes = Array.isArray(detail.dynamic_commentary?.notes)
|
|
||||||
? detail.dynamic_commentary?.notes
|
|
||||||
?.map((item) => String(item || "").trim())
|
|
||||||
.filter(Boolean) || []
|
|
||||||
: [];
|
|
||||||
const backendNotes = rawBackendNotes.filter(
|
|
||||||
(note) => !isEnglish(locale) || !containsCjk(note),
|
|
||||||
);
|
|
||||||
const slice = getFutureSlice(detail, dateStr);
|
|
||||||
const currentTemp = Number(detail.current?.temp);
|
|
||||||
const currentDew = Number(detail.current?.dewpoint);
|
|
||||||
|
|
||||||
if (!slice.length) {
|
|
||||||
return {
|
|
||||||
confidence: "low",
|
|
||||||
label: isEnglish(locale) ? "Monitoring" : "监控中",
|
|
||||||
metrics: [] as Array<{
|
|
||||||
label: string;
|
|
||||||
note: string;
|
|
||||||
tone?: string;
|
|
||||||
value: string;
|
|
||||||
}>,
|
|
||||||
upperAirMetrics,
|
|
||||||
upperAirSummary,
|
|
||||||
precipMax: 0,
|
|
||||||
score: 0,
|
|
||||||
summary:
|
|
||||||
backendSummary ||
|
|
||||||
(isEnglish(locale)
|
|
||||||
? "Insufficient intraday structured data. Keep baseline monitoring."
|
|
||||||
: "当日日内结构化数据不足,暂时只保留基础监控。"),
|
|
||||||
weatherGovPeriods: [] as ReturnType<typeof getForecastTextForDate>,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeHm = (value: unknown): string | null => {
|
|
||||||
const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
|
|
||||||
if (!match) return null;
|
|
||||||
const hour = Number.parseInt(match[1], 10);
|
|
||||||
const minute = Number.parseInt(match[2], 10);
|
|
||||||
if (
|
|
||||||
!Number.isFinite(hour) ||
|
|
||||||
!Number.isFinite(minute) ||
|
|
||||||
hour < 0 ||
|
|
||||||
hour > 23 ||
|
|
||||||
minute < 0 ||
|
|
||||||
minute > 59
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
|
||||||
};
|
|
||||||
const hmToMinutes = (value: string | null) => {
|
|
||||||
if (!value) return null;
|
|
||||||
const [hourPart, minutePart] = value.split(":");
|
|
||||||
const hour = Number.parseInt(hourPart || "", 10);
|
|
||||||
const minute = Number.parseInt(minutePart || "", 10);
|
|
||||||
if (
|
|
||||||
!Number.isFinite(hour) ||
|
|
||||||
!Number.isFinite(minute) ||
|
|
||||||
hour < 0 ||
|
|
||||||
hour > 23 ||
|
|
||||||
minute < 0 ||
|
|
||||||
minute > 59
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return hour * 60 + minute;
|
|
||||||
};
|
|
||||||
const pointMinutes = (point: { label?: string }) =>
|
|
||||||
hmToMinutes(normalizeHm(point.label));
|
|
||||||
|
|
||||||
const isTargetToday = dateStr === detail.local_date;
|
|
||||||
const currentHm = normalizeHm(detail.local_time);
|
|
||||||
const sunsetHm = normalizeHm(detail.forecast?.sunset);
|
|
||||||
const peakFirstHour = Number(detail.peak?.first_h);
|
|
||||||
const peakLastHour = Number(detail.peak?.last_h);
|
|
||||||
const hasPeakWindow =
|
|
||||||
Number.isFinite(peakFirstHour) &&
|
|
||||||
Number.isFinite(peakLastHour) &&
|
|
||||||
peakFirstHour >= 0 &&
|
|
||||||
peakLastHour >= peakFirstHour;
|
|
||||||
const currentMinutes = hmToMinutes(currentHm);
|
|
||||||
const sunsetMinutes = hmToMinutes(sunsetHm);
|
|
||||||
const peakWindowStartMinutes = hasPeakWindow
|
|
||||||
? Math.max(0, (peakFirstHour - 2) * 60)
|
|
||||||
: null;
|
|
||||||
const peakWindowEndMinutes = hasPeakWindow
|
|
||||||
? Math.min(23 * 60 + 59, (peakLastHour + 1) * 60)
|
|
||||||
: null;
|
|
||||||
const canUseSunsetWindow =
|
|
||||||
isTargetToday &&
|
|
||||||
currentMinutes !== null &&
|
|
||||||
sunsetMinutes !== null &&
|
|
||||||
sunsetMinutes > currentMinutes;
|
|
||||||
|
|
||||||
const futureSlice =
|
const futureSlice =
|
||||||
isTargetToday && currentMinutes !== null
|
isTargetToday && currentMinutes !== null
|
||||||
@@ -1359,7 +1447,14 @@ export function computeFrontTrendSignal(
|
|||||||
: "";
|
: "";
|
||||||
const backendSupplement =
|
const backendSupplement =
|
||||||
backendSummary && backendSummary !== summary ? backendSummary : "";
|
backendSummary && backendSummary !== summary ? backendSummary : "";
|
||||||
const combinedSummary = [summary, tafSummary, tafContrastSummary, backendSupplement]
|
const combinedSummary = [
|
||||||
|
summary,
|
||||||
|
tafTimelineSummary,
|
||||||
|
effectivePeakTafSummary,
|
||||||
|
tafSummary,
|
||||||
|
tafContrastSummary,
|
||||||
|
backendSupplement,
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(isEnglish(locale) ? " " : "");
|
.join(isEnglish(locale) ? " " : "");
|
||||||
const cloudNote = (() => {
|
const cloudNote = (() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user