diff --git a/lib/microwaveprop/rover/compute.ex b/lib/microwaveprop/rover/compute.ex index c420bb0d..eeddd0f4 100644 --- a/lib/microwaveprop/rover/compute.ex +++ b/lib/microwaveprop/rover/compute.ex @@ -131,8 +131,17 @@ defmodule Microwaveprop.Rover.Compute do step.("Loading propagation grid") raw_cells = time_step("scores_at", fn -> scores_at.(band_mhz, valid_time, bbox) end) + ideal_locations = Map.get(args, :ideal_locations, nil) + + candidate_cells = + case ideal_locations do + nil -> raw_cells + [] -> raw_cells + locations -> snap_cells_to_locations(locations, raw_cells) + end + in_radius = - Enum.filter(raw_cells, fn cell -> + Enum.filter(candidate_cells, fn cell -> DriveTime.haversine_km({home.lat, home.lon}, {cell.lat, cell.lon}) <= radius_km end) @@ -325,6 +334,27 @@ defmodule Microwaveprop.Rover.Compute do |> Canopy.lookup_many() end + # When the user constrains candidates to known ideal locations, build + # synthetic cells at each location's exact lat/lon, inheriting the + # propagation score from the nearest grid cell (so atmospheric forecast + # still factors in even though the spot itself isn't a grid centroid). + defp snap_cells_to_locations(locations, raw_cells) do + Enum.flat_map(locations, fn loc -> + case nearest_grid_cell(raw_cells, loc) do + nil -> [] + cell -> [%{lat: loc.lat, lon: loc.lon, score: cell.score}] + end + end) + end + + defp nearest_grid_cell([], _loc), do: nil + + defp nearest_grid_cell(cells, loc) do + Enum.min_by(cells, fn c -> + DriveTime.haversine_km({c.lat, c.lon}, {loc.lat, loc.lon}) + end) + end + defp terrain_db(nil), do: 0.0 defp terrain_db(clearance_m) do diff --git a/lib/microwaveprop_web/live/rover_live.ex b/lib/microwaveprop_web/live/rover_live.ex index 7bc07428..ff4aa5af 100644 --- a/lib/microwaveprop_web/live/rover_live.ex +++ b/lib/microwaveprop_web/live/rover_live.ex @@ -67,6 +67,7 @@ defmodule MicrowavepropWeb.RoverLive do home_form_error: nil, scoring_loading: false, scoring_step: nil, + only_ideal_locations: false, url_loaded?: false )} end @@ -193,6 +194,10 @@ defmodule MicrowavepropWeb.RoverLive do |> push_event("update_drive_radius", %{km: radius_km})} end + def handle_event("toggle_only_ideal", _params, socket) do + {:noreply, assign(socket, only_ideal_locations: not socket.assigns.only_ideal_locations)} + end + def handle_event("toggle_station", %{"id" => id}, socket) do handle_station_mutation(socket, id, &toggle_station/2) end @@ -713,7 +718,7 @@ defmodule MicrowavepropWeb.RoverLive do end defp scoring_args(socket) do - %{ + base = %{ home: socket.assigns.home, stations: stations_for_compute(socket.assigns.fixed_stations), band_mhz: socket.assigns.band, @@ -722,6 +727,17 @@ defmodule MicrowavepropWeb.RoverLive do max_distance_km: socket.assigns.drive_radius_km, min_elev_gain: 0 } + + if socket.assigns.only_ideal_locations do + ideals = + Rover.list_locations() + |> Enum.filter(&(&1.status == :ideal)) + |> Enum.map(fn l -> %{lat: l.lat, lon: l.lon} end) + + Map.put(base, :ideal_locations, ideals) + else + base + end end defp find_candidate(candidates, grid) do @@ -1163,6 +1179,27 @@ defmodule MicrowavepropWeb.RoverLive do + <.sidebar_section title="CANDIDATES"> + + +
Tip: click any cell to see per-station predicted SNR.