Clarify current and peak-window TAF chart markers
This commit is contained in:
@@ -305,6 +305,30 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
||||
});
|
||||
}
|
||||
if ((todayChartData.tafMarkers || []).length > 0) {
|
||||
datasets.push({
|
||||
backgroundColor: "#f59e0b",
|
||||
borderColor: "#f59e0b",
|
||||
borderWidth: 0,
|
||||
data: todayChartData.datasets.tafCurrentMarkerPoints,
|
||||
fill: false,
|
||||
label: locale === "en-US" ? "Current TAF" : "当前 TAF",
|
||||
order: -3,
|
||||
pointHoverRadius: 8,
|
||||
pointRadius: 6,
|
||||
showLine: false,
|
||||
});
|
||||
datasets.push({
|
||||
backgroundColor: "rgba(250, 204, 21, 0.72)",
|
||||
borderColor: "rgba(250, 204, 21, 0.72)",
|
||||
borderWidth: 0,
|
||||
data: todayChartData.datasets.tafPeakWindowMarkerPoints,
|
||||
fill: false,
|
||||
label: locale === "en-US" ? "Peak-window TAF" : "峰值窗口 TAF",
|
||||
order: -2,
|
||||
pointHoverRadius: 7,
|
||||
pointRadius: 4,
|
||||
showLine: false,
|
||||
});
|
||||
datasets.push({
|
||||
backgroundColor: "#f59e0b",
|
||||
borderColor: "#f59e0b",
|
||||
@@ -312,9 +336,9 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
||||
data: todayChartData.datasets.tafMarkerPoints,
|
||||
fill: false,
|
||||
label: locale === "en-US" ? "TAF Timing" : "TAF 时段",
|
||||
order: -2,
|
||||
pointHoverRadius: 8,
|
||||
pointRadius: 5,
|
||||
order: -4,
|
||||
pointHoverRadius: 0,
|
||||
pointRadius: 0,
|
||||
showLine: false,
|
||||
});
|
||||
}
|
||||
@@ -334,6 +358,7 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
||||
filter: (legendItem, chartData) => {
|
||||
const text = String(legendItem.text || "");
|
||||
if (!text) return false;
|
||||
if (text === "TAF Timing" || text === "TAF 时段") return false;
|
||||
if (!text.includes("DEB")) return true;
|
||||
|
||||
const firstDebIndex = (chartData.datasets || []).findIndex(
|
||||
@@ -351,12 +376,33 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
||||
callbacks: {
|
||||
label: (ctx) => {
|
||||
const label = String(ctx.dataset.label || "");
|
||||
if (label === "TAF Timing" || label === "TAF 时段") {
|
||||
if (
|
||||
label === "TAF Timing" ||
|
||||
label === "TAF 时段" ||
|
||||
label === "Current TAF" ||
|
||||
label === "当前 TAF" ||
|
||||
label === "Peak-window TAF" ||
|
||||
label === "峰值窗口 TAF"
|
||||
) {
|
||||
const marker = (todayChartData.tafMarkers || []).find(
|
||||
(item) => item.index === ctx.dataIndex,
|
||||
);
|
||||
if (!marker) return label;
|
||||
return `${label}: ${String(marker.summary || "").replace(
|
||||
const prefix =
|
||||
marker.isCurrent && marker.isPeakWindow
|
||||
? locale === "en-US"
|
||||
? "Current / peak-window TAF"
|
||||
: "当前 / 峰值窗口 TAF"
|
||||
: marker.isCurrent
|
||||
? locale === "en-US"
|
||||
? "Current TAF"
|
||||
: "当前 TAF"
|
||||
: marker.isPeakWindow
|
||||
? locale === "en-US"
|
||||
? "Peak-window TAF"
|
||||
: "峰值窗口 TAF"
|
||||
: label;
|
||||
return `${prefix}: ${String(marker.summary || "").replace(
|
||||
marker.markerType,
|
||||
marker.displayType || marker.markerType,
|
||||
)}`;
|
||||
|
||||
+170
-29
@@ -374,8 +374,70 @@ export function getTemperatureChartData(
|
||||
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
||||
? detail.taf?.signal?.markers || []
|
||||
: [];
|
||||
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 currentMinutes = hmToMinutes(normalizeHm(detail.local_time));
|
||||
const peakFirstHour = Number(detail.peak?.first_h);
|
||||
const peakLastHour = Number(detail.peak?.last_h);
|
||||
const peakWindowStartMinutes =
|
||||
Number.isFinite(peakFirstHour) && peakFirstHour >= 0
|
||||
? Math.max(0, (peakFirstHour - 2) * 60)
|
||||
: null;
|
||||
const peakWindowEndMinutes =
|
||||
Number.isFinite(peakLastHour) && peakLastHour >= peakFirstHour
|
||||
? Math.min(23 * 60 + 59, (peakLastHour + 1) * 60)
|
||||
: null;
|
||||
const tafMarkerValue = max - 0.4;
|
||||
const tafMarkerPoints = new Array(times.length).fill(null);
|
||||
const tafCurrentMarkerPoints = new Array(times.length).fill(null);
|
||||
const tafPeakWindowMarkerPoints = new Array(times.length).fill(null);
|
||||
const sameMarker = (
|
||||
left:
|
||||
| { markerType?: string | null; startLocal?: string | null; endLocal?: string | null }
|
||||
| null
|
||||
| undefined,
|
||||
right:
|
||||
| { markerType?: string | null; startLocal?: string | null; endLocal?: string | null }
|
||||
| null
|
||||
| undefined,
|
||||
) =>
|
||||
!!left &&
|
||||
!!right &&
|
||||
String(left.markerType || "") === String(right.markerType || "") &&
|
||||
String(left.startLocal || "") === String(right.startLocal || "") &&
|
||||
String(left.endLocal || "") === String(right.endLocal || "");
|
||||
const tafMarkers = tafMarkersRaw
|
||||
.map((marker) => {
|
||||
const labelTime = String(marker?.label_time || "").trim();
|
||||
@@ -397,10 +459,63 @@ export function getTemperatureChartData(
|
||||
isEnglish(locale)
|
||||
? String(marker?.summary_en || "").trim()
|
||||
: String(marker?.summary_zh || "").trim(),
|
||||
isCurrent: false,
|
||||
isPeakWindow: false,
|
||||
suppressionLevel: String(marker?.suppression_level || "").trim(),
|
||||
};
|
||||
})
|
||||
.filter((marker) => marker.index >= 0);
|
||||
const currentTafMarker =
|
||||
currentMinutes !== null
|
||||
? tafMarkers.find((marker) => {
|
||||
const start = hmToMinutes(normalizeHm(marker.startLocal));
|
||||
const end = hmToMinutes(normalizeHm(marker.endLocal));
|
||||
return start !== null && end !== null && currentMinutes >= start && currentMinutes <= end;
|
||||
}) || null
|
||||
: null;
|
||||
const nextTafMarker =
|
||||
currentMinutes !== null && !currentTafMarker
|
||||
? tafMarkers.find((marker) => {
|
||||
const start = hmToMinutes(normalizeHm(marker.startLocal));
|
||||
return start !== null && start > currentMinutes;
|
||||
}) || null
|
||||
: null;
|
||||
const peakWindowTafMarker =
|
||||
peakWindowStartMinutes !== null && peakWindowEndMinutes !== null
|
||||
? tafMarkers.find((marker) => {
|
||||
const start = hmToMinutes(normalizeHm(marker.startLocal));
|
||||
const end = hmToMinutes(normalizeHm(marker.endLocal));
|
||||
return (
|
||||
start !== null &&
|
||||
end !== null &&
|
||||
start <= peakWindowEndMinutes &&
|
||||
end >= peakWindowStartMinutes
|
||||
);
|
||||
}) || null
|
||||
: null;
|
||||
tafMarkers.forEach((marker) => {
|
||||
if (sameMarker(marker, currentTafMarker) || sameMarker(marker, nextTafMarker)) {
|
||||
marker.isCurrent = true;
|
||||
tafCurrentMarkerPoints[marker.index] = tafMarkerValue;
|
||||
}
|
||||
if (sameMarker(marker, peakWindowTafMarker)) {
|
||||
marker.isPeakWindow = true;
|
||||
tafPeakWindowMarkerPoints[marker.index] = tafMarkerValue - 0.15;
|
||||
}
|
||||
});
|
||||
const formatTafLegendMarker = (
|
||||
marker:
|
||||
| { displayType?: string | null; startLocal?: string | null; endLocal?: string | null; summary?: string | null }
|
||||
| null
|
||||
| undefined,
|
||||
) => {
|
||||
if (!marker) return "";
|
||||
const range = `${marker.startLocal || "--:--"}-${marker.endLocal || "--:--"}`;
|
||||
const status = String(marker.summary || "").trim();
|
||||
return status
|
||||
? `${marker.displayType || ""} ${range} ${status}`.trim()
|
||||
: `${marker.displayType || ""} ${range}`.trim();
|
||||
};
|
||||
|
||||
const legendParts: string[] = [];
|
||||
if (detail.mgm?.temp != null) {
|
||||
@@ -471,14 +586,25 @@ export function getTemperatureChartData(
|
||||
);
|
||||
}
|
||||
if (tafMarkers.length) {
|
||||
const tafText = tafMarkers
|
||||
.slice(0, 4)
|
||||
.map((marker) => `${marker.displayType} ${marker.startLocal}-${marker.endLocal}`)
|
||||
.join(" | ");
|
||||
const primaryTafMarker = currentTafMarker || nextTafMarker;
|
||||
if (primaryTafMarker) {
|
||||
legendParts.push(
|
||||
isEnglish(locale)
|
||||
? `Current TAF: ${formatTafLegendMarker(primaryTafMarker)}`
|
||||
: `当前 TAF:${formatTafLegendMarker(primaryTafMarker)}`,
|
||||
);
|
||||
}
|
||||
if (peakWindowTafMarker && !sameMarker(peakWindowTafMarker, primaryTafMarker)) {
|
||||
legendParts.push(
|
||||
isEnglish(locale)
|
||||
? `Peak-window TAF: ${formatTafLegendMarker(peakWindowTafMarker)}`
|
||||
: `峰值窗口 TAF:${formatTafLegendMarker(peakWindowTafMarker)}`,
|
||||
);
|
||||
}
|
||||
legendParts.push(
|
||||
isEnglish(locale)
|
||||
? `TAF timing: ${tafText}`
|
||||
: `TAF 时段: ${tafText}`,
|
||||
? "Use the current TAF segment as primary; peak-window segments are reference only."
|
||||
: "以当前 TAF 时段为准,峰值窗口时段仅作参考。",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -492,7 +618,9 @@ export function getTemperatureChartData(
|
||||
mgmHourlyPoints,
|
||||
mgmPoints,
|
||||
offset,
|
||||
tafCurrentMarkerPoints,
|
||||
tafMarkerPoints,
|
||||
tafPeakWindowMarkerPoints,
|
||||
temps,
|
||||
},
|
||||
observationLabel:
|
||||
@@ -1013,7 +1141,7 @@ export function computeFrontTrendSignal(
|
||||
String(left.marker_type || "") === String(right.marker_type || "") &&
|
||||
String(left.start_local || "") === String(right.start_local || "") &&
|
||||
String(left.end_local || "") === String(right.end_local || "");
|
||||
const formatTafSegmentLine = (
|
||||
const formatTafSegmentBody = (
|
||||
marker:
|
||||
| {
|
||||
marker_type?: string | null;
|
||||
@@ -1043,20 +1171,9 @@ export function computeFrontTrendSignal(
|
||||
: level === "medium"
|
||||
? "有云量或弱降水扰动"
|
||||
: "以稳定为主";
|
||||
const prefix = isEnglish(locale)
|
||||
? kind === "current"
|
||||
? "Current TAF"
|
||||
: kind === "next"
|
||||
? "Next TAF"
|
||||
: "Peak-window TAF"
|
||||
: kind === "current"
|
||||
? "当前 TAF"
|
||||
: kind === "next"
|
||||
? "下一段 TAF"
|
||||
: "峰值窗口 TAF";
|
||||
return isEnglish(locale)
|
||||
? `${prefix}: ${typeLabel} (${range}), ${statusText}.`
|
||||
: `${prefix}:${typeLabel}(${range}),${statusText}。`;
|
||||
? `${typeLabel} (${range}), ${statusText}.`
|
||||
: `${typeLabel}(${range}),${statusText}。`;
|
||||
};
|
||||
const peakWindowTafMarker =
|
||||
tafSignal.available &&
|
||||
@@ -1082,20 +1199,44 @@ export function computeFrontTrendSignal(
|
||||
? peakTafSummary
|
||||
: "";
|
||||
const currentTafLine = currentTafMarker
|
||||
? formatTafSegmentLine(currentTafMarker, "current")
|
||||
? isEnglish(locale)
|
||||
? `Use current TAF as primary: ${formatTafSegmentBody(currentTafMarker, "current")}`
|
||||
: `以当前 TAF 为准:${formatTafSegmentBody(currentTafMarker, "current")}`
|
||||
: nextTafMarker
|
||||
? formatTafSegmentLine(nextTafMarker, "next")
|
||||
? isEnglish(locale)
|
||||
? `Use next TAF segment as primary: ${formatTafSegmentBody(nextTafMarker, "next")}`
|
||||
: `以下一段 TAF 为准:${formatTafSegmentBody(nextTafMarker, "next")}`
|
||||
: "";
|
||||
const currentTafLineForSummary = currentTafLine.replace(/^Use (current|next) TAF (as primary|segment as primary):\s*/i, "").replace(/^以(当前|下一段) TAF 为准:/, "");
|
||||
const peakTafLine =
|
||||
effectivePeakTafSummary && peakWindowTafMarker
|
||||
? formatTafSegmentLine(peakWindowTafMarker, "peak")
|
||||
? isEnglish(locale)
|
||||
? `Peak-window reference: ${formatTafSegmentBody(peakWindowTafMarker, "peak")}`
|
||||
: `峰值窗口参考:${formatTafSegmentBody(peakWindowTafMarker, "peak")}`
|
||||
: "";
|
||||
const peakTafLineForSummary = peakTafLine.replace(/^Peak-window reference:\s*/i, "").replace(/^峰值窗口参考:/, "");
|
||||
const tafFallbackSummary =
|
||||
currentTafLine || peakTafLine ? "" : tafSummary;
|
||||
const tafPrimarySummary = currentTafMarker
|
||||
? isEnglish(locale)
|
||||
? `Use current TAF as primary: ${currentTafLineForSummary}`
|
||||
: `以当前 TAF 为准:${currentTafLineForSummary}`
|
||||
: nextTafMarker
|
||||
? isEnglish(locale)
|
||||
? `Use next TAF segment as primary: ${currentTafLineForSummary}`
|
||||
: `以下一段 TAF 为准:${currentTafLineForSummary}`
|
||||
: "";
|
||||
const tafReferenceSummary =
|
||||
peakTafLineForSummary &&
|
||||
peakTafLineForSummary !== currentTafLineForSummary
|
||||
? isEnglish(locale)
|
||||
? `Peak-window reference: ${peakTafLineForSummary}`
|
||||
: `峰值窗口参考:${peakTafLineForSummary}`
|
||||
: "";
|
||||
upperAirSummary = [
|
||||
baseUpperAirSummary,
|
||||
currentTafLine,
|
||||
peakTafLine,
|
||||
tafPrimarySummary,
|
||||
tafReferenceSummary,
|
||||
tafFallbackSummary,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -1105,8 +1246,8 @@ export function computeFrontTrendSignal(
|
||||
? {
|
||||
label: isEnglish(locale) ? "Airport TAF" : "机场预报",
|
||||
note:
|
||||
currentTafLine ||
|
||||
peakTafLine ||
|
||||
tafPrimarySummary ||
|
||||
tafReferenceSummary ||
|
||||
tafFallbackSummary ||
|
||||
(isEnglish(locale)
|
||||
? "Airport TAF is available for the current peak window."
|
||||
@@ -1559,8 +1700,8 @@ export function computeFrontTrendSignal(
|
||||
})()
|
||||
: "";
|
||||
const tafSummaryLineBody = [
|
||||
currentTafLine,
|
||||
peakTafLine,
|
||||
tafPrimarySummary,
|
||||
tafReferenceSummary,
|
||||
tafFallbackSummary,
|
||||
tafContrastSummary,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user