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.
117 lines
3.1 KiB
Elixir
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
|