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
|
elev: number
|
||||||
beam: number
|
beam: number
|
||||||
r1: number
|
r1: number
|
||||||
|
building_m?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DuctRaw {
|
interface DuctRaw {
|
||||||
|
|
@ -191,6 +192,15 @@ export const ElevationProfile: ElevationProfileHook = {
|
||||||
return (p.elev + bulge) * M2FT
|
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
|
// LOS beam: straight line from antenna A to antenna B
|
||||||
const losLine = points.map(p => p.beam * M2FT)
|
const losLine = points.map(p => p.beam * M2FT)
|
||||||
const fresnelLower = points.map(p => (p.beam - p.r1) * M2FT)
|
const fresnelLower = points.map(p => (p.beam - p.r1) * M2FT)
|
||||||
|
|
@ -207,7 +217,7 @@ export const ElevationProfile: ElevationProfileHook = {
|
||||||
likely: d.likely || false
|
likely: d.likely || false
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const allElevs = [...elevations, ...losLine, ...earthSurface]
|
const allElevs = [...elevations, ...losLine, ...earthSurface, ...(hasBuildings ? buildingTops : [])]
|
||||||
let minY = Math.min(...allElevs) - 40
|
let minY = Math.min(...allElevs) - 40
|
||||||
let maxY = Math.max(...allElevs) + 120
|
let maxY = Math.max(...allElevs) + 120
|
||||||
|
|
||||||
|
|
@ -248,6 +258,19 @@ export const ElevationProfile: ElevationProfileHook = {
|
||||||
tension: 0.1,
|
tension: 0.1,
|
||||||
order: 4
|
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",
|
label: "Line of Sight",
|
||||||
data: losLine,
|
data: losLine,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
|
|
||||||
import MicrowavepropWeb.Components.SkewTChart
|
import MicrowavepropWeb.Components.SkewTChart
|
||||||
|
|
||||||
|
alias Microwaveprop.Buildings.Index, as: BuildingsIndex
|
||||||
|
alias Microwaveprop.Buildings.Loader, as: BuildingsLoader
|
||||||
alias Microwaveprop.Ionosphere
|
alias Microwaveprop.Ionosphere
|
||||||
alias Microwaveprop.Propagation
|
alias Microwaveprop.Propagation
|
||||||
alias Microwaveprop.Propagation.BandConfig
|
alias Microwaveprop.Propagation.BandConfig
|
||||||
|
|
@ -531,6 +533,10 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
|> Map.put_new(:lat, lat)
|
|> Map.put_new(:lat, lat)
|
||||||
|> Map.put_new(:lon, lon)
|
|> Map.put_new(:lon, lon)
|
||||||
|> Map.put_new(:valid_time, valid_time)
|
|> 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
|
end
|
||||||
|
|
||||||
# Linear interpolation along the great-circle-approximate path. Good
|
# Linear interpolation along the great-circle-approximate path. Good
|
||||||
|
|
@ -1622,14 +1628,18 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
defp factor_name(:pressure), do: "Pressure"
|
defp factor_name(:pressure), do: "Pressure"
|
||||||
|
|
||||||
defp elevation_data(result) do
|
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 =
|
points =
|
||||||
Enum.map(result.terrain.analysis.points, fn p ->
|
Enum.map(raw_points, fn p ->
|
||||||
%{
|
%{
|
||||||
dist_km: p.dist_km,
|
dist_km: p.dist_km,
|
||||||
elev: p.elev,
|
elev: p.elev,
|
||||||
beam: p[:beam] || 0,
|
beam: p[:beam] || 0,
|
||||||
r1: p[:r1] || 0
|
r1: p[:r1] || 0,
|
||||||
|
building_m: BuildingsIndex.max_height_near(p.lat, p.lon, 80)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -1641,4 +1651,12 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
ducts: []
|
ducts: []
|
||||||
}
|
}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue