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 — about 35° × 90° = 3150 sq degrees. Any caller-provided viewport is clamped to a generous superset of that bbox, and viewports whose area exceeds the supported region are rejected as `:viewport_too_large` so a malicious caller cannot trigger global / multi-million-cell binary responses. `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 # Comfortably above the HRDPS+HRRR union (~3150 sq deg), but tight # enough that the fully-clamped extent (~4500 sq deg) still trips # the cap — a "give me everything" request is exactly the DoS # shape we want to reject. @max_viewport_area_sq_deg 4000.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}` when the clamped viewport area is OK * `{:error, :viewport_too_large}` when the clamped area still exceeds `@max_viewport_area_sq_deg` (catches degenerate requests like global bounds that survive clamping) * `{:error, :invalid_bounds}` when north < south or east < west """ @spec clamp(bounds()) :: {:ok, bounds()} | {:error, :viewport_too_large | :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) cond do north < south or east < west -> {:error, :invalid_bounds} (north - south) * (east - west) > @max_viewport_area_sq_deg -> {:error, :viewport_too_large} true -> {:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}} end end def clamp(_), do: {:error, :invalid_bounds} @doc "Maximum viewport area in square degrees." @spec max_viewport_area_sq_deg() :: float() def max_viewport_area_sq_deg, do: @max_viewport_area_sq_deg defp clamp_value(v, lo, hi), do: v |> max(lo) |> min(hi) end