prop/lib/microwaveprop_web/live/skewt_location_resolver.ex
Graham McIntire d67186176f
fix: resolve all dialyzer type errors across project (46→0 project errors)
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
2026-06-08 17:51:13 -05:00

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