diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 21de57d7..720b52e7 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -136,16 +136,38 @@ defmodule MicrowavepropWeb.MapLive do # Resolve the initial band/time/center/zoom from URL params, falling # back to LiveStash recovery, then session geolocation and defaults. + # Parsers return nil for anything malformed so `||` walks to the next + # sensible source; ISO8601 times that sit outside the current HRRR + # window get dropped too so a stale shared link doesn't land the map + # on a hour with no scores. defp resolve_view(params, session, recovered_band, recovered_time) do band = parse_band_param(params["band"]) || recovered_band || @default_band valid_times = Propagation.available_valid_times(band) - time = parse_time_param(params["t"]) || recovered_time || closest_to_now(valid_times) + time = resolve_time(params["t"], recovered_time, valid_times) center = parse_center_param(params["lat"], params["lon"]) || initial_center(session) zoom = parse_zoom_param(params["zoom"]) || @default_zoom %{band: band, time: time, center: center, zoom: zoom, valid_times: valid_times} end + defp resolve_time(param, recovered_time, valid_times) do + case parse_time_param(param) do + %DateTime{} = t -> + if time_in_window?(t, valid_times), do: t, else: closest_to_now(valid_times) + + nil -> + recovered_time || closest_to_now(valid_times) + end + end + + defp time_in_window?(_, []), do: false + + defp time_in_window?(%DateTime{} = t, valid_times) do + earliest = Enum.min(valid_times, DateTime) + latest = Enum.max(valid_times, DateTime) + DateTime.compare(t, earliest) != :lt and DateTime.compare(t, latest) != :gt + end + # Build a bounding box roughly matching the hardcoded default (~3.4° × 9°) # around a given center so the initial HRRR score query still returns a # useful tile set for the visible area. diff --git a/test/microwaveprop_web/live/map_live_test.exs b/test/microwaveprop_web/live/map_live_test.exs index dac01da7..dc7c5750 100644 --- a/test/microwaveprop_web/live/map_live_test.exs +++ b/test/microwaveprop_web/live/map_live_test.exs @@ -145,6 +145,48 @@ defmodule MicrowavepropWeb.MapLiveTest do {:ok, _lv, html} = live(conn, ~p"/map?band=notanumber") assert html =~ ~s(data-selected-band="10000") end + + test "unknown band MHz falls back to the default", %{conn: conn} do + {:ok, _lv, html} = live(conn, ~p"/map?band=99999") + assert html =~ ~s(data-selected-band="10000") + end + + test "malformed lat/lon/zoom are ignored", %{conn: conn} do + {:ok, _lv, html} = live(conn, ~p"/map?lat=banana&lon=apple&zoom=over9000") + assert html =~ ~s(data-zoom="7") + refute html =~ ~s(data-center-lat="banana") + end + + test "out-of-range lat/lon are ignored", %{conn: conn} do + {:ok, _lv, html} = live(conn, ~p"/map?lat=500&lon=-999") + refute html =~ ~s(data-center-lat="500) + end + + test "out-of-range zoom falls back to default", %{conn: conn} do + {:ok, _lv, html} = live(conn, ~p"/map?zoom=42") + assert html =~ ~s(data-zoom="7") + end + + test "unparseable time is ignored", %{conn: conn} do + {:ok, lv, _html} = live(conn, ~p"/map?t=not-a-date") + # Page renders normally without crashing. + assert render(lv) =~ "data-selected-time" + end + + test "ISO8601 time outside the current forecast window falls back", %{conn: conn} do + # Year 2000 is well before any valid HRRR data; page should still + # mount and fall back to the normal 'now' cursor rather than try + # to fetch scores for an empty time. + {:ok, _lv, _html} = live(conn, ~p"/map?t=2000-01-01T00:00:00Z") + end + + test "every param garbage at once still mounts cleanly", %{conn: conn} do + {:ok, _lv, html} = + live(conn, ~p"/map?band=xyz&t=nope&lat=abc&lon=def&zoom=foo") + + assert html =~ ~s(data-selected-band="10000") + assert html =~ ~s(data-zoom="7") + end end describe "cursor_for_now/2" do