prop/test/microwaveprop_web/controllers/weather_tile_controller_test.exs
Graham McIntire f4a177c9d0
perf(weather): single-fetch viewport cells, client-side recolor on layer switch
Previously every layer toggle triggered 21 fresh tile requests with the
new layer in the URL — ~21 server-side SVG renders, plus the round-trip
overhead. Even with the GridCache fix the server still spent real CPU
generating thousands of <rect>s per tile, and the user felt it.

Add /weather/cells: a single JSON endpoint returning every layer field
for every cell in the requested viewport. The hook fetches once on
mount, time change, or pan/zoom, caches the cells in memory, and paints
them via L.svgOverlay. Layer switches now re-color the same cached
cells locally — zero network, zero server CPU.

The single SVG uses lat/lon coords with preserveAspectRatio="none";
Leaflet stretches it linearly to the bbox. There is mild equirect→
mercator distortion at low zoom over CONUS but it's imperceptible at
typical use (z6+) and the layer-toggle UX win is large.

Tile endpoint kept for back-compat. svgOverlay opacity matches the
prior tile renderer's 0.55 fill-opacity.
2026-04-29 13:31:17 -05:00

117 lines
3.1 KiB
Elixir

defmodule MicrowavepropWeb.WeatherTileControllerTest do
use MicrowavepropWeb.ConnCase, async: false
alias Microwaveprop.Propagation.ProfilesFile
alias Microwaveprop.Weather.GridCache
setup do
GridCache.clear()
on_exit(fn ->
GridCache.clear()
ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z])
end)
:ok
end
test "serves an SVG weather tile for the requested viewport", %{conn: conn} do
valid_time = ~U[2026-04-28 12:00:00Z]
lat = 33.0
lon = -97.0
ProfilesFile.write!(valid_time, %{
{lat, lon} => %{
surface_temp_c: 22.0,
surface_dewpoint_c: 12.0,
surface_pressure_mb: 1010.0,
hpbl_m: 800.0,
pwat_mm: 25.0,
profile: []
}
})
conn =
get(
conn,
~p"/weather/tiles/0/0/0?layer=temperature&time=#{DateTime.to_iso8601(valid_time)}"
)
assert response(conn, 200) =~ "<svg"
assert response(conn, 200) =~ "<rect"
assert ["image/svg+xml; charset=utf-8"] = get_resp_header(conn, "content-type")
end
test "returns an empty svg for invalid params", %{conn: conn} do
conn = get(conn, ~p"/weather/tiles/6/18/25?layer=bogus&time=not-a-time")
body = response(conn, 200)
assert body =~ "<svg"
refute body =~ "<rect"
end
describe "GET /weather/cells" do
test "returns viewport cells as JSON", %{conn: conn} do
valid_time = ~U[2026-04-28 12:00:00Z]
ProfilesFile.write!(valid_time, %{
{33.0, -97.0} => %{
surface_temp_c: 22.0,
surface_dewpoint_c: 12.0,
surface_pressure_mb: 1010.0,
hpbl_m: 800.0,
pwat_mm: 25.0,
profile: []
},
{33.125, -97.0} => %{
surface_temp_c: 23.0,
surface_dewpoint_c: 13.0,
surface_pressure_mb: 1011.0,
hpbl_m: 700.0,
pwat_mm: 26.0,
profile: []
}
})
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=32.5&north=33.5&west=-97.5&east=-96.5"
)
assert ["application/json; charset=utf-8"] = get_resp_header(conn, "content-type")
body = json_response(conn, 200)
assert is_list(body["cells"])
assert length(body["cells"]) == 2
assert body["valid_time"] == time
first = hd(body["cells"])
assert is_number(first["lat"])
assert is_number(first["lon"])
assert Map.has_key?(first, "temperature")
assert Map.has_key?(first, "dewpoint_depression")
end
test "returns 400 for missing or invalid params", %{conn: conn} do
conn = get(conn, ~p"/weather/cells?time=not-a-time")
assert json_response(conn, 400)
end
test "returns empty cells when no data exists for the valid_time", %{conn: conn} do
valid_time = ~U[2099-04-28 12:00:00Z]
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=32.0&north=34.0&west=-98.0&east=-96.0"
)
body = json_response(conn, 200)
assert body["cells"] == []
end
end
end