Compact beacon detail page, toggleable coverage, About page formatting
- Beacon map defaults to just the marker at zoom 11. A daisyUI toggle switch shows/hides the estimated-coverage cell layer; the coverage info line and tier legend are hidden unless the toggle is on. - Beacon detail list replaced with a compact dl grid (2-4 columns). Lat/lon rounded to 5 decimals, power/height/beamwidth formatted via Beacon.format_mw/1 so no more "2.5e3" scientific notation or "280.0". format_mw/1 hoisted onto the schema so index and show share it. - About page no longer uses prose classes (this project doesn't load @tailwindcss/typography). Headings, lists, and paragraphs now use explicit utility classes so they render as intended.
This commit is contained in:
parent
db39f94fc3
commit
a95d2ada52
5 changed files with 264 additions and 179 deletions
|
|
@ -6,23 +6,24 @@ export const BeaconMap = {
|
|||
const onTheAir = this.el.dataset.onTheAir === "true"
|
||||
const cells = JSON.parse(this.el.dataset.cells || "[]")
|
||||
const step = parseFloat(this.el.dataset.gridStep || "0.125")
|
||||
const initialShowCoverage = this.el.dataset.showCoverage === "true"
|
||||
|
||||
const map = L.map(this.el, {
|
||||
zoomControl: true,
|
||||
attributionControl: false,
|
||||
scrollWheelZoom: false
|
||||
}).setView([lat, lon], 9)
|
||||
}).setView([lat, lon], 11)
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19
|
||||
}).addTo(map)
|
||||
|
||||
// Draw each HRRR grid cell as a filled rectangle colored by its received
|
||||
// signal tier. Use a single layerGroup so zoom/pan stays fast.
|
||||
const cellLayer = L.layerGroup().addTo(map)
|
||||
// Build the coverage cell layer up-front but keep it off the map until
|
||||
// the user toggles it on. Using a single layerGroup keeps pan/zoom fast.
|
||||
const cellLayer = L.layerGroup()
|
||||
const half = step / 2.0
|
||||
|
||||
const allBounds = []
|
||||
|
||||
for (const c of cells) {
|
||||
const sw = [c.lat - half, c.lon - half]
|
||||
const ne = [c.lat + half, c.lon + half]
|
||||
|
|
@ -56,13 +57,29 @@ export const BeaconMap = {
|
|||
offset: [0, -10]
|
||||
})
|
||||
|
||||
// Fit to the rendered cells (initial view already set above), then zoom
|
||||
// in two steps so the beacon's immediate surroundings are visible.
|
||||
if (allBounds.length > 0) {
|
||||
map.fitBounds(L.latLngBounds(allBounds), {padding: [20, 20]})
|
||||
map.setZoom(map.getZoom() + 2)
|
||||
const showLayer = () => {
|
||||
if (!map.hasLayer(cellLayer)) cellLayer.addTo(map)
|
||||
if (allBounds.length > 0) {
|
||||
map.fitBounds(L.latLngBounds(allBounds), {padding: [20, 20]})
|
||||
map.setZoom(map.getZoom() + 2)
|
||||
}
|
||||
}
|
||||
|
||||
const hideLayer = () => {
|
||||
if (map.hasLayer(cellLayer)) map.removeLayer(cellLayer)
|
||||
map.setView([lat, lon], 11)
|
||||
}
|
||||
|
||||
if (initialShowCoverage) showLayer()
|
||||
|
||||
this.handleEvent("toggle_coverage", ({show}) => {
|
||||
if (show) {
|
||||
showLayer()
|
||||
} else {
|
||||
hideLayer()
|
||||
}
|
||||
})
|
||||
|
||||
setTimeout(() => map.invalidateSize(), 50)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,31 @@ defmodule Microwaveprop.Beacons.Beacon do
|
|||
|
||||
def keying_label(key), do: Map.get(@keying_labels, key, key)
|
||||
|
||||
@doc """
|
||||
Formats a milliwatt power value as a plain decimal string, never scientific
|
||||
notation. Integers are printed without a decimal; floats keep up to three
|
||||
decimals with trailing zeros trimmed.
|
||||
"""
|
||||
def format_mw(nil), do: ""
|
||||
|
||||
def format_mw(mw) when is_float(mw) do
|
||||
if mw == trunc(mw) do
|
||||
Integer.to_string(trunc(mw))
|
||||
else
|
||||
mw |> :erlang.float_to_binary(decimals: 3) |> trim_trailing_zeros()
|
||||
end
|
||||
end
|
||||
|
||||
def format_mw(mw), do: to_string(mw)
|
||||
|
||||
defp trim_trailing_zeros(str) do
|
||||
if String.contains?(str, ".") do
|
||||
str |> String.trim_trailing("0") |> String.trim_trailing(".")
|
||||
else
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Keying options for `Phoenix.HTML.Form.options_for_select/2`, grouped for a
|
||||
cleaner UI when dozens of weak-signal modes are present.
|
||||
|
|
|
|||
|
|
@ -21,135 +21,145 @@ defmodule MicrowavepropWeb.AboutLive do
|
|||
<:subtitle>What we're building and why.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<section class="prose prose-sm max-w-none dark:prose-invert">
|
||||
<h2>What we're trying to do</h2>
|
||||
<p>
|
||||
Microwave propagation above 10 GHz is dominated by the lower atmosphere:
|
||||
humidity gradients, temperature inversions, ducting layers, rain cells,
|
||||
and hyper-local refractivity structure. At 10, 24, 47, 76, 122, and 241 GHz
|
||||
these effects decide whether a contact happens at 50 km or 500 km — and the
|
||||
window often lasts minutes, not hours.
|
||||
</p>
|
||||
<p>
|
||||
Traditionally microwave contacts are ruled by line of sight, but we've seen that is incorrect. This project is an attempt to build a data-driven
|
||||
propagation prediction model specifically for the amateur microwave bands,
|
||||
using hourly numerical weather forecasts, historical contact records, and
|
||||
eventually calibrated beacon measurements.
|
||||
</p>
|
||||
|
||||
<h2>The approach</h2>
|
||||
<p>
|
||||
We pair two things that most propagation tools keep separate:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<strong>Atmospheric state</strong> — hourly 3 km HRRR forecasts
|
||||
(surface fields plus pressure-level profiles), hourly surface
|
||||
observations from ASOS, 12-hourly upper-air soundings, and gridded
|
||||
IEMRE reanalysis. From these we derive refractivity profiles,
|
||||
ducting detection, minimum refractivity gradient, precipitable water,
|
||||
boundary-layer depth, and a 9-factor composite score for every
|
||||
0.125° cell on a CONUS grid, for each hour of an 18-hour forecast.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Historical contacts</strong> — 58k+ amateur microwave QSOs
|
||||
tagged with the atmospheric conditions at both ends and along the
|
||||
path at the exact time they happened. That gives us a ground-truth
|
||||
dataset we can use to calibrate the scoring weights and, eventually,
|
||||
train a machine learning model that predicts contact success given
|
||||
conditions.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
The current scoring algorithm is a weighted sum of nine factors
|
||||
(humidity, time of day, T–Td depression, refractivity gradient, sky
|
||||
cover, season, rain, wind, pressure) with band-dependent weights —
|
||||
humidity helps at 10 GHz via enhanced refractivity but hurts at
|
||||
24+ GHz via absorption, for example. Every coefficient in that
|
||||
formula is a hypothesis waiting to be tested against the contact
|
||||
and beacon data.
|
||||
</p>
|
||||
|
||||
<h2>What we've collected</h2>
|
||||
<p>Live counts from the production database:</p>
|
||||
|
||||
<div class="not-prose my-4 grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<.stat_card label="Contacts" value={@stats.contacts} />
|
||||
<.stat_card label="Weather stations" value={@stats.weather_stations} />
|
||||
<.stat_card label="Surface observations" value={@stats.surface_observations} />
|
||||
<.stat_card label="Upper-air soundings" value={@stats.soundings} />
|
||||
<.stat_card label="HRRR profiles" value={@stats.hrrr_profiles} />
|
||||
<.stat_card label="IEMRE gridded obs" value={@stats.iemre_observations} />
|
||||
<.stat_card label="Terrain profiles" value={@stats.terrain_profiles} />
|
||||
<.stat_card label="Propagation scores" value={@stats.propagation_scores} />
|
||||
<.stat_card label="Beacons" value={@stats.beacons} />
|
||||
<.stat_card label="Commercial link samples" value={@stats.commercial_samples} />
|
||||
<section class="space-y-8 text-sm leading-relaxed">
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-bold">What we're trying to do</h2>
|
||||
<p>
|
||||
Microwave propagation above 10 GHz is dominated by the lower atmosphere:
|
||||
humidity gradients, temperature inversions, ducting layers, rain cells,
|
||||
and hyper-local refractivity structure. At 10, 24, 47, 76, 122, and 241 GHz
|
||||
these effects decide whether a contact happens at 50 km or 500 km — and the
|
||||
window often lasts minutes, not hours.
|
||||
</p>
|
||||
<p>
|
||||
Traditionally microwave contacts are ruled by line of sight, but we've
|
||||
seen that is incorrect. This project is an attempt to build a data-driven
|
||||
propagation prediction model specifically for the amateur microwave bands,
|
||||
using hourly numerical weather forecasts, historical contact records, and
|
||||
eventually calibrated beacon measurements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>The stack</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Backend:</strong> Elixir / Phoenix 1.8 with LiveView for the
|
||||
real-time map, Ecto on PostgreSQL for storage, Oban for the
|
||||
background data pipelines (HRRR fetch, terrain, ASOS, soundings,
|
||||
IEMRE, solar indices, commercial links), Bandit as the HTTP server.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Frontend:</strong> LiveView with Leaflet for maps, Canvas
|
||||
tile layers for the HRRR heatmap (we render 0.125° cells at
|
||||
interactive frame rates), Tailwind v4 + daisyUI for layout, esbuild
|
||||
for JS bundling.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Physics:</strong> ITU-R P.526-16 knife-edge + Deygout
|
||||
3-edge terrain diffraction, ITU-R P.838-3 rain attenuation,
|
||||
dynamic k-factor from live HRRR refractivity gradients, great-circle
|
||||
geometry for link budgets, Free-Space Path Loss with frequency-
|
||||
dependent O₂ and H₂O absorption per band.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Machine learning:</strong> Nx / Axon / EXLA scaffolding for
|
||||
a 13-feature (8 atmospheric + 4 cyclical temporal + 1 log-frequency)
|
||||
feed-forward network, 64 → 32 → 1 sigmoid. Not trained yet —
|
||||
waiting on a larger calibration dataset.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Data sources:</strong> NOAA HRRR model (AWS S3, hourly
|
||||
analysis + 18 h forecasts), Iowa Environmental Mesonet (ASOS &
|
||||
upper-air soundings), IEMRE gridded reanalysis, SRTM 90 m terrain
|
||||
tiles, SNMP polling of seven commercial microwave links near DFW
|
||||
at 5-minute intervals, Copernicus ERA5 for pre-2014 contact
|
||||
enrichment.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-bold">The approach</h2>
|
||||
<p>We pair two things that most propagation tools keep separate:</p>
|
||||
<ol class="list-decimal list-outside pl-5 space-y-2">
|
||||
<li>
|
||||
<strong>Atmospheric state</strong> — hourly 3 km HRRR forecasts
|
||||
(surface fields plus pressure-level profiles), hourly surface
|
||||
observations from ASOS, 12-hourly upper-air soundings, and gridded
|
||||
IEMRE reanalysis. From these we derive refractivity profiles,
|
||||
ducting detection, minimum refractivity gradient, precipitable water,
|
||||
boundary-layer depth, and a 9-factor composite score for every
|
||||
0.125° cell on a CONUS grid, for each hour of an 18-hour forecast.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Historical contacts</strong> — 58k+ amateur microwave QSOs
|
||||
tagged with the atmospheric conditions at both ends and along the
|
||||
path at the exact time they happened. That gives us a ground-truth
|
||||
dataset we can use to calibrate the scoring weights and, eventually,
|
||||
train a machine learning model that predicts contact success given
|
||||
conditions.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
The current scoring algorithm is a weighted sum of nine factors
|
||||
(humidity, time of day, T–Td depression, refractivity gradient, sky
|
||||
cover, season, rain, wind, pressure) with band-dependent weights —
|
||||
humidity helps at 10 GHz via enhanced refractivity but hurts at
|
||||
24+ GHz via absorption, for example. Every coefficient in that
|
||||
formula is a hypothesis waiting to be tested against the contact
|
||||
and beacon data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>Where this goes next</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Beacon calibration.</strong> A distributed network of
|
||||
amateur receivers continuously reporting CW beacon signal levels,
|
||||
feeding ground-truth measurements back into the scoring algorithm.
|
||||
Beacon submissions are now open to anyone via the Beacons page.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Scoring weight calibration.</strong> Fit the 9-factor
|
||||
weights against recorded QSO distances / counts per band so the
|
||||
composite score reflects real propagation rather than intuition.
|
||||
</li>
|
||||
<li>
|
||||
<strong>ML model training.</strong> Once we have enough labeled
|
||||
contact+condition pairs, train the Axon model to replace or augment
|
||||
the hand-tuned scoring function.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Better inputs.</strong> MRMS for precipitation at
|
||||
24+ GHz (where rain attenuation dominates), RTMA/URMA surface
|
||||
analysis blending, GOES total precipitable water.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-bold">What we've collected</h2>
|
||||
<p>Live counts from the production database:</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<.stat_card label="Contacts" value={@stats.contacts} />
|
||||
<.stat_card label="Weather stations" value={@stats.weather_stations} />
|
||||
<.stat_card label="Surface observations" value={@stats.surface_observations} />
|
||||
<.stat_card label="Upper-air soundings" value={@stats.soundings} />
|
||||
<.stat_card label="HRRR profiles" value={@stats.hrrr_profiles} />
|
||||
<.stat_card label="IEMRE gridded obs" value={@stats.iemre_observations} />
|
||||
<.stat_card label="Terrain profiles" value={@stats.terrain_profiles} />
|
||||
<.stat_card label="Propagation scores" value={@stats.propagation_scores} />
|
||||
<.stat_card label="Beacons" value={@stats.beacons} />
|
||||
<.stat_card label="Commercial link samples" value={@stats.commercial_samples} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs opacity-60 mt-8">
|
||||
Built by and for the <a href="https://www.ntms.org" target="_blank">North Texas Microwave Society</a>.
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-bold">The stack</h2>
|
||||
<ul class="list-disc list-outside pl-5 space-y-2">
|
||||
<li>
|
||||
<strong>Backend:</strong> Elixir / Phoenix 1.8 with LiveView for the
|
||||
real-time map, Ecto on PostgreSQL for storage, Oban for the
|
||||
background data pipelines (HRRR fetch, terrain, ASOS, soundings,
|
||||
IEMRE, solar indices, commercial links), Bandit as the HTTP server.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Frontend:</strong> LiveView with Leaflet for maps, Canvas
|
||||
tile layers for the HRRR heatmap (we render 0.125° cells at
|
||||
interactive frame rates), Tailwind v4 + daisyUI for layout, esbuild
|
||||
for JS bundling.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Physics:</strong> ITU-R P.526-16 knife-edge + Deygout
|
||||
3-edge terrain diffraction, ITU-R P.838-3 rain attenuation,
|
||||
dynamic k-factor from live HRRR refractivity gradients, great-circle
|
||||
geometry for link budgets, Free-Space Path Loss with frequency-
|
||||
dependent O₂ and H₂O absorption per band.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Machine learning:</strong> Nx / Axon / EXLA scaffolding for
|
||||
a 13-feature (8 atmospheric + 4 cyclical temporal + 1 log-frequency)
|
||||
feed-forward network, 64 → 32 → 1 sigmoid. Not trained yet —
|
||||
waiting on a larger calibration dataset.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Data sources:</strong> NOAA HRRR model (AWS S3, hourly
|
||||
analysis + 18 h forecasts), Iowa Environmental Mesonet (ASOS &
|
||||
upper-air soundings), IEMRE gridded reanalysis, SRTM 90 m terrain
|
||||
tiles, SNMP polling of seven commercial microwave links near DFW
|
||||
at 5-minute intervals, Copernicus ERA5 for pre-2014 contact
|
||||
enrichment.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xl font-bold">Where this goes next</h2>
|
||||
<ul class="list-disc list-outside pl-5 space-y-2">
|
||||
<li>
|
||||
<strong>Beacon calibration.</strong> A distributed network of
|
||||
amateur receivers continuously reporting CW beacon signal levels,
|
||||
feeding ground-truth measurements back into the scoring algorithm.
|
||||
Beacon submissions are now open to anyone via the Beacons page.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Scoring weight calibration.</strong> Fit the 9-factor
|
||||
weights against recorded QSO distances / counts per band so the
|
||||
composite score reflects real propagation rather than intuition.
|
||||
</li>
|
||||
<li>
|
||||
<strong>ML model training.</strong> Once we have enough labeled
|
||||
contact+condition pairs, train the Axon model to replace or augment
|
||||
the hand-tuned scoring function.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Better inputs.</strong> MRMS for precipitation at
|
||||
24+ GHz (where rain attenuation dominates), RTMA/URMA surface
|
||||
analysis blending, GOES total precipitable water.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p class="text-xs opacity-60 pt-4 border-t border-base-300">
|
||||
Built by and for the <a href="https://www.ntms.org" target="_blank" class="link link-hover">
|
||||
North Texas Microwave Society
|
||||
</a>.
|
||||
</p>
|
||||
</section>
|
||||
</Layouts.app>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
||||
<:col :let={{_id, beacon}} label="Lat">{beacon.lat}</:col>
|
||||
<:col :let={{_id, beacon}} label="Lon">{beacon.lon}</:col>
|
||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{format_mw(beacon.power_mw)}</:col>
|
||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)}</:col>
|
||||
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||
<:col :let={{_id, beacon}} label="Keying">{Beacon.keying_label(beacon.keying)}</:col>
|
||||
<:col :let={{_id, beacon}} label="On air">
|
||||
|
|
@ -68,7 +68,7 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
<:col :let={{_id, beacon}} label="Frequency (MHz)">{beacon.frequency_mhz}</:col>
|
||||
<:col :let={{_id, beacon}} label="Call">{beacon.callsign}</:col>
|
||||
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{format_mw(beacon.power_mw)}</:col>
|
||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{Beacon.format_mw(beacon.power_mw)}</:col>
|
||||
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||
<:col :let={{_id, beacon}} label="Submitted">
|
||||
{Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")}
|
||||
|
|
@ -162,26 +162,4 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
|||
|
||||
defp admin?(%{user: %{is_admin: true}}), do: true
|
||||
defp admin?(_), do: false
|
||||
|
||||
# Format a milliwatt power value without scientific notation.
|
||||
# Keeps up to 3 decimal places, drops trailing zeros.
|
||||
defp format_mw(nil), do: ""
|
||||
|
||||
defp format_mw(mw) when is_float(mw) do
|
||||
if mw == trunc(mw) do
|
||||
Integer.to_string(trunc(mw))
|
||||
else
|
||||
mw |> :erlang.float_to_binary(decimals: 3) |> trim_trailing_zeros()
|
||||
end
|
||||
end
|
||||
|
||||
defp format_mw(mw), do: to_string(mw)
|
||||
|
||||
defp trim_trailing_zeros(str) do
|
||||
if String.contains?(str, ".") do
|
||||
str |> String.trim_trailing("0") |> String.trim_trailing(".")
|
||||
else
|
||||
str
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -50,10 +50,23 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
|||
data-on-the-air={to_string(@beacon.on_the_air)}
|
||||
data-grid-step={@estimate.grid_step}
|
||||
data-cells={Jason.encode!(@estimate.cells)}
|
||||
data-show-coverage={to_string(@show_coverage)}
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="text-xs opacity-70 mb-2 flex flex-wrap items-center gap-3">
|
||||
<div class="mb-2 flex items-center justify-between flex-wrap gap-2">
|
||||
<label class="label cursor-pointer gap-2 py-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm toggle-primary"
|
||||
checked={@show_coverage}
|
||||
phx-click="toggle_coverage"
|
||||
/>
|
||||
<span class="label-text text-sm">Show estimated current coverage</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div :if={@show_coverage} class="text-xs opacity-70 mb-2 flex flex-wrap items-center gap-3">
|
||||
<span>
|
||||
Band <strong>{@estimate.band_label}</strong> ·
|
||||
EIRP <strong>{@estimate.eirp_dbm} dBm</strong> ·
|
||||
|
|
@ -71,7 +84,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-xs mb-4">
|
||||
<div :if={@show_coverage} class="flex flex-wrap gap-3 text-xs mb-4">
|
||||
<%= for tier <- @estimate.tiers do %>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span
|
||||
|
|
@ -86,24 +99,39 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<.list>
|
||||
<:item title="Frequency (MHz)">{@beacon.frequency_mhz}</:item>
|
||||
<:item title="Callsign">{@beacon.callsign}</:item>
|
||||
<:item title="Grid">{@beacon.grid}</:item>
|
||||
<:item title="Latitude">{@beacon.lat}</:item>
|
||||
<:item title="Longitude">{@beacon.lon}</:item>
|
||||
<:item title="TX power (EIRP) (mW)">{@beacon.power_mw}</:item>
|
||||
<:item title="Height above ground (ft)">{@beacon.height_ft}</:item>
|
||||
<:item title="Bearing">{bearing_label(@beacon.bearing)}</:item>
|
||||
<:item :if={@beacon.beamwidth_deg} title="Beamwidth">
|
||||
{@beacon.beamwidth_deg}°
|
||||
</:item>
|
||||
<:item title="Keying">{Beacon.keying_label(@beacon.keying)}</:item>
|
||||
<:item title="On the air">{if @beacon.on_the_air, do: "Yes", else: "No"}</:item>
|
||||
<:item :if={@beacon.notes && @beacon.notes != ""} title="Notes">
|
||||
<div class="whitespace-pre-wrap">{@beacon.notes}</div>
|
||||
</:item>
|
||||
</.list>
|
||||
<div class="rounded-lg border border-base-300 bg-base-200/40 p-3 text-sm">
|
||||
<dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-x-4 gap-y-2">
|
||||
<.stat_field label="Frequency">{@beacon.frequency_mhz} MHz</.stat_field>
|
||||
<.stat_field label="Callsign">{@beacon.callsign}</.stat_field>
|
||||
<.stat_field label="Keying">{Beacon.keying_label(@beacon.keying)}</.stat_field>
|
||||
<.stat_field label="On the air">
|
||||
<span class={[
|
||||
"badge badge-sm",
|
||||
(@beacon.on_the_air && "badge-success") || "badge-ghost"
|
||||
]}>
|
||||
{if @beacon.on_the_air, do: "Yes", else: "No"}
|
||||
</span>
|
||||
</.stat_field>
|
||||
|
||||
<.stat_field label="Grid">{@beacon.grid}</.stat_field>
|
||||
<.stat_field label="Latitude">{format_coord(@beacon.lat)}</.stat_field>
|
||||
<.stat_field label="Longitude">{format_coord(@beacon.lon)}</.stat_field>
|
||||
<.stat_field label="EIRP">{Beacon.format_mw(@beacon.power_mw)} mW</.stat_field>
|
||||
|
||||
<.stat_field label="Height AGL">{Beacon.format_mw(@beacon.height_ft)} ft</.stat_field>
|
||||
<.stat_field label="Bearing">{bearing_label(@beacon.bearing)}</.stat_field>
|
||||
<.stat_field :if={@beacon.beamwidth_deg} label="Beamwidth">
|
||||
{Beacon.format_mw(@beacon.beamwidth_deg)}°
|
||||
</.stat_field>
|
||||
</dl>
|
||||
|
||||
<div :if={@beacon.notes && @beacon.notes != ""} class="mt-3 pt-3 border-t border-base-300">
|
||||
<div class="font-semibold text-[11px] uppercase tracking-wider opacity-60 mb-1">
|
||||
Notes
|
||||
</div>
|
||||
<div class="whitespace-pre-wrap text-sm">{@beacon.notes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
|
@ -118,10 +146,20 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
|||
socket
|
||||
|> assign(:page_title, "Beacon")
|
||||
|> assign(:beacon, beacon)
|
||||
|> assign(:show_coverage, false)
|
||||
|> assign(:estimate, RangeEstimate.estimate(beacon))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_coverage", _params, socket) do
|
||||
show = not socket.assigns.show_coverage
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:show_coverage, show)
|
||||
|> push_event("toggle_coverage", %{show: show})}
|
||||
end
|
||||
|
||||
def handle_event("approve", _params, socket) do
|
||||
if admin?(socket.assigns.current_scope) do
|
||||
{:ok, approved} = Beacons.approve_beacon(socket.assigns.beacon)
|
||||
|
|
@ -160,4 +198,21 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
|||
defp bearing_label(nil), do: "Omni"
|
||||
defp bearing_label("omni"), do: "Omni"
|
||||
defp bearing_label(value), do: "#{value}°"
|
||||
|
||||
defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 5)
|
||||
defp format_coord(value), do: to_string(value)
|
||||
|
||||
attr :label, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp stat_field(assigns) do
|
||||
~H"""
|
||||
<div class="min-w-0">
|
||||
<dt class="font-semibold text-[11px] uppercase tracking-wider opacity-60">
|
||||
{@label}
|
||||
</dt>
|
||||
<dd class="truncate">{render_slot(@inner_block)}</dd>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue