feat(rover): show buildings stacked on terrain in path profile
Each profile sample now carries the tallest building height within 80m of the path point. The candidate-detail SVG renders these as a red polygon stacked on top of the terrain so blockage from buildings is visible alongside ridges. max_obstacle_m now reflects building tops too, so the clearance label downgrades when buildings sit on the link.
This commit is contained in:
parent
3b8d87c356
commit
b97af8a5ce
2 changed files with 50 additions and 4 deletions
|
|
@ -5,6 +5,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do
|
||||||
along the great-circle path to each selected fixed station.
|
along the great-circle path to each selected fixed station.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
alias Microwaveprop.Buildings.Index, as: BuildingsIndex
|
||||||
alias Microwaveprop.Radio.Maidenhead
|
alias Microwaveprop.Radio.Maidenhead
|
||||||
alias Microwaveprop.Rover.DriveTime
|
alias Microwaveprop.Rover.DriveTime
|
||||||
alias Microwaveprop.Rover.LinkMargin
|
alias Microwaveprop.Rover.LinkMargin
|
||||||
|
|
@ -12,6 +13,11 @@ defmodule Microwaveprop.Rover.CandidateDetail do
|
||||||
|
|
||||||
@sample_count 48
|
@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 station :: %{callsign: String.t(), lat: float(), lon: float()}
|
||||||
|
|
||||||
@type link_summary :: %{
|
@type link_summary :: %{
|
||||||
|
|
@ -24,7 +30,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do
|
||||||
station_elev_m: integer() | nil,
|
station_elev_m: integer() | nil,
|
||||||
max_obstacle_m: integer() | nil,
|
max_obstacle_m: integer() | nil,
|
||||||
clearance_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()) :: %{
|
@spec summarize(map(), [station()], non_neg_integer(), DateTime.t(), atom()) :: %{
|
||||||
|
|
@ -43,7 +49,7 @@ defmodule Microwaveprop.Rover.CandidateDetail do
|
||||||
max_obstacle =
|
max_obstacle =
|
||||||
profile
|
profile
|
||||||
|> middle_samples()
|
|> middle_samples()
|
||||||
|> Enum.map(& &1.elev)
|
|> Enum.map(fn pt -> pt.elev + round(pt.building_m) end)
|
||||||
|> safe_max()
|
|> safe_max()
|
||||||
|
|
||||||
clearance =
|
clearance =
|
||||||
|
|
@ -93,7 +99,13 @@ defmodule Microwaveprop.Rover.CandidateDetail do
|
||||||
@sample_count
|
@sample_count
|
||||||
) do
|
) do
|
||||||
{:ok, points} ->
|
{: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)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
[]
|
[]
|
||||||
|
|
|
||||||
|
|
@ -791,9 +791,10 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
defp profile_to_svg([]), do: nil
|
defp profile_to_svg([]), do: nil
|
||||||
|
|
||||||
defp profile_to_svg(points) do
|
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)
|
elevs = Enum.map(points, & &1.elev)
|
||||||
min_e = Enum.min(elevs)
|
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)
|
span = max(max_e - min_e, 1)
|
||||||
first = List.first(points)
|
first = List.first(points)
|
||||||
last = List.last(points)
|
last = List.last(points)
|
||||||
|
|
@ -808,12 +809,38 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
|
|
||||||
fill = "0,60 " <> line <> " 240,60"
|
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_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)
|
los_y_end = Float.round(60.0 - (last.elev - min_e) / span * 50.0, 2)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
line: line,
|
line: line,
|
||||||
fill: fill,
|
fill: fill,
|
||||||
|
building_fill: building_fill,
|
||||||
start_elev_m: first.elev,
|
start_elev_m: first.elev,
|
||||||
end_elev_m: last.elev,
|
end_elev_m: last.elev,
|
||||||
total_km: total_km,
|
total_km: total_km,
|
||||||
|
|
@ -1208,6 +1235,13 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
~H"""
|
~H"""
|
||||||
<svg viewBox="0 0 240 64" class="w-full h-12 mt-1" preserveAspectRatio="none">
|
<svg viewBox="0 0 240 64" class="w-full h-12 mt-1" preserveAspectRatio="none">
|
||||||
<polygon points={@svg.fill} fill="rgb(14 165 233 / 0.25)" />
|
<polygon points={@svg.fill} fill="rgb(14 165 233 / 0.25)" />
|
||||||
|
<polygon
|
||||||
|
:if={@svg.building_fill}
|
||||||
|
points={@svg.building_fill}
|
||||||
|
fill="rgb(220 38 38 / 0.55)"
|
||||||
|
stroke="#dc2626"
|
||||||
|
stroke-width="0.6"
|
||||||
|
/>
|
||||||
<polyline points={@svg.line} fill="none" stroke="#0ea5e9" stroke-width="1.2" />
|
<polyline points={@svg.line} fill="none" stroke="#0ea5e9" stroke-width="1.2" />
|
||||||
<line
|
<line
|
||||||
x1="0"
|
x1="0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue