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.
91 lines
2.9 KiB
Elixir
91 lines
2.9 KiB
Elixir
defmodule MicrowavepropWeb.WeatherTileController do
|
|
use MicrowavepropWeb, :controller
|
|
|
|
alias Microwaveprop.Weather
|
|
alias Microwaveprop.Weather.MapLayers
|
|
alias Microwaveprop.Weather.TileRenderer
|
|
|
|
# Fields shipped to the client. The client recolors locally on layer
|
|
# switch from this same payload, so include every field any layer
|
|
# might need.
|
|
@cell_fields ~w(
|
|
temperature dewpoint_depression surface_rh surface_pressure_mb
|
|
surface_refractivity refractivity_gradient bl_height pwat
|
|
temp_850mb dewpoint_850mb temp_700mb dewpoint_700mb
|
|
lapse_rate mid_lapse_rate inversion_strength inversion_base_m
|
|
ducting duct_base_m duct_strength
|
|
)a
|
|
|
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def show(conn, %{"z" => z, "x" => x, "y" => y, "layer" => layer_id, "time" => time_param}) do
|
|
with true <- MapLayers.valid_id?(layer_id),
|
|
{z, ""} <- Integer.parse(z),
|
|
{x, ""} <- Integer.parse(x),
|
|
{y, ""} <- Integer.parse(y),
|
|
{:ok, valid_time, _offset} <- DateTime.from_iso8601(time_param) do
|
|
body = TileRenderer.render_svg(valid_time, layer_id, z, x, y)
|
|
|
|
conn
|
|
|> put_resp_content_type("image/svg+xml")
|
|
|> put_resp_header("cache-control", "public, max-age=60")
|
|
|> send_resp(200, body)
|
|
else
|
|
_ ->
|
|
conn
|
|
|> put_resp_content_type("image/svg+xml")
|
|
|> put_resp_header("cache-control", "public, max-age=5")
|
|
|> send_resp(200, TileRenderer.empty_svg())
|
|
end
|
|
end
|
|
|
|
@spec cells(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def cells(conn, %{"time" => time_param} = params) do
|
|
with {:ok, valid_time, _offset} <- DateTime.from_iso8601(time_param),
|
|
{:ok, bounds} <- parse_bounds(params) do
|
|
rows = Weather.weather_grid_at(valid_time, bounds)
|
|
|
|
conn
|
|
|> put_resp_header("cache-control", "public, max-age=60")
|
|
|> json(%{
|
|
valid_time: DateTime.to_iso8601(valid_time),
|
|
cells: Enum.map(rows, &cell_payload/1)
|
|
})
|
|
else
|
|
_ -> bad_request(conn)
|
|
end
|
|
end
|
|
|
|
def cells(conn, _params), do: bad_request(conn)
|
|
|
|
defp bad_request(conn) do
|
|
conn
|
|
|> put_status(400)
|
|
|> json(%{error: "invalid params"})
|
|
end
|
|
|
|
defp parse_bounds(%{"south" => s, "north" => n, "west" => w, "east" => e}) do
|
|
with {:ok, south} <- parse_float(s),
|
|
{:ok, north} <- parse_float(n),
|
|
{:ok, west} <- parse_float(w),
|
|
{:ok, east} <- parse_float(e) do
|
|
{:ok, %{"south" => south, "north" => north, "west" => west, "east" => east}}
|
|
end
|
|
end
|
|
|
|
defp parse_bounds(_), do: :error
|
|
|
|
defp parse_float(value) when is_binary(value) do
|
|
case Float.parse(value) do
|
|
{f, ""} -> {:ok, f}
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
defp parse_float(_), do: :error
|
|
|
|
defp cell_payload(row) do
|
|
Enum.reduce(@cell_fields, %{lat: row.lat, lon: row.lon}, fn field, acc ->
|
|
Map.put(acc, field, Map.get(row, field))
|
|
end)
|
|
end
|
|
end
|