Highlight likely propagation duct, improve contact detail layout
- Mark duct closest to endpoint elevations as likely signal path - Likely duct drawn with solid borders, bold label, stronger fill - Other ducts shown dimmed with dashed borders - Fix path info layout: table columns prevent El wrapping - Surface observations as compact table, limited to 5 nearest stations
This commit is contained in:
parent
5f8c689035
commit
4aaa028ed1
2 changed files with 55 additions and 20 deletions
|
|
@ -22,12 +22,17 @@ const ductPlugin = {
|
|||
const drawBase = Math.min(yBase, bottom)
|
||||
if (drawTop >= drawBase) continue
|
||||
|
||||
ctx.fillStyle = "rgba(34, 197, 94, 0.12)"
|
||||
const likely = duct.likely
|
||||
const fillAlpha = likely ? 0.18 : 0.08
|
||||
const strokeAlpha = likely ? 0.8 : 0.3
|
||||
const lineWidth = likely ? 1.5 : 1
|
||||
|
||||
ctx.fillStyle = `rgba(34, 197, 94, ${fillAlpha})`
|
||||
ctx.fillRect(left, drawTop, right - left, drawBase - drawTop)
|
||||
|
||||
ctx.strokeStyle = "rgba(34, 197, 94, 0.5)"
|
||||
ctx.lineWidth = 1
|
||||
ctx.setLineDash([4, 3])
|
||||
ctx.strokeStyle = `rgba(34, 197, 94, ${strokeAlpha})`
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.setLineDash(likely ? [] : [4, 3])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(left, drawTop)
|
||||
ctx.lineTo(right, drawTop)
|
||||
|
|
@ -38,13 +43,15 @@ const ductPlugin = {
|
|||
|
||||
// Label
|
||||
const labelY = Math.max(drawTop + 12, top + 12)
|
||||
ctx.fillStyle = "rgba(34, 197, 94, 0.8)"
|
||||
ctx.font = "9px sans-serif"
|
||||
const textAlpha = likely ? 1.0 : 0.5
|
||||
ctx.fillStyle = `rgba(34, 197, 94, ${textAlpha})`
|
||||
ctx.font = likely ? "bold 10px sans-serif" : "9px sans-serif"
|
||||
const labels = {hrrr: "Duct", sounding: "Duct (sounding)", inferred: "Est. duct"}
|
||||
const units = {hrrr: "M-units", sounding: "M-units", inferred: "dN/dh÷10"}
|
||||
const prefix = labels[duct.source] || "Duct"
|
||||
const unit = units[duct.source] || "M-units"
|
||||
ctx.fillText(`${prefix} ${Math.round(duct.base_ft)}–${Math.round(duct.top_ft)} ft (${duct.strength} ${unit})`, left + 4, labelY)
|
||||
const tag = likely ? " — likely path" : ""
|
||||
ctx.fillText(`${prefix} ${Math.round(duct.base_ft)}–${Math.round(duct.top_ft)} ft (${duct.strength} ${unit})${tag}`, left + 4, labelY)
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
|
@ -69,7 +76,8 @@ export const ElevationProfile = {
|
|||
base_ft: d.base_m_msl * M2FT,
|
||||
top_ft: d.top_m_msl * M2FT,
|
||||
strength: d.strength,
|
||||
source: d.source || "hrrr"
|
||||
source: d.source || "hrrr",
|
||||
likely: d.likely || false
|
||||
}))
|
||||
|
||||
const allElevs = [...elevations, ...losLine]
|
||||
|
|
|
|||
|
|
@ -257,18 +257,18 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
{format_band_ghz(@contact.band)} · {@contact.mode}
|
||||
· {format_dist(@elevation_profile.dist_km)}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-8 text-sm font-mono mb-2">
|
||||
<div>
|
||||
{@contact.station1} → {@contact.station2}
|
||||
<span class="ml-4">Az: {format_bearing(@elevation_profile.fwd_az)}</span>
|
||||
<span class="ml-4">El: {@elevation_profile.fwd_el}°</span>
|
||||
</div>
|
||||
<div>
|
||||
{@contact.station2} → {@contact.station1}
|
||||
<span class="ml-4">Az: {format_bearing(@elevation_profile.rev_az)}</span>
|
||||
<span class="ml-4">El: {@elevation_profile.rev_el}°</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="text-sm font-mono mb-2 mx-auto">
|
||||
<tr>
|
||||
<td class="pr-6">{@contact.station1} → {@contact.station2}</td>
|
||||
<td class="pr-6">Az: {format_bearing(@elevation_profile.fwd_az)}</td>
|
||||
<td>El: {@elevation_profile.fwd_el}°</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="pr-6">{@contact.station2} → {@contact.station1}</td>
|
||||
<td class="pr-6">Az: {format_bearing(@elevation_profile.rev_az)}</td>
|
||||
<td>El: {@elevation_profile.rev_el}°</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<%= if @elevation_profile.verdict == "BLOCKED" && @elevation_profile.first_obstruction_km do %>
|
||||
<div class="bg-error/10 text-error font-semibold px-3 py-1 rounded mb-2 text-sm">
|
||||
|
|
@ -645,6 +645,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
|> merge_nearby_ducts()
|
||||
|> Enum.sort_by(& &1.strength, :desc)
|
||||
|> Enum.take(3)
|
||||
|> mark_likely_duct(tx_elev, rx_elev)
|
||||
|
||||
%{
|
||||
points:
|
||||
|
|
@ -785,6 +786,32 @@ defmodule MicrowavepropWeb.ContactLive.Show do
|
|||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
# Mark the duct most likely carrying the signal. The signal enters at
|
||||
# surface level from each end, so the duct whose base is closest to
|
||||
# the average endpoint elevation is the most probable propagation path.
|
||||
defp mark_likely_duct([], _tx_elev, _rx_elev), do: []
|
||||
|
||||
defp mark_likely_duct(ducts, tx_elev, rx_elev) do
|
||||
avg_elev = (tx_elev + rx_elev) / 2
|
||||
|
||||
{likely_idx, _} =
|
||||
ducts
|
||||
|> Enum.with_index()
|
||||
|> Enum.min_by(fn {d, _idx} ->
|
||||
# Distance from average endpoint elevation to the duct layer
|
||||
# Prefer ducts that the endpoints are actually inside of
|
||||
if avg_elev >= d.base_m_msl and avg_elev <= d.top_m_msl do
|
||||
-d.strength
|
||||
else
|
||||
min(abs(d.base_m_msl - avg_elev), abs(d.top_m_msl - avg_elev))
|
||||
end
|
||||
end)
|
||||
|
||||
Enum.with_index(ducts, fn d, i ->
|
||||
Map.put(d, :likely, i == likely_idx)
|
||||
end)
|
||||
end
|
||||
|
||||
defp band_to_ghz(nil), do: 10.0
|
||||
|
||||
defp band_to_ghz(band) do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue