diff --git a/lib/microwaveprop_web/live/path_live.ex b/lib/microwaveprop_web/live/path_live.ex index f0b0a9ff..42e7e7f8 100644 --- a/lib/microwaveprop_web/live/path_live.ex +++ b/lib/microwaveprop_web/live/path_live.ex @@ -1,8 +1,8 @@ defmodule MicrowavepropWeb.PathLive do @moduledoc false use MicrowavepropWeb, :live_view - use LiveStash + alias Microwaveprop.Propagation alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.Scorer alias Microwaveprop.Radio.CallsignClient @@ -22,56 +22,93 @@ defmodule MicrowavepropWeb.PathLive do {"241 GHz", "241000"} ] - @stash_fields [ - :source, - :destination, - :band, - :src_height_ft, - :dst_height_ft, - :tx_power_dbm, - :src_gain_dbi, - :dst_gain_dbi - ] + @url_params ~w(source destination band src_height_ft dst_height_ft tx_power_dbm src_gain_dbi dst_gain_dbi) + @defaults %{ + "source" => "", + "destination" => "", + "band" => "10000", + "src_height_ft" => "30", + "dst_height_ft" => "30", + "tx_power_dbm" => "20", + "src_gain_dbi" => "30", + "dst_gain_dbi" => "30" + } @impl true def mount(_params, _session, socket) do - defaults = %{ - source: "", - destination: "", - band: "10000", - src_height_ft: "30", - dst_height_ft: "30", - tx_power_dbm: "20", - src_gain_dbi: "30", - dst_gain_dbi: "30" - } - - recovered = - case LiveStash.recover_state(socket) do - {:recovered, socket} -> - socket.assigns - |> Map.take(@stash_fields) - |> then(&Map.merge(defaults, &1)) - - _ -> - defaults - end - {:ok, - assign( - socket, - [ - {:page_title, "Path Calculator"}, - {:band_options, @band_options}, - {:result, nil}, - {:error, nil}, - {:computing, false} - ] ++ - Map.to_list(recovered) + assign(socket, + page_title: "Path Calculator", + band_options: @band_options, + result: nil, + error: nil, + computing: false, + source: "", + destination: "", + band: "10000", + src_height_ft: "30", + dst_height_ft: "30", + tx_power_dbm: "20", + src_gain_dbi: "30", + dst_gain_dbi: "30" )} end @impl true + def handle_params(params, _uri, socket) do + p = Map.merge(@defaults, Map.take(params, @url_params)) + + socket = + assign(socket, + source: p["source"], + destination: p["destination"], + band: p["band"], + src_height_ft: p["src_height_ft"], + dst_height_ft: p["dst_height_ft"], + tx_power_dbm: p["tx_power_dbm"], + src_gain_dbi: p["src_gain_dbi"], + dst_gain_dbi: p["dst_gain_dbi"] + ) + + # Auto-calculate if source and destination are in the URL + if p["source"] != "" and p["destination"] != "" and not socket.assigns.computing and is_nil(socket.assigns.result) do + src_ht = parse_float(p["src_height_ft"], 30.0) + dst_ht = parse_float(p["dst_height_ft"], 30.0) + tx_dbm = parse_float(p["tx_power_dbm"], 20.0) + + station_params = %{ + src_height_ft: src_ht, + dst_height_ft: dst_ht, + src_height_m: src_ht * 0.3048, + dst_height_m: dst_ht * 0.3048, + tx_power_dbm: tx_dbm, + tx_power_mw: :math.pow(10, tx_dbm / 10), + src_gain_dbi: parse_float(p["src_gain_dbi"], 30.0), + dst_gain_dbi: parse_float(p["dst_gain_dbi"], 30.0) + } + + send(self(), {:compute_path, p["source"], p["destination"], String.to_integer(p["band"]), station_params}) + {:noreply, assign(socket, computing: true)} + else + {:noreply, socket} + end + end + + @impl true + def handle_event("update_form", params, socket) do + {:noreply, + assign(socket, + source: params["source"] || "", + destination: params["destination"] || "", + band: params["band"] || "10000", + src_height_ft: params["src_height_ft"] || "30", + dst_height_ft: params["dst_height_ft"] || "30", + tx_power_dbm: params["tx_power_dbm"] || "20", + src_gain_dbi: params["src_gain_dbi"] || "30", + dst_gain_dbi: params["dst_gain_dbi"] || "30" + )} + end + def handle_event("calculate", params, socket) do src_height_ft = parse_float(params["src_height_ft"], 30.0) dst_height_ft = parse_float(params["dst_height_ft"], 30.0) @@ -110,7 +147,8 @@ defmodule MicrowavepropWeb.PathLive do {:compute_path, params["source"], params["destination"], String.to_integer(params["band"]), station_params} ) - {:noreply, LiveStash.stash_assigns(socket, @stash_fields)} + url_params = params |> Map.take(@url_params) |> Enum.reject(fn {_k, v} -> v == "" end) |> Map.new() + {:noreply, push_patch(socket, to: ~p"/path?#{url_params}", replace: true)} end def handle_event("gps_location", %{"lat" => lat, "lon" => lon}, socket) do @@ -172,6 +210,9 @@ defmodule MicrowavepropWeb.PathLive do loss_budget = compute_loss_budget(dist_km, freq_ghz, band_config, terrain_result, conditions) power_budget = compute_power_budget(station_params, loss_budget) + # 18-hour forecast from propagation grid (midpoint of path) + forecast = Propagation.point_forecast(band_mhz, midlat, midlon) + {:ok, %{ source: src, @@ -187,6 +228,7 @@ defmodule MicrowavepropWeb.PathLive do scoring: scoring, loss_budget: loss_budget, power_budget: power_budget, + forecast: forecast, hrrr_count: length(hrrr_profiles) }} end @@ -399,6 +441,9 @@ defmodule MicrowavepropWeb.PathLive do defp format_number(n) when is_float(n), do: :erlang.float_to_binary(n, decimals: 1) defp format_number(n), do: to_string(n) + defp format_utc_hour(%DateTime{} = dt), do: Calendar.strftime(dt, "%H:%M") + defp format_utc_hour(_), do: "?" + defp format_mw(mw) when mw >= 1000, do: "#{format_number(mw / 1000)} W" defp format_mw(mw) when mw >= 1, do: "#{format_number(mw)} mW" defp format_mw(mw), do: "#{format_number(mw * 1000)} uW" @@ -414,7 +459,7 @@ defmodule MicrowavepropWeb.PathLive do <:subtitle>Analyze microwave propagation between two points -
+
@@ -781,6 +826,50 @@ defmodule MicrowavepropWeb.PathLive do
<% end %>
+ + <%!-- 18-Hour Forecast --%> + <%= if @result.forecast != [] do %> +
+
+ 18-Hour Propagation Forecast · {@result.band_config.label} +
+
+ <%= for point <- @result.forecast do %> + <% pct = max(point.score, 2) %> +
+
+ <% end %> +
+
+ <%= if length(@result.forecast) > 0 do %> + {format_utc_hour(List.first(@result.forecast).valid_time)} UTC + {format_utc_hour(List.last(@result.forecast).valid_time)} UTC + <% end %> +
+
+ <% best = Enum.max_by(@result.forecast, & &1.score) %> + + Best: + + {best.score} + + at {format_utc_hour(best.valid_time)} UTC + + <% worst = Enum.min_by(@result.forecast, & &1.score) %> + + Worst: + + {worst.score} + + at {format_utc_hour(worst.valid_time)} UTC + +
+
+ <% end %> """ end