feat(path): show buildings stacked on terrain in path elevation profile
Path calculator now queries Microsoft Building Footprints for the tallest structure within 80m of each profile sample and renders a red "Buildings" dataset on top of the terrain layer in the elevation chart. Also normalize on-disk grid HRRR cells to include the legacy :min_refractivity_gradient / :surface_refractivity / :ducting_detected keys (mapped from :native_min_gradient and :duct_count). Path-calculator crashed in prod with KeyError when a path's HRRR points came from the new on-disk grid format instead of the DB profile shape.
This commit is contained in:
parent
031db7befc
commit
aaf3115fd6
2 changed files with 45 additions and 4 deletions
|
|
@ -8,6 +8,7 @@ interface ProfilePoint {
|
|||
elev: number
|
||||
beam: number
|
||||
r1: number
|
||||
building_m?: number
|
||||
}
|
||||
|
||||
interface DuctRaw {
|
||||
|
|
@ -191,6 +192,15 @@ export const ElevationProfile: ElevationProfileHook = {
|
|||
return (p.elev + bulge) * M2FT
|
||||
})
|
||||
|
||||
// Buildings: terrain + max nearby building height. Same earth bulge so the
|
||||
// dataset stacks correctly on top of terrain.
|
||||
const hasBuildings = points.some(p => (p.building_m || 0) > 0.5)
|
||||
const buildingTops = points.map((p, i) => {
|
||||
const f = n > 0 ? i / n : 0
|
||||
const bulge = (f * dTotal * (1 - f) * dTotal) / (2 * k * R)
|
||||
return (p.elev + (p.building_m || 0) + bulge) * M2FT
|
||||
})
|
||||
|
||||
// LOS beam: straight line from antenna A to antenna B
|
||||
const losLine = points.map(p => p.beam * M2FT)
|
||||
const fresnelLower = points.map(p => (p.beam - p.r1) * M2FT)
|
||||
|
|
@ -207,7 +217,7 @@ export const ElevationProfile: ElevationProfileHook = {
|
|||
likely: d.likely || false
|
||||
}))
|
||||
|
||||
const allElevs = [...elevations, ...losLine, ...earthSurface]
|
||||
const allElevs = [...elevations, ...losLine, ...earthSurface, ...(hasBuildings ? buildingTops : [])]
|
||||
let minY = Math.min(...allElevs) - 40
|
||||
let maxY = Math.max(...allElevs) + 120
|
||||
|
||||
|
|
@ -248,6 +258,19 @@ export const ElevationProfile: ElevationProfileHook = {
|
|||
tension: 0.1,
|
||||
order: 4
|
||||
},
|
||||
...(hasBuildings ? [
|
||||
{
|
||||
label: "Buildings",
|
||||
data: buildingTops,
|
||||
borderColor: "#dc2626",
|
||||
backgroundColor: "rgba(220, 38, 38, 0.45)",
|
||||
fill: "-1",
|
||||
pointRadius: 0,
|
||||
borderWidth: 1,
|
||||
tension: 0.1,
|
||||
order: 3
|
||||
}
|
||||
] : []),
|
||||
{
|
||||
label: "Line of Sight",
|
||||
data: losLine,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ defmodule MicrowavepropWeb.PathLive do
|
|||
|
||||
import MicrowavepropWeb.Components.SkewTChart
|
||||
|
||||
alias Microwaveprop.Buildings.Index, as: BuildingsIndex
|
||||
alias Microwaveprop.Buildings.Loader, as: BuildingsLoader
|
||||
alias Microwaveprop.Ionosphere
|
||||
alias Microwaveprop.Propagation
|
||||
alias Microwaveprop.Propagation.BandConfig
|
||||
|
|
@ -531,6 +533,10 @@ defmodule MicrowavepropWeb.PathLive do
|
|||
|> Map.put_new(:lat, lat)
|
||||
|> Map.put_new(:lon, lon)
|
||||
|> Map.put_new(:valid_time, valid_time)
|
||||
|> Map.put_new(:min_refractivity_gradient, Map.get(cell, :native_min_gradient))
|
||||
|> Map.put_new(:surface_refractivity, nil)
|
||||
|> Map.put_new(:ducting_detected, Map.get(cell, :duct_count, 0) > 0)
|
||||
|> Map.put_new(:duct_characteristics, nil)
|
||||
end
|
||||
|
||||
# Linear interpolation along the great-circle-approximate path. Good
|
||||
|
|
@ -1622,14 +1628,18 @@ defmodule MicrowavepropWeb.PathLive do
|
|||
defp factor_name(:pressure), do: "Pressure"
|
||||
|
||||
defp elevation_data(result) do
|
||||
# ElevationProfile hook expects: points with dist_km, elev, beam, r1
|
||||
raw_points = result.terrain.analysis.points
|
||||
_ = BuildingsLoader.ensure_loaded_for_bbox(profile_bbox(raw_points))
|
||||
|
||||
# ElevationProfile hook expects: points with dist_km, elev, beam, r1, building_m
|
||||
points =
|
||||
Enum.map(result.terrain.analysis.points, fn p ->
|
||||
Enum.map(raw_points, fn p ->
|
||||
%{
|
||||
dist_km: p.dist_km,
|
||||
elev: p.elev,
|
||||
beam: p[:beam] || 0,
|
||||
r1: p[:r1] || 0
|
||||
r1: p[:r1] || 0,
|
||||
building_m: BuildingsIndex.max_height_near(p.lat, p.lon, 80)
|
||||
}
|
||||
end)
|
||||
|
||||
|
|
@ -1641,4 +1651,12 @@ defmodule MicrowavepropWeb.PathLive do
|
|||
ducts: []
|
||||
}
|
||||
end
|
||||
|
||||
defp profile_bbox([]), do: %{"south" => 0, "north" => 0, "west" => 0, "east" => 0}
|
||||
|
||||
defp profile_bbox(points) do
|
||||
lats = Enum.map(points, & &1.lat)
|
||||
lons = Enum.map(points, & &1.lon)
|
||||
%{"south" => Enum.min(lats), "north" => Enum.max(lats), "west" => Enum.min(lons), "east" => Enum.max(lons)}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue