defmodule MicrowavepropWeb.GridBounds do @moduledoc """ Bounds clamp + sanity checks for grid-cell HTTP endpoints (`/scores/cells`, `/weather/cells`). The propagation grid covers HRRR (CONUS) and HRDPS (Canada to 60°N): roughly lat 25-60, lon -141 to -52. Any caller-provided viewport is clamped to a generous superset of that bbox, which is what bounds the work: after clamping no request can ask for more than the full grid (~92k cells, ~1.4 MB), already materialized once per valid_time and served from cache. A global or otherwise absurd viewport therefore costs no more than a fully zoomed-out map does. `nil` bounds are passed through (caller intends the full grid). """ # Generous superset of HRRR + HRDPS — a few extra degrees on each # edge so legitimate clients zoomed slightly past the data extent # still get a 200 with the actual covered cells. @lat_min 20.0 @lat_max 65.0 @lon_min -150.0 @lon_max -50.0 @type bounds :: %{optional(String.t()) => float()} | nil @doc """ Clamp `bounds` to the supported grid extent. Returns: * `{:ok, nil}` when bounds are nil (caller wants the full grid) * `{:ok, clamped_bounds}` otherwise * `{:error, :invalid_bounds}` when north < south or east < west """ @spec clamp(bounds()) :: {:ok, bounds()} | {:error, :invalid_bounds} def clamp(nil), do: {:ok, nil} def clamp(%{"south" => s, "north" => n, "west" => w, "east" => e}) when is_number(s) and is_number(n) and is_number(w) and is_number(e) do south = clamp_value(s, @lat_min, @lat_max) north = clamp_value(n, @lat_min, @lat_max) west = clamp_value(w, @lon_min, @lon_max) east = clamp_value(e, @lon_min, @lon_max) if north < south or east < west do {:error, :invalid_bounds} else {:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}} end end def clamp(_), do: {:error, :invalid_bounds} defp clamp_value(v, lo, hi), do: v |> max(lo) |> min(hi) end