Highlight precipitation windows in front trend signals
This commit is contained in:
@@ -1854,6 +1854,107 @@ export function computeFrontTrendSignal(
|
|||||||
(max, point) => Math.max(max, Number(point.precipProb) || 0),
|
(max, point) => Math.max(max, Number(point.precipProb) || 0),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
const precipWindowSource =
|
||||||
|
futureSlice.length >= 2 ? futureSlice : slice;
|
||||||
|
const precipCandidates = precipWindowSource
|
||||||
|
.map((point) => ({
|
||||||
|
label: normalizeHm(point.label),
|
||||||
|
minute: pointMinutes(point),
|
||||||
|
precip: Number(point.precipProb) || 0,
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(point): point is { label: string; minute: number; precip: number } =>
|
||||||
|
point.label != null && point.minute != null,
|
||||||
|
);
|
||||||
|
const precipThreshold = precipMax >= 70 ? 50 : precipMax >= 40 ? 40 : 0;
|
||||||
|
const precipWindows: Array<{
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
startLabel: string;
|
||||||
|
endLabel: string;
|
||||||
|
peak: number;
|
||||||
|
}> = [];
|
||||||
|
if (precipThreshold > 0) {
|
||||||
|
let active:
|
||||||
|
| {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
startLabel: string;
|
||||||
|
endLabel: string;
|
||||||
|
peak: number;
|
||||||
|
}
|
||||||
|
| null = null;
|
||||||
|
for (const point of precipCandidates) {
|
||||||
|
if (point.precip >= precipThreshold) {
|
||||||
|
if (!active) {
|
||||||
|
active = {
|
||||||
|
start: point.minute,
|
||||||
|
end: point.minute,
|
||||||
|
startLabel: point.label,
|
||||||
|
endLabel: point.label,
|
||||||
|
peak: point.precip,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
active.end = point.minute;
|
||||||
|
active.endLabel = point.label;
|
||||||
|
active.peak = Math.max(active.peak, point.precip);
|
||||||
|
}
|
||||||
|
} else if (active) {
|
||||||
|
precipWindows.push(active);
|
||||||
|
active = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (active) precipWindows.push(active);
|
||||||
|
}
|
||||||
|
const primaryPrecipWindow =
|
||||||
|
precipWindows.length > 0
|
||||||
|
? [...precipWindows].sort((left, right) => {
|
||||||
|
if (right.peak !== left.peak) return right.peak - left.peak;
|
||||||
|
return (right.end - right.start) - (left.end - left.start);
|
||||||
|
})[0]
|
||||||
|
: null;
|
||||||
|
const precipWindowLabel = primaryPrecipWindow
|
||||||
|
? `${primaryPrecipWindow.startLabel}-${primaryPrecipWindow.endLabel}`
|
||||||
|
: "";
|
||||||
|
const precipPeakOverlap =
|
||||||
|
primaryPrecipWindow &&
|
||||||
|
peakWindowStartMinutes != null &&
|
||||||
|
peakWindowEndMinutes != null
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(primaryPrecipWindow.end, peakWindowEndMinutes) -
|
||||||
|
Math.max(primaryPrecipWindow.start, peakWindowStartMinutes),
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
const precipOverlapLevel =
|
||||||
|
primaryPrecipWindow && peakWindowStartMinutes != null && peakWindowEndMinutes != null
|
||||||
|
? precipPeakOverlap >= 120
|
||||||
|
? "high"
|
||||||
|
: precipPeakOverlap > 0
|
||||||
|
? "medium"
|
||||||
|
: "low"
|
||||||
|
: "unknown";
|
||||||
|
const precipWindowSummary = (() => {
|
||||||
|
if (!primaryPrecipWindow) {
|
||||||
|
return isEnglish(locale)
|
||||||
|
? "No concentrated model precipitation window is visible yet."
|
||||||
|
: "模型里暂时还看不出明显集中的降水窗口。";
|
||||||
|
}
|
||||||
|
const overlapText = isEnglish(locale)
|
||||||
|
? precipOverlapLevel === "high"
|
||||||
|
? "It overlaps heavily with the peak window."
|
||||||
|
: precipOverlapLevel === "medium"
|
||||||
|
? "It overlaps part of the peak window."
|
||||||
|
: "It sits mostly outside the peak window."
|
||||||
|
: precipOverlapLevel === "high"
|
||||||
|
? "与峰值窗口重叠较高。"
|
||||||
|
: precipOverlapLevel === "medium"
|
||||||
|
? "与峰值窗口部分重叠。"
|
||||||
|
: "主要落在峰值窗口之外。";
|
||||||
|
return isEnglish(locale)
|
||||||
|
? `Model precipitation window is ${precipWindowLabel}, peaking near ${Math.round(primaryPrecipWindow.peak)}%. ${overlapText}`
|
||||||
|
: `模型降水窗口在 ${precipWindowLabel},峰值约 ${Math.round(primaryPrecipWindow.peak)}%。${overlapText}`;
|
||||||
|
})();
|
||||||
const firstBucket = trendBucketFromDir(first.windDir);
|
const firstBucket = trendBucketFromDir(first.windDir);
|
||||||
const lastBucket = trendBucketFromDir(last.windDir);
|
const lastBucket = trendBucketFromDir(last.windDir);
|
||||||
const weatherGovPeriods = getForecastTextForDate(detail, dateStr);
|
const weatherGovPeriods = getForecastTextForDate(detail, dateStr);
|
||||||
@@ -1981,7 +2082,11 @@ export function computeFrontTrendSignal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (precipMax >= 50) {
|
if (precipMax >= 50) {
|
||||||
parts.push("Precipitation risk is high enough to watch for cloud/rain suppression.");
|
parts.push(
|
||||||
|
primaryPrecipWindow
|
||||||
|
? `Model precipitation risk concentrates in ${precipWindowLabel}.`
|
||||||
|
: "Precipitation risk is high enough to watch for cloud/rain suppression.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parts.length) {
|
if (!parts.length) {
|
||||||
@@ -2023,7 +2128,11 @@ export function computeFrontTrendSignal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (precipMax >= 50) {
|
if (precipMax >= 50) {
|
||||||
parts.push("降水概率已足以关注云雨压温。");
|
parts.push(
|
||||||
|
primaryPrecipWindow
|
||||||
|
? `模型降水窗口主要落在 ${precipWindowLabel}。`
|
||||||
|
: "降水概率已足以关注云雨压温。",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parts.length) {
|
if (!parts.length) {
|
||||||
@@ -2175,13 +2284,13 @@ export function computeFrontTrendSignal(
|
|||||||
const precipNote = (() => {
|
const precipNote = (() => {
|
||||||
if (precipMax >= 60) {
|
if (precipMax >= 60) {
|
||||||
return isEnglish(locale)
|
return isEnglish(locale)
|
||||||
? "Precipitation risk is high enough that cloud/rain suppression can materially change the peak outcome."
|
? `${precipWindowSummary} Cloud/rain suppression can materially change the peak outcome.`
|
||||||
: "降水概率已高到足以显著改变峰值兑现结果,需要重点防压温。";
|
: `${precipWindowSummary} 降水风险已高到足以显著改变峰值兑现结果,需要重点防压温。`;
|
||||||
}
|
}
|
||||||
if (precipMax >= 40) {
|
if (precipMax >= 40) {
|
||||||
return isEnglish(locale)
|
return isEnglish(locale)
|
||||||
? "Precipitation risk is meaningful; watch whether cloud and showers interrupt daytime heating."
|
? `${precipWindowSummary} Watch whether cloud and showers interrupt daytime heating.`
|
||||||
: "降水概率已有存在感,需要关注云系和阵雨是否打断白天增温。";
|
: `${precipWindowSummary} 需要关注云系和阵雨是否打断白天增温。`;
|
||||||
}
|
}
|
||||||
return isEnglish(locale)
|
return isEnglish(locale)
|
||||||
? "Precipitation risk remains limited and is used mainly as a suppression check."
|
? "Precipitation risk remains limited and is used mainly as a suppression check."
|
||||||
@@ -2215,10 +2324,12 @@ export function computeFrontTrendSignal(
|
|||||||
value: `${bucketLabel(firstBucket, locale)} -> ${bucketLabel(lastBucket, locale)}`,
|
value: `${bucketLabel(firstBucket, locale)} -> ${bucketLabel(lastBucket, locale)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isEnglish(locale) ? "Precip probability" : "降水概率",
|
label: isEnglish(locale) ? "Precip window" : "降水窗口",
|
||||||
note: precipNote,
|
note: precipNote,
|
||||||
tone: precipMax >= 50 ? "cold" : "",
|
tone: precipMax >= 50 ? "cold" : "",
|
||||||
value: `${Math.round(precipMax)}%`,
|
value: primaryPrecipWindow
|
||||||
|
? `${precipWindowLabel} · ${Math.round(precipMax)}%`
|
||||||
|
: `${Math.round(precipMax)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: isEnglish(locale) ? "Forecast cloud-cover delta" : "预测云量增幅",
|
label: isEnglish(locale) ? "Forecast cloud-cover delta" : "预测云量增幅",
|
||||||
@@ -2253,6 +2364,8 @@ export function computeFrontTrendSignal(
|
|||||||
metrics,
|
metrics,
|
||||||
upperAirMetrics,
|
upperAirMetrics,
|
||||||
upperAirSummary,
|
upperAirSummary,
|
||||||
|
precipOverlapLevel,
|
||||||
|
precipWindowLabel,
|
||||||
precipMax,
|
precipMax,
|
||||||
score,
|
score,
|
||||||
summary: combinedSummary || backendSummary || summary,
|
summary: combinedSummary || backendSummary || summary,
|
||||||
|
|||||||
Reference in New Issue
Block a user