fix(map-timeline): never label a future forecast hour as 'Now'
Both the Elixir initial-cursor pick and the JS timeline renderer used nearest-by-absolute-distance, so any wall-clock time past :30 snapped the 'Now' label onto the next top-of-hour forecast — up to ~30 min in the future. Switch to the latest valid_time at or before wall-clock, with a fallback to the earliest time when every slot is future (shouldn't happen for HRRR, safe default).
This commit is contained in:
parent
6c652ef2d4
commit
28a9eb3a92
3 changed files with 63 additions and 7 deletions
|
|
@ -1330,9 +1330,16 @@ export const PropagationMap: Record<string, unknown> & {
|
|||
return { ...t, dt, isSelected, isPast, offsetH, idx, label: "" }
|
||||
})
|
||||
|
||||
// "Now" = the item closest to current time
|
||||
const closestIdx = items.reduce((best, item, i) =>
|
||||
Math.abs(item.dt.getTime() - now.getTime()) < Math.abs(items[best].dt.getTime() - now.getTime()) ? i : best, 0)
|
||||
// "Now" = the latest forecast hour at or before wall-clock. Picking
|
||||
// by absolute distance rounds up past the half-hour and labels a
|
||||
// future slot "Now" (e.g. at 18:35 UTC, 19:00 is 25 min away vs.
|
||||
// 18:00 at 35 min). Fall back to the earliest slot if they're all
|
||||
// in the future.
|
||||
const nowMs = now.getTime()
|
||||
const pastOrNow = items.filter(t => t.dt.getTime() <= nowMs)
|
||||
const closestIdx = pastOrNow.length > 0
|
||||
? items.indexOf(pastOrNow.reduce((latest, t) => t.dt.getTime() > latest.dt.getTime() ? t : latest))
|
||||
: items.reduce((earliest, t, i) => items[earliest].dt.getTime() < t.dt.getTime() ? earliest : i, 0)
|
||||
|
||||
items.forEach((t, i) => {
|
||||
if (i === closestIdx) {
|
||||
|
|
|
|||
|
|
@ -424,11 +424,25 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
"#{hours}h ago"
|
||||
end
|
||||
|
||||
defp closest_to_now([]), do: nil
|
||||
defp closest_to_now(times), do: cursor_for_now(times, DateTime.utc_now())
|
||||
|
||||
defp closest_to_now(times) do
|
||||
now = DateTime.utc_now()
|
||||
Enum.min_by(times, fn t -> abs(DateTime.diff(t, now)) end)
|
||||
@doc false
|
||||
# Picks the forecast valid_time the map should display as "Now".
|
||||
# The rule is "latest valid_time at or before `now`" — a nearest-by-
|
||||
# absolute-distance pick can land on a future hour (e.g. at 18:35 UTC,
|
||||
# 19:00 is 25 min away vs. 18:00 at 35 min, so nearest would label
|
||||
# 19:00 "Now"). Falls back to the earliest time only if every slot is
|
||||
# in the future, which HRRR analysis + forecast lists shouldn't produce.
|
||||
@spec cursor_for_now([DateTime.t()], DateTime.t()) :: DateTime.t() | nil
|
||||
def cursor_for_now([], _now), do: nil
|
||||
|
||||
def cursor_for_now(times, %DateTime{} = now) do
|
||||
past_or_now = Enum.filter(times, &(DateTime.compare(&1, now) != :gt))
|
||||
|
||||
case past_or_now do
|
||||
[] -> Enum.min_by(times, &DateTime.to_unix/1)
|
||||
past -> Enum.max_by(past, &DateTime.to_unix/1)
|
||||
end
|
||||
end
|
||||
|
||||
defp score_range_km(score, config) do
|
||||
|
|
|
|||
|
|
@ -132,6 +132,41 @@ defmodule MicrowavepropWeb.MapLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "cursor_for_now/2" do
|
||||
alias MicrowavepropWeb.MapLive
|
||||
|
||||
test "returns nil for an empty list" do
|
||||
assert MapLive.cursor_for_now([], DateTime.utc_now()) == nil
|
||||
end
|
||||
|
||||
test "picks the latest time at or before now — never future" do
|
||||
# Valid times at 18:00, 19:00, 20:00 UTC. Wall clock 19:35.
|
||||
# Nearest-by-abs would round up to 20:00 (25 min future);
|
||||
# floor picks 19:00 (35 min past).
|
||||
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z], ~U[2026-04-18 20:00:00Z]]
|
||||
now = ~U[2026-04-18 19:35:00Z]
|
||||
|
||||
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 19:00:00Z]
|
||||
end
|
||||
|
||||
test "matches exact hour when now is on the hour" do
|
||||
times = [~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z]]
|
||||
assert MapLive.cursor_for_now(times, ~U[2026-04-18 19:00:00Z]) == ~U[2026-04-18 19:00:00Z]
|
||||
end
|
||||
|
||||
test "falls back to the earliest time when every slot is in the future" do
|
||||
times = [~U[2026-04-18 20:00:00Z], ~U[2026-04-18 21:00:00Z]]
|
||||
now = ~U[2026-04-18 19:00:00Z]
|
||||
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 20:00:00Z]
|
||||
end
|
||||
|
||||
test "ignores list order" do
|
||||
times = [~U[2026-04-18 20:00:00Z], ~U[2026-04-18 18:00:00Z], ~U[2026-04-18 19:00:00Z]]
|
||||
now = ~U[2026-04-18 19:35:00Z]
|
||||
assert MapLive.cursor_for_now(times, now) == ~U[2026-04-18 19:00:00Z]
|
||||
end
|
||||
end
|
||||
|
||||
describe "format_data_timestamp/1" do
|
||||
alias MicrowavepropWeb.MapLive
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue