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