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) {
|
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({
|
datasets.push({
|
||||||
backgroundColor: "#f59e0b",
|
backgroundColor: "#f59e0b",
|
||||||
borderColor: "#f59e0b",
|
borderColor: "#f59e0b",
|
||||||
@@ -312,9 +336,9 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
|||||||
data: todayChartData.datasets.tafMarkerPoints,
|
data: todayChartData.datasets.tafMarkerPoints,
|
||||||
fill: false,
|
fill: false,
|
||||||
label: locale === "en-US" ? "TAF Timing" : "TAF 时段",
|
label: locale === "en-US" ? "TAF Timing" : "TAF 时段",
|
||||||
order: -2,
|
order: -4,
|
||||||
pointHoverRadius: 8,
|
pointHoverRadius: 0,
|
||||||
pointRadius: 5,
|
pointRadius: 0,
|
||||||
showLine: false,
|
showLine: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -334,6 +358,7 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
|||||||
filter: (legendItem, chartData) => {
|
filter: (legendItem, chartData) => {
|
||||||
const text = String(legendItem.text || "");
|
const text = String(legendItem.text || "");
|
||||||
if (!text) return false;
|
if (!text) return false;
|
||||||
|
if (text === "TAF Timing" || text === "TAF 时段") return false;
|
||||||
if (!text.includes("DEB")) return true;
|
if (!text.includes("DEB")) return true;
|
||||||
|
|
||||||
const firstDebIndex = (chartData.datasets || []).findIndex(
|
const firstDebIndex = (chartData.datasets || []).findIndex(
|
||||||
@@ -351,12 +376,33 @@ function DailyTemperatureChart({ dateStr }: { dateStr: string }) {
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
label: (ctx) => {
|
label: (ctx) => {
|
||||||
const label = String(ctx.dataset.label || "");
|
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(
|
const marker = (todayChartData.tafMarkers || []).find(
|
||||||
(item) => item.index === ctx.dataIndex,
|
(item) => item.index === ctx.dataIndex,
|
||||||
);
|
);
|
||||||
if (!marker) return label;
|
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.markerType,
|
||||||
marker.displayType || marker.markerType,
|
marker.displayType || marker.markerType,
|
||||||
)}`;
|
)}`;
|
||||||
|
|||||||
+170
-29
@@ -374,8 +374,70 @@ export function getTemperatureChartData(
|
|||||||
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
const tafMarkersRaw = Array.isArray(detail.taf?.signal?.markers)
|
||||||
? 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 tafMarkerValue = max - 0.4;
|
||||||
const tafMarkerPoints = new Array(times.length).fill(null);
|
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
|
const tafMarkers = tafMarkersRaw
|
||||||
.map((marker) => {
|
.map((marker) => {
|
||||||
const labelTime = String(marker?.label_time || "").trim();
|
const labelTime = String(marker?.label_time || "").trim();
|
||||||
@@ -397,10 +459,63 @@ export function getTemperatureChartData(
|
|||||||
isEnglish(locale)
|
isEnglish(locale)
|
||||||
? String(marker?.summary_en || "").trim()
|
? String(marker?.summary_en || "").trim()
|
||||||
: String(marker?.summary_zh || "").trim(),
|
: String(marker?.summary_zh || "").trim(),
|
||||||
|
isCurrent: false,
|
||||||
|
isPeakWindow: false,
|
||||||
suppressionLevel: String(marker?.suppression_level || "").trim(),
|
suppressionLevel: String(marker?.suppression_level || "").trim(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((marker) => marker.index >= 0);
|
.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[] = [];
|
const legendParts: string[] = [];
|
||||||
if (detail.mgm?.temp != null) {
|
if (detail.mgm?.temp != null) {
|
||||||
@@ -471,14 +586,25 @@ export function getTemperatureChartData(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tafMarkers.length) {
|
if (tafMarkers.length) {
|
||||||
const tafText = tafMarkers
|
const primaryTafMarker = currentTafMarker || nextTafMarker;
|
||||||
.slice(0, 4)
|
if (primaryTafMarker) {
|
||||||
.map((marker) => `${marker.displayType} ${marker.startLocal}-${marker.endLocal}`)
|
legendParts.push(
|
||||||
.join(" | ");
|
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(
|
legendParts.push(
|
||||||
isEnglish(locale)
|
isEnglish(locale)
|
||||||
? `TAF timing: ${tafText}`
|
? "Use the current TAF segment as primary; peak-window segments are reference only."
|
||||||
: `TAF 时段: ${tafText}`,
|
: "以当前 TAF 时段为准,峰值窗口时段仅作参考。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +618,9 @@ export function getTemperatureChartData(
|
|||||||
mgmHourlyPoints,
|
mgmHourlyPoints,
|
||||||
mgmPoints,
|
mgmPoints,
|
||||||
offset,
|
offset,
|
||||||
|
tafCurrentMarkerPoints,
|
||||||
tafMarkerPoints,
|
tafMarkerPoints,
|
||||||
|
tafPeakWindowMarkerPoints,
|
||||||
temps,
|
temps,
|
||||||
},
|
},
|
||||||
observationLabel:
|
observationLabel:
|
||||||
@@ -1013,7 +1141,7 @@ export function computeFrontTrendSignal(
|
|||||||
String(left.marker_type || "") === String(right.marker_type || "") &&
|
String(left.marker_type || "") === String(right.marker_type || "") &&
|
||||||
String(left.start_local || "") === String(right.start_local || "") &&
|
String(left.start_local || "") === String(right.start_local || "") &&
|
||||||
String(left.end_local || "") === String(right.end_local || "");
|
String(left.end_local || "") === String(right.end_local || "");
|
||||||
const formatTafSegmentLine = (
|
const formatTafSegmentBody = (
|
||||||
marker:
|
marker:
|
||||||
| {
|
| {
|
||||||
marker_type?: string | null;
|
marker_type?: string | null;
|
||||||
@@ -1043,20 +1171,9 @@ export function computeFrontTrendSignal(
|
|||||||
: level === "medium"
|
: 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)
|
return isEnglish(locale)
|
||||||
? `${prefix}: ${typeLabel} (${range}), ${statusText}.`
|
? `${typeLabel} (${range}), ${statusText}.`
|
||||||
: `${prefix}:${typeLabel}(${range}),${statusText}。`;
|
: `${typeLabel}(${range}),${statusText}。`;
|
||||||
};
|
};
|
||||||
const peakWindowTafMarker =
|
const peakWindowTafMarker =
|
||||||
tafSignal.available &&
|
tafSignal.available &&
|
||||||
@@ -1082,20 +1199,44 @@ export function computeFrontTrendSignal(
|
|||||||
? peakTafSummary
|
? peakTafSummary
|
||||||
: "";
|
: "";
|
||||||
const currentTafLine = currentTafMarker
|
const currentTafLine = currentTafMarker
|
||||||
? formatTafSegmentLine(currentTafMarker, "current")
|
? isEnglish(locale)
|
||||||
|
? `Use current TAF as primary: ${formatTafSegmentBody(currentTafMarker, "current")}`
|
||||||
|
: `以当前 TAF 为准:${formatTafSegmentBody(currentTafMarker, "current")}`
|
||||||
: nextTafMarker
|
: 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 =
|
const peakTafLine =
|
||||||
effectivePeakTafSummary && peakWindowTafMarker
|
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 =
|
const tafFallbackSummary =
|
||||||
currentTafLine || peakTafLine ? "" : tafSummary;
|
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 = [
|
upperAirSummary = [
|
||||||
baseUpperAirSummary,
|
baseUpperAirSummary,
|
||||||
currentTafLine,
|
tafPrimarySummary,
|
||||||
peakTafLine,
|
tafReferenceSummary,
|
||||||
tafFallbackSummary,
|
tafFallbackSummary,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -1105,8 +1246,8 @@ export function computeFrontTrendSignal(
|
|||||||
? {
|
? {
|
||||||
label: isEnglish(locale) ? "Airport TAF" : "机场预报",
|
label: isEnglish(locale) ? "Airport TAF" : "机场预报",
|
||||||
note:
|
note:
|
||||||
currentTafLine ||
|
tafPrimarySummary ||
|
||||||
peakTafLine ||
|
tafReferenceSummary ||
|
||||||
tafFallbackSummary ||
|
tafFallbackSummary ||
|
||||||
(isEnglish(locale)
|
(isEnglish(locale)
|
||||||
? "Airport TAF is available for the current peak window."
|
? "Airport TAF is available for the current peak window."
|
||||||
@@ -1559,8 +1700,8 @@ export function computeFrontTrendSignal(
|
|||||||
})()
|
})()
|
||||||
: "";
|
: "";
|
||||||
const tafSummaryLineBody = [
|
const tafSummaryLineBody = [
|
||||||
currentTafLine,
|
tafPrimarySummary,
|
||||||
peakTafLine,
|
tafReferenceSummary,
|
||||||
tafFallbackSummary,
|
tafFallbackSummary,
|
||||||
tafContrastSummary,
|
tafContrastSummary,
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user