diff --git a/lib/microwaveprop/propagation.ex b/lib/microwaveprop/propagation.ex index 5f75541e..67f5cc77 100644 --- a/lib/microwaveprop/propagation.ex +++ b/lib/microwaveprop/propagation.ex @@ -322,6 +322,34 @@ defmodule Microwaveprop.Propagation do end end + @doc """ + Returns the per-day peak score at `(lat, lon)` for the next `:days` + UTC days on `:band_mhz`, in ascending date order. Days with no + available forecast valid_times are omitted. + + Backs the 7-day outlook strip on the map — turns the hourly forecast + sparkline into a coarse "is Saturday any good" verdict. + """ + @spec daily_outlook_at(float(), float(), keyword()) :: + [%{date: Date.t(), peak_score: non_neg_integer()}] + def daily_outlook_at(lat, lon, opts \\ []) do + band_mhz = Keyword.get(opts, :band_mhz, 10_000) + days = Keyword.get(opts, :days, 7) + today = DateTime.to_date(DateTime.utc_now()) + horizon = Date.add(today, days - 1) + + band_mhz + |> point_forecast(lat, lon) + |> Enum.group_by(fn entry -> + DateTime.to_date(entry.valid_time) + end) + |> Enum.filter(fn {date, _} -> Date.compare(date, today) != :lt and Date.compare(date, horizon) != :gt end) + |> Enum.map(fn {date, entries} -> + %{date: date, peak_score: entries |> Enum.map(& &1.score) |> Enum.max()} + end) + |> Enum.sort_by(& &1.date, Date) + end + @doc "Get scores across all forecast hours for a single grid point (for sparkline)." @spec point_forecast(non_neg_integer(), float(), float()) :: [%{valid_time: DateTime.t(), score: non_neg_integer()}] diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index b770b8cb..1b62a5cb 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -87,6 +87,7 @@ defmodule MicrowavepropWeb.MapLive do outside_conus: outside_conus?(visitor), grid_visible: false, radar_visible: false, + daily_outlook: Propagation.daily_outlook_at(center.lat, center.lon, band_mhz: selected_band, days: 7), deploy_iso: Calendar.strftime(build_ts, "%Y-%m-%d %H:%M UTC"), deploy_ago: format_deploy_ago(build_ts) )} @@ -159,6 +160,15 @@ defmodule MicrowavepropWeb.MapLive do |> LiveStash.stash_assigns([:selected_band, :selected_time]) |> push_event("update_scores", %{scores: scores}) |> push_event("update_band_info", %{band_info: band_info(band)}) + |> assign( + :daily_outlook, + Propagation.daily_outlook_at( + socket.assigns.bounds["south"] + (socket.assigns.bounds["north"] - socket.assigns.bounds["south"]) / 2, + socket.assigns.bounds["west"] + (socket.assigns.bounds["east"] - socket.assigns.bounds["west"]) / 2, + band_mhz: band, + days: 7 + ) + ) |> push_timeline() {:noreply, socket} @@ -451,6 +461,53 @@ defmodule MicrowavepropWeb.MapLive do end end + # Outlook helpers. Day label is the 3-letter weekday ("Sat", "Sun"); + # badge class paints the card background based on the peak score so + # the strip reads as a rough heatmap at a glance. + defp outlook_day_label(%Date{} = date), do: Calendar.strftime(date, "%a") + + defp outlook_bg_class(score) when is_integer(score) do + cond do + score >= 80 -> "bg-emerald-500/30 border-emerald-400/50" + score >= 65 -> "bg-lime-500/25 border-lime-400/50" + score >= 50 -> "bg-amber-500/25 border-amber-400/50" + score >= 33 -> "bg-orange-500/25 border-orange-400/50" + true -> "bg-rose-500/25 border-rose-400/50" + end + end + + defp outlook_bg_class(_), do: "bg-base-300/30 border-base-300/50" + + attr :id, :string, required: true + attr :outlook, :list, required: true + + defp daily_outlook_strip(assigns) do + ~H""" +