Add weather grid query functions for /weather map page

This commit is contained in:
Graham McIntire 2026-04-03 15:08:21 -05:00
parent 411951147a
commit 0dd695c574
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 245 additions and 5 deletions

View file

@ -3,17 +3,17 @@ defmodule Microwaveprop.Weather do
import Ecto.Query import Ecto.Query
require Logger
alias Microwaveprop.Repo alias Microwaveprop.Repo
alias Microwaveprop.Weather.IemClient
alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.IemClient
alias Microwaveprop.Weather.IemreObservation alias Microwaveprop.Weather.IemreObservation
alias Microwaveprop.Weather.SolarIndex alias Microwaveprop.Weather.SolarIndex
alias Microwaveprop.Weather.Sounding alias Microwaveprop.Weather.Sounding
alias Microwaveprop.Weather.Station alias Microwaveprop.Weather.Station
alias Microwaveprop.Weather.SurfaceObservation alias Microwaveprop.Weather.SurfaceObservation
require Logger
# Approximate km per degree latitude # Approximate km per degree latitude
@km_per_deg_lat 111.0 @km_per_deg_lat 111.0
@ -97,7 +97,10 @@ defmodule Microwaveprop.Weather do
end end
def sync_stations! do def sync_stations! do
asos = for s <- ~w(AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY), do: "#{s}_ASOS" asos =
for s <-
~w(AK AL AR AZ CA CO CT DE FL GA HI IA ID IL IN KS KY LA MA MD ME MI MN MO MS MT NC ND NE NH NJ NM NV NY OH OK OR PA RI SC SD TN TX UT VA VT WA WI WV WY),
do: "#{s}_ASOS"
for network <- asos ++ ["RAOB"] do for network <- asos ++ ["RAOB"] do
type = if String.contains?(network, "ASOS"), do: "asos", else: "sounding" type = if String.contains?(network, "ASOS"), do: "asos", else: "sounding"
@ -198,6 +201,72 @@ defmodule Microwaveprop.Weather do
%{surface_observations: surface_observations, soundings: soundings} %{surface_observations: surface_observations, soundings: soundings}
end end
def latest_grid_valid_time do
Repo.one(
from(h in HrrrProfile,
where:
fragment("(? * 1000)::int % 125 = 0", h.lat) and
fragment("(? * 1000)::int % 125 = 0", h.lon),
select: max(h.valid_time)
)
)
end
def latest_weather_grid(bounds) do
case latest_grid_valid_time() do
nil ->
[]
latest_vt ->
Repo.all(
from(h in HrrrProfile,
where:
h.valid_time == ^latest_vt and
h.lat >= ^bounds["south"] and h.lat <= ^bounds["north"] and
h.lon >= ^bounds["west"] and h.lon <= ^bounds["east"] and
fragment("(? * 1000)::int % 125 = 0", h.lat) and
fragment("(? * 1000)::int % 125 = 0", h.lon),
select: %{
lat: h.lat,
lon: h.lon,
valid_time: h.valid_time,
temperature: h.surface_temp_c,
dewpoint_depression: fragment("? - ?", h.surface_temp_c, h.surface_dewpoint_c),
bl_height: h.hpbl_m,
pwat: h.pwat_mm,
refractivity_gradient: h.min_refractivity_gradient,
ducting: h.ducting_detected,
surface_pressure_mb: h.surface_pressure_mb
}
)
)
end
end
def weather_point_detail(lat, lon, valid_time) do
step = 0.125
snapped_lat = Float.round(Float.round(lat / step) * step, 3)
snapped_lon = Float.round(Float.round(lon / step) * step, 3)
Repo.one(
from(h in HrrrProfile,
where: h.lat == ^snapped_lat and h.lon == ^snapped_lon and h.valid_time == ^valid_time,
select: %{
lat: h.lat,
lon: h.lon,
valid_time: h.valid_time,
temperature: h.surface_temp_c,
dewpoint_depression: fragment("? - ?", h.surface_temp_c, h.surface_dewpoint_c),
bl_height: h.hpbl_m,
pwat: h.pwat_mm,
refractivity_gradient: h.min_refractivity_gradient,
ducting: h.ducting_detected,
surface_pressure_mb: h.surface_pressure_mb
}
)
)
end
def upsert_hrrr_profile(attrs) do def upsert_hrrr_profile(attrs) do
%HrrrProfile{} %HrrrProfile{}
|> HrrrProfile.changeset(attrs) |> HrrrProfile.changeset(attrs)
@ -210,7 +279,9 @@ defmodule Microwaveprop.Weather do
def upsert_hrrr_profiles_batch(profiles, opts \\ []) do def upsert_hrrr_profiles_batch(profiles, opts \\ []) do
now = DateTime.truncate(DateTime.utc_now(), :second) now = DateTime.truncate(DateTime.utc_now(), :second)
on_conflict = if Keyword.get(opts, :skip_existing, false), do: :nothing, else: {:replace_all_except, [:id, :inserted_at]}
on_conflict =
if Keyword.get(opts, :skip_existing, false), do: :nothing, else: {:replace_all_except, [:id, :inserted_at]}
profiles profiles
|> Enum.chunk_every(500) |> Enum.chunk_every(500)

