Single search bar accepts an address, Maidenhead grid square, or callsign; resolves it to lat/lon via Geocoder / Maidenhead / CallsignLocation (cheapest classification first), snaps to the HRRR grid via the existing ProfilesFile reader, and renders an SVG Skew-T-Log-P with isobars, skewed isotherms, dry/moist adiabats, and mixing-ratio lines. A button row picks between every available forecast hour (now → f18); default is the most recent analysis. Right pane lists derived stability and refractivity stats from SoundingParams.derive (PWAT, BL depth, K-index, lifted index, min dN/dh, ducting status + per-duct base/top/ΔM). Renderer is server-side SVG so the page works without JS and serializes into the LiveView payload as a single tag. Wind barbs are deliberately omitted — HRRR's persisted profile carries wind only at 10 m AGL.
91 lines
2.6 KiB
Elixir
91 lines
2.6 KiB
Elixir
defmodule MicrowavepropWeb.SkewtLocationResolver do
|
|
@moduledoc """
|
|
Turn a single search-bar string ("EM12kp", "W5ISP", or
|
|
"Plano, TX") into a `{lat, lon, label}` triple for the Skew-T
|
|
page. Tries the cheapest classification first (grid → callsign →
|
|
geocoder) so a typo on a grid square doesn't burn a Google API
|
|
request before the user has a chance to correct it.
|
|
"""
|
|
|
|
alias Microwaveprop.CallsignLocation
|
|
alias Microwaveprop.Geocoder
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
|
|
@grid_regex ~r/^[A-Ra-r]{2}[0-9]{2}([A-Xa-x]{2}([0-9]{2}([A-Xa-x]{2})?)?)?$/
|
|
@callsign_regex ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}$/
|
|
|
|
@type classification :: :grid | :callsign | :address
|
|
|
|
@type result :: %{
|
|
lat: float(),
|
|
lon: float(),
|
|
label: String.t(),
|
|
source: classification()
|
|
}
|
|
|
|
@spec classify(String.t()) :: classification()
|
|
def classify(query) when is_binary(query) do
|
|
cleaned = query |> String.trim() |> String.upcase()
|
|
|
|
cond do
|
|
Regex.match?(@grid_regex, cleaned) -> :grid
|
|
Regex.match?(@callsign_regex, cleaned) -> :callsign
|
|
true -> :address
|
|
end
|
|
end
|
|
|
|
@spec resolve(String.t()) :: {:ok, result()} | {:error, String.t()}
|
|
def resolve(query) when is_binary(query) do
|
|
cleaned = String.trim(query)
|
|
|
|
case classify(cleaned) do
|
|
:grid -> resolve_grid(cleaned)
|
|
:callsign -> resolve_callsign(cleaned)
|
|
:address -> resolve_address(cleaned)
|
|
end
|
|
end
|
|
|
|
defp resolve_grid(query) do
|
|
case Maidenhead.to_latlon(query) do
|
|
{:ok, {lat, lon}} ->
|
|
{:ok, %{lat: lat, lon: lon, label: String.upcase(query), source: :grid}}
|
|
|
|
:error ->
|
|
{:error, "could not parse '#{query}' as a Maidenhead grid square"}
|
|
end
|
|
end
|
|
|
|
defp resolve_callsign(query) do
|
|
upper = String.upcase(query)
|
|
|
|
case CallsignLocation.lookup(upper) do
|
|
{:ok, %{latitude: lat, longitude: lon, gridsquare: grid, name: name}} ->
|
|
label =
|
|
[upper, name, grid]
|
|
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|
|
|> Enum.uniq()
|
|
|> Enum.join(" — ")
|
|
|
|
{:ok, %{lat: lat, lon: lon, label: label, source: :callsign}}
|
|
|
|
{:error, reason} when is_binary(reason) ->
|
|
{:error, "callsign lookup failed: #{reason}"}
|
|
|
|
{:error, _} ->
|
|
{:error, "callsign lookup failed"}
|
|
end
|
|
end
|
|
|
|
defp resolve_address(query) do
|
|
case Geocoder.geocode(query) do
|
|
{:ok, %{lat: lat, lon: lon}} ->
|
|
{:ok, %{lat: lat, lon: lon, label: query, source: :address}}
|
|
|
|
{:error, reason} when is_binary(reason) ->
|
|
{:error, "geocode failed: #{reason}"}
|
|
|
|
{:error, _} ->
|
|
{:error, "geocode failed"}
|
|
end
|
|
end
|
|
end
|