Type spec fixes: - duct_usable_* return boolean→float (delegate to duct_usable_for_band) - sanitize/1 spec includes :unicode error tuples - match_delete/1 broadened from :ets.match_spec() - telemetry_event local type replaces :telemetry.event/0 - wgrib2 parse_lon_val_segment corrected to tuple spec - preloaded Ecto assoc types in get_mission/get_contact! specs Unmatched returns: - _ = prefix on Task.start, Oban.insert, Repo.query!, PubSub.subscribe, :ets.new, and if-expression returns across 18 files Pattern match fixes: - markdown: restructure acc!=[] guard as direct pattern match - path_compute/pskr/skewt_location_resolver: remove dead clauses - calibrate.aprs_144: remove unreachable format_float catch-all - unused.ex: suppress MapSet.union no_opaque Also: remove unused unicode_util_compat from mix.lock
85 lines
2.5 KiB
Elixir
85 lines
2.5 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}"}
|
|
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}"}
|
|
end
|
|
end
|
|
end
|