Move the /weather render path off raw HRRR profile decoding + per-cell SoundingParams + WeatherLayers derivation. The dominant cost was re-deriving the same scalar fields on every viewport pan and timeline scrub. Server side: - New Microwaveprop.Weather.ScalarFile persists pre-derived scalar rows on NFS, bucketed into 5°×5° chunk files under <base>/weather_scalars/<iso>/<lat_band>_<lon_band>.etf.gz. Viewport reads only decode the chunks that overlap the requested bounds; point-detail clicks read exactly one chunk. - weather_grid_at/2 and weather_point_detail/3 prefer ScalarFile; ProfilesFile is now the cold-start fallback only. - warm_grid_cache_and_broadcast/1 and warm_grid_cache_from_latest_profile/0 also persist the scalar artifact, and the cold-derive path kicks off a per-valid_time-locked async materialization so the next reader gets the cheap path. - NotifyListener.handle_propagation_ready/1 fires Weather.materialize_scalar_file/1 in a detached Task on the Rust pipeline's NOTIFY propagation_ready, so steady-state forecast hours arrive with their scalar artifact already on disk. - available_weather_valid_times/0 unions ScalarFile + ProfilesFile so the timeline survives an aggressive retention sweep on either side. Browser side (finding #8): - Mount the legend Leaflet control once and patch its inner content on layer changes instead of remove + re-add on every renderLayer. - renderTimeline now keys off the rendered button set: selection-only updates take an applyTimelineSelection patch path that restyles existing buttons in place. Full innerHTML rebuild only fires when timelineData itself changes. Also fixes a credo nesting-depth flag in tile_renderer.ex by extracting interpolate_pair/2 from the inner anonymous function.
28 lines
999 B
Elixir
28 lines
999 B
Elixir
defmodule MicrowavepropWeb.WeatherTileController do
|
|
use MicrowavepropWeb, :controller
|
|
|
|
alias Microwaveprop.Weather.MapLayers
|
|
alias Microwaveprop.Weather.TileRenderer
|
|
|
|
@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
|
|
end
|