prop/lib/microwaveprop/weather/grib2/lambert_conformal.ex
Graham McIntire 51e390959c Add dialyzer specs and types across the codebase
277 @spec/@type annotations added to 58 files covering all public
APIs: contexts (propagation, radio, weather, terrain, beacons,
commercial), GRIB2 decoders, terrain analysis, duct detection,
rain scatter, CSV/ADIF import, weather clients, and all Ecto
schemas. Dialyzer passes with 0 errors.
2026-04-12 08:55:04 -05:00

86 lines
2.2 KiB
Elixir

defmodule Microwaveprop.Weather.Grib2.LambertConformal do
@moduledoc false
@r_earth 6_371_229.0
@doc """
Convert a lat/lon to grid indices (i, j) on a Lambert Conformal Conic grid.
Returns `{:ok, {i, j}}` or `{:error, :outside_grid}`.
"""
@spec to_grid_index(map(), float(), float()) ::
{:ok, {non_neg_integer(), non_neg_integer()}} | {:error, :outside_grid}
def to_grid_index(grid, lat, lon) do
%{
nx: nx,
ny: ny,
la1: la1,
lo1: lo1,
dx: dx,
dy: dy,
latin1: latin1,
lov: lov
} = grid
# Normalize longitudes to 0..360
lo1 = normalize_lon(lo1)
lon = normalize_lon(lon)
lov = normalize_lon(lov)
latin1_rad = deg_to_rad(latin1)
la1_rad = deg_to_rad(la1)
lat_rad = deg_to_rad(lat)
# Cone constant (for HRRR, latin1 == latin2 == 38.5, so n = sin(latin1))
n = :math.sin(latin1_rad)
# F factor
f =
:math.cos(latin1_rad) *
:math.pow(:math.tan(:math.pi() / 4 + latin1_rad / 2), n) / n
# rho for a given latitude
rho_origin = rho(la1_rad, n, f)
rho_target = rho(lat_rad, n, f)
# Theta angles (difference from central meridian, scaled by cone constant)
theta_origin = n * deg_to_rad(lo1 - lov)
theta_target = n * deg_to_rad(adjusted_lon_diff(lon, lov))
# Grid coordinates
x_origin = rho_origin * :math.sin(theta_origin)
y_origin = rho_origin * :math.cos(theta_origin)
x_target = rho_target * :math.sin(theta_target)
y_target = rho_target * :math.cos(theta_target)
i = round((x_target - x_origin) / dx)
j = round((y_origin - y_target) / dy)
if i >= 0 and i < nx and j >= 0 and j < ny do
{:ok, {i, j}}
else
{:error, :outside_grid}
end
end
defp rho(lat_rad, n, f) do
@r_earth * f / :math.pow(:math.tan(:math.pi() / 4 + lat_rad / 2), n)
end
defp deg_to_rad(deg), do: deg * :math.pi() / 180.0
defp normalize_lon(lon) when lon < 0, do: lon + 360.0
defp normalize_lon(lon), do: lon + 0.0
# Compute longitude difference handling wrap-around
defp adjusted_lon_diff(lon, lov) do
diff = lon - lov
cond do
diff > 180.0 -> diff - 360.0
diff < -180.0 -> diff + 360.0
true -> diff
end
end
end