161 lines
5.4 KiB
Elixir
161 lines
5.4 KiB
Elixir
defmodule AprsmeWeb.Api.V1.WeatherController do
|
|
@moduledoc """
|
|
Controller for weather-related API endpoints.
|
|
"""
|
|
use AprsmeWeb, :controller
|
|
|
|
alias Aprsme.ApiMetrics
|
|
alias Aprsme.Packets.PreparedQueries
|
|
alias Aprsme.PlausibleAnalytics
|
|
|
|
action_fallback AprsmeWeb.Api.V1.FallbackController
|
|
|
|
@doc """
|
|
Returns nearby weather stations within a specified radius.
|
|
|
|
## Parameters
|
|
|
|
* `lat` - Latitude (-90 to 90) - required
|
|
* `lon` - Longitude (-180 to 180) - required
|
|
* `radius` - Search radius in miles (> 0, max 1000) - required
|
|
* `hours` - Hours of data to retrieve (1-168, default: 6) - optional
|
|
* `limit` - Maximum number of results (1-100, default: 50) - optional
|
|
|
|
## Examples
|
|
|
|
GET /api/v1/weather/nearby?lat=37.7749&lon=-122.4194&radius=10
|
|
GET /api/v1/weather/nearby?lat=37.7749&lon=-122.4194&radius=10&hours=24&limit=25
|
|
|
|
"""
|
|
def nearby(conn, params) do
|
|
PlausibleAnalytics.track(conn, "api_weather_nearby")
|
|
ApiMetrics.track(%{}, "api_weather_nearby")
|
|
|
|
with {:ok, validated_params} <- validate_params(params) do
|
|
stations =
|
|
PreparedQueries.get_nearby_weather_stations(
|
|
validated_params.lat,
|
|
validated_params.lon,
|
|
validated_params.radius,
|
|
hours: validated_params.hours,
|
|
limit: validated_params.limit
|
|
)
|
|
|
|
# Transform params for JSON response
|
|
response_params = %{
|
|
latitude: validated_params.lat,
|
|
longitude: validated_params.lon,
|
|
radius_miles: validated_params.radius,
|
|
hours: validated_params.hours,
|
|
limit: validated_params.limit
|
|
}
|
|
|
|
render(conn, :nearby,
|
|
stations: stations,
|
|
params: response_params
|
|
)
|
|
end
|
|
end
|
|
|
|
# Private Functions
|
|
|
|
@spec validate_params(map()) ::
|
|
{:ok,
|
|
%{
|
|
lat: float(),
|
|
lon: float(),
|
|
radius: float(),
|
|
hours: integer(),
|
|
limit: integer()
|
|
}}
|
|
| {:error, :bad_request, String.t()}
|
|
| {:error, :unprocessable_entity, String.t()}
|
|
defp validate_params(params) do
|
|
with {:ok, lat} <- validate_required_float(params, "lat"),
|
|
{:ok, lon} <- validate_required_float(params, "lon"),
|
|
{:ok, radius} <- validate_required_float(params, "radius"),
|
|
{:ok, hours} <- validate_optional_integer(params, "hours", 6),
|
|
{:ok, limit} <- validate_optional_integer(params, "limit", 50),
|
|
:ok <- validate_latitude(lat),
|
|
:ok <- validate_longitude(lon),
|
|
:ok <- validate_radius(radius),
|
|
:ok <- validate_hours(hours),
|
|
:ok <- validate_limit(limit) do
|
|
{:ok,
|
|
%{
|
|
lat: lat,
|
|
lon: lon,
|
|
radius: radius,
|
|
hours: hours,
|
|
limit: limit
|
|
}}
|
|
end
|
|
end
|
|
|
|
@spec validate_required_float(map(), String.t()) ::
|
|
{:ok, float()} | {:error, :bad_request, String.t()}
|
|
defp validate_required_float(params, key) do
|
|
case Map.get(params, key) do
|
|
nil ->
|
|
{:error, :bad_request, "Missing required parameter: #{key}"}
|
|
|
|
value when is_binary(value) ->
|
|
case Float.parse(value) do
|
|
{float_value, ""} -> {:ok, float_value}
|
|
_ -> {:error, :bad_request, "Invalid numeric value for parameter: #{key}"}
|
|
end
|
|
|
|
value when is_number(value) ->
|
|
{:ok, value / 1}
|
|
end
|
|
end
|
|
|
|
@spec validate_optional_integer(map(), <<_::40>>, 6 | 50) ::
|
|
{:ok, integer()} | {:error, :bad_request, <<_::64, _::_*8>>}
|
|
defp validate_optional_integer(params, key, default) do
|
|
case Map.get(params, key) do
|
|
nil ->
|
|
{:ok, default}
|
|
|
|
value when is_binary(value) ->
|
|
case Integer.parse(value) do
|
|
{int_value, ""} -> {:ok, int_value}
|
|
_ -> {:error, :bad_request, "Invalid integer value for parameter: #{key}"}
|
|
end
|
|
|
|
value when is_integer(value) ->
|
|
{:ok, value}
|
|
|
|
value when is_float(value) ->
|
|
{:ok, trunc(value)}
|
|
end
|
|
end
|
|
|
|
@spec validate_latitude(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
|
|
defp validate_latitude(lat) when lat >= -90 and lat <= 90, do: :ok
|
|
|
|
defp validate_latitude(_lat), do: {:error, :unprocessable_entity, "Invalid latitude: must be between -90 and 90"}
|
|
|
|
@spec validate_longitude(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
|
|
defp validate_longitude(lon) when lon >= -180 and lon <= 180, do: :ok
|
|
|
|
defp validate_longitude(_lon), do: {:error, :unprocessable_entity, "Invalid longitude: must be between -180 and 180"}
|
|
|
|
@spec validate_radius(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
|
|
defp validate_radius(radius) when radius > 0 and radius <= 1000, do: :ok
|
|
|
|
defp validate_radius(radius) when radius <= 0,
|
|
do: {:error, :unprocessable_entity, "Invalid radius: must be greater than 0"}
|
|
|
|
defp validate_radius(_radius), do: {:error, :unprocessable_entity, "Invalid radius: must not exceed 1000 miles"}
|
|
|
|
@spec validate_hours(integer()) :: :ok | {:error, :unprocessable_entity, String.t()}
|
|
defp validate_hours(hours) when hours >= 1 and hours <= 168, do: :ok
|
|
|
|
defp validate_hours(_hours), do: {:error, :unprocessable_entity, "Invalid hours: must be between 1 and 168"}
|
|
|
|
@spec validate_limit(integer()) :: :ok | {:error, :unprocessable_entity, String.t()}
|
|
defp validate_limit(limit) when limit >= 1 and limit <= 100, do: :ok
|
|
|
|
defp validate_limit(_limit), do: {:error, :unprocessable_entity, "Invalid limit: must be between 1 and 100"}
|
|
end
|