View file

@ -0,0 +1,169 @@
defmodule Microwaveprop.WeatherGridTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Repo
alias Microwaveprop.Weather
describe "latest_weather_grid/1" do
test "returns grid data for latest valid_time within bounds" do
now = DateTime.truncate(DateTime.utc_now(), :second)
insert_hrrr_profile(%{
lat: 32.125,
lon: -97.375,
valid_time: now,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
hpbl_m: 500.0,
pwat_mm: 30.0,
min_refractivity_gradient: -280.0,
ducting_detected: true
})
insert_hrrr_profile(%{
lat: 32.250,
lon: -97.375,
valid_time: now,
surface_temp_c: 26.0,
surface_dewpoint_c: 19.0,
surface_pressure_mb: 1012.0,
hpbl_m: 600.0,
pwat_mm: 32.0,
min_refractivity_gradient: -150.0,
ducting_detected: false
})
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
result = Weather.latest_weather_grid(bounds)
assert length(result) == 2
point = Enum.find(result, &(&1.lat == 32.125))
assert point.temperature == 25.0
assert point.dewpoint_depression == 7.0
assert point.bl_height == 500.0
assert point.pwat == 30.0
assert point.refractivity_gradient == -280.0
assert point.ducting == true
end
test "returns empty list when no data exists" do
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
assert Weather.latest_weather_grid(bounds) == []
end
test "only returns points on 0.125 grid" do
now = DateTime.truncate(DateTime.utc_now(), :second)
insert_hrrr_profile(%{
lat: 32.125,
lon: -97.375,
valid_time: now,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
hpbl_m: 500.0,
pwat_mm: 30.0
})
insert_hrrr_profile(%{
lat: 32.1337,
lon: -97.3821,
valid_time: now,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
hpbl_m: 500.0,
pwat_mm: 30.0
})
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
result = Weather.latest_weather_grid(bounds)
assert length(result) == 1
assert hd(result).lat == 32.125
end
test "uses most recent valid_time only" do
old = DateTime.utc_now() |> DateTime.add(-7200, :second) |> DateTime.truncate(:second)
new = DateTime.truncate(DateTime.utc_now(), :second)
insert_hrrr_profile(%{
lat: 32.125,
lon: -97.375,
valid_time: old,
surface_temp_c: 20.0,
surface_dewpoint_c: 15.0,
surface_pressure_mb: 1010.0,
hpbl_m: 400.0,
pwat_mm: 25.0
})
insert_hrrr_profile(%{
lat: 32.125,
lon: -97.375,
valid_time: new,
surface_temp_c: 28.0,
surface_dewpoint_c: 20.0,
surface_pressure_mb: 1015.0,
hpbl_m: 700.0,
pwat_mm: 35.0
})
bounds = %{"south" => 32.0, "north" => 33.0, "west" => -98.0, "east" => -97.0}
result = Weather.latest_weather_grid(bounds)
assert length(result) == 1
assert hd(result).temperature == 28.0
end
end
describe "weather_point_detail/3" do
test "returns all fields for a single grid point" do
now = DateTime.truncate(DateTime.utc_now(), :second)
insert_hrrr_profile(%{
lat: 32.125,
lon: -97.375,
valid_time: now,
surface_temp_c: 25.0,
surface_dewpoint_c: 18.0,
surface_pressure_mb: 1013.0,
hpbl_m: 500.0,
pwat_mm: 30.0,
min_refractivity_gradient: -280.0,
ducting_detected: true
})
result = Weather.weather_point_detail(32.15, -97.40, now)
assert result.lat == 32.125
assert result.temperature == 25.0
assert result.surface_pressure_mb == 1013.0
end
test "returns nil when no data at point" do
now = DateTime.truncate(DateTime.utc_now(), :second)
assert Weather.weather_point_detail(32.15, -97.40, now) == nil
end
end
defp insert_hrrr_profile(attrs) do
now = DateTime.truncate(DateTime.utc_now(), :second)
defaults = %{
id: Ecto.UUID.bingenerate(),
run_time: DateTime.add(attrs.valid_time, -3600, :second),
profile: [],
surface_refractivity: nil,
min_refractivity_gradient: nil,
ducting_detected: nil,
duct_characteristics: nil,
inserted_at: now,
updated_at: now
}
entry = Map.merge(defaults, attrs)
Repo.insert_all("hrrr_profiles", [entry])
end
end