diff --git a/lib/microwaveprop/rover/candidate_detail.ex b/lib/microwaveprop/rover/candidate_detail.ex index c084e179..06005711 100644 --- a/lib/microwaveprop/rover/candidate_detail.ex +++ b/lib/microwaveprop/rover/candidate_detail.ex @@ -5,6 +5,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do along the great-circle path to each selected fixed station. """ + alias Microwaveprop.Buildings.Index, as: BuildingsIndex alias Microwaveprop.Radio.Maidenhead alias Microwaveprop.Rover.DriveTime alias Microwaveprop.Rover.LinkMargin @@ -12,6 +13,11 @@ defmodule Microwaveprop.Rover.CandidateDetail do @sample_count 48 + # Search radius around each profile sample when checking buildings. + # Slightly tighter than the path-clearance radius (150 m) because the + # profile is a narrower visualization — we want what's "on the line". + @profile_building_radius_m 80 + @type station :: %{callsign: String.t(), lat: float(), lon: float()} @type link_summary :: %{ @@ -24,7 +30,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do station_elev_m: integer() | nil, max_obstacle_m: integer() | nil, clearance_m: integer() | nil, - profile: [%{dist_km: float(), elev: integer()}] + profile: [%{dist_km: float(), elev: integer(), building_m: float()}] } @spec summarize(map(), [station()], non_neg_integer(), DateTime.t(), atom()) :: %{ @@ -43,7 +49,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do max_obstacle = profile |> middle_samples() - |> Enum.map(& &1.elev) + |> Enum.map(fn pt -> pt.elev + round(pt.building_m) end) |> safe_max() clearance = @@ -93,7 +99,13 @@ defmodule Microwaveprop.Rover.CandidateDetail do @sample_count ) do {:ok, points} -> - Enum.map(points, fn pt -> %{dist_km: pt.dist_km, elev: pt.elev} end) + Enum.map(points, fn pt -> + %{ + dist_km: pt.dist_km, + elev: pt.elev, + building_m: BuildingsIndex.max_height_near(pt.lat, pt.lon, @profile_building_radius_m) + } + end) _ -> [] diff --git a/lib/microwaveprop_web/live/rover_live.ex b/lib/microwaveprop_web/live/rover_live.ex index 9f12fdce..c19c1e2d 100644 --- a/lib/microwaveprop_web/live/rover_live.ex +++ b/lib/microwaveprop_web/live/rover_live.ex @@ -791,9 +791,10 @@ defmodule MicrowavepropWeb.RoverLive do defp profile_to_svg([]), do: nil defp profile_to_svg(points) do + tops = Enum.map(points, fn pt -> pt.elev + round(Map.get(pt, :building_m, 0.0)) end) elevs = Enum.map(points, & &1.elev) min_e = Enum.min(elevs) - max_e = Enum.max(elevs) + max_e = max(Enum.max(elevs), Enum.max(tops)) span = max(max_e - min_e, 1) first = List.first(points) last = List.last(points) @@ -808,12 +809,38 @@ defmodule MicrowavepropWeb.RoverLive do fill = "0,60 " <> line <> " 240,60" + has_buildings? = Enum.any?(points, fn pt -> Map.get(pt, :building_m, 0.0) > 0.5 end) + + building_fill = + if has_buildings? do + building_line = + Enum.map_join(points, " ", fn pt -> + x = pt.dist_km / total_km * 240.0 + top = pt.elev + round(Map.get(pt, :building_m, 0.0)) + y = 60.0 - (top - min_e) / span * 50.0 + "#{Float.round(x, 2)},#{Float.round(y, 2)}" + end) + + # Close back along the terrain so the building polygon sits on top. + terrain_back = + points + |> Enum.reverse() + |> Enum.map_join(" ", fn pt -> + x = pt.dist_km / total_km * 240.0 + y = 60.0 - (pt.elev - min_e) / span * 50.0 + "#{Float.round(x, 2)},#{Float.round(y, 2)}" + end) + + building_line <> " " <> terrain_back + end + los_y_start = Float.round(60.0 - (first.elev - min_e) / span * 50.0, 2) los_y_end = Float.round(60.0 - (last.elev - min_e) / span * 50.0, 2) %{ line: line, fill: fill, + building_fill: building_fill, start_elev_m: first.elev, end_elev_m: last.elev, total_km: total_km, @@ -1208,6 +1235,13 @@ defmodule MicrowavepropWeb.RoverLive do ~H""" +