Add Data Sources section to contact detail page

Shows provenance of all data used in the analysis:
- HRRR: path profile count, pressure levels, valid time, grid point
- Elevation: sample count, path distance, source (SRTM), duct layers
- Terrain: sample count, verdict, max elevation, diffraction loss
- Surface observations: station count and time range
- Soundings: profile count, stations, levels, ducting detection
- Solar indices: SFI, SSN, Ap
This commit is contained in:
Graham McIntire 2026-04-02 13:28:58 -05:00
parent 2452f1f397
commit faa23a9693
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -38,6 +38,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
contact = maybe_enqueue_weather(weather, contact)
elevation_profile = compute_elevation_profile(contact, hrrr_path, weather.soundings)
propagation_analysis = build_propagation_analysis(contact, hrrr, terrain, elevation_profile, weather.soundings)
data_sources = build_data_sources(hrrr, hrrr_path, terrain, weather, elevation_profile)
{:ok,
assign(socket,
@ -50,6 +51,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
terrain: terrain,
elevation_profile: elevation_profile,
propagation_analysis: propagation_analysis,
data_sources: data_sources,
terrain_expanded: false,
hrrr_profile_expanded: false,
obs_sort_by: "station_name",
@ -776,6 +778,84 @@ defmodule MicrowavepropWeb.ContactLive.Show do
</table>
</div>
<% end %>
<%= if @data_sources do %>
<div class="divider" />
<h2 class="text-base font-semibold mb-2">Data Sources</h2>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-xs">
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">HRRR Model</div>
<%= if @data_sources.hrrr do %>
<div>{@data_sources.hrrr.profile_count} path profiles</div>
<div>{@data_sources.hrrr.levels} pressure levels each</div>
<div>Valid: {@data_sources.hrrr.valid_time}</div>
<div>Grid: {@data_sources.hrrr.lat}, {@data_sources.hrrr.lon}</div>
<% else %>
<div class="opacity-50">Not available</div>
<% end %>
</div>
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">Elevation Profile</div>
<%= if @data_sources.elevation do %>
<div>{@data_sources.elevation.samples} samples along path</div>
<div>{@data_sources.elevation.dist} path distance</div>
<div>Source: {@data_sources.elevation.source}</div>
<div>{@data_sources.elevation.duct_count} duct layers detected</div>
<% else %>
<div class="opacity-50">Not available</div>
<% end %>
</div>
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">Terrain Analysis</div>
<%= if @data_sources.terrain do %>
<div>{@data_sources.terrain.samples} profile samples</div>
<div>Verdict: {@data_sources.terrain.verdict}</div>
<div>Max elevation: {@data_sources.terrain.max_elev} m</div>
<div>Diffraction: {@data_sources.terrain.diffraction} dB</div>
<% else %>
<div class="opacity-50">Not available</div>
<% end %>
</div>
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">Surface Observations</div>
<div>{length(@surface_observations)} stations</div>
<%= if @surface_observations != [] do %>
<div>
{Calendar.strftime(hd(@surface_observations).observed_at, "%H:%M")}
{Calendar.strftime(List.last(@surface_observations).observed_at, "%H:%M")} UTC
</div>
<% end %>
</div>
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">Soundings (RAOB)</div>
<div>{length(@soundings)} profiles</div>
<%= if @soundings != [] do %>
<div>
{Enum.map_join(@soundings, ", ", fn s ->
"#{s.station.station_code} (#{s.level_count} levels)"
end)}
</div>
<div>{Enum.count(@soundings, & &1.ducting_detected)} with ducting</div>
<% end %>
</div>
<div class="bg-base-200 rounded-box p-3">
<div class="font-semibold mb-1">Solar Indices</div>
<%= if @solar do %>
<div>SFI: {@solar.sfi || ""}</div>
<div>SSN: {@solar.sunspot_number || ""}</div>
<div>Ap: {@solar.ap_index || ""}</div>
<% else %>
<div class="opacity-50">Not available</div>
<% end %>
</div>
</div>
<% end %>
</Layouts.app>
"""
end
@ -1081,6 +1161,54 @@ defmodule MicrowavepropWeb.ContactLive.Show do
defp rad_to_deg(rad), do: rad * 180 / :math.pi()
defp rem_float(a, b), do: a - Float.floor(a / b) * b
# ── Data sources summary ────────────────────────────────────────
defp build_data_sources(hrrr, hrrr_path, terrain, weather, elevation_profile) do
%{
hrrr: build_hrrr_source(hrrr, hrrr_path),
elevation: build_elevation_source(elevation_profile),
terrain: build_terrain_source(terrain),
obs_count: length(weather.surface_observations),
sounding_count: length(weather.soundings)
}
end
defp build_hrrr_source(nil, _), do: nil
defp build_hrrr_source(hrrr, hrrr_path) do
levels = if hrrr.profile, do: length(hrrr.profile), else: 0
%{
profile_count: length(hrrr_path),
levels: levels,
valid_time: Calendar.strftime(hrrr.valid_time, "%Y-%m-%d %H:%M UTC"),
lat: :erlang.float_to_binary(hrrr.lat / 1, decimals: 2),
lon: :erlang.float_to_binary(hrrr.lon / 1, decimals: 2)
}
end
defp build_elevation_source(nil), do: nil
defp build_elevation_source(ep) do
%{
samples: length(ep.points),
dist: format_dist(ep.dist_km),
source: "SRTM 1-arcsecond",
duct_count: length(ep.ducts || [])
}
end
defp build_terrain_source(nil), do: nil
defp build_terrain_source(terrain) do
%{
samples: terrain.sample_count,
verdict: terrain.verdict,
max_elev: :erlang.float_to_binary((terrain.max_elevation_m || 0) / 1, decimals: 0),
diffraction: :erlang.float_to_binary((terrain.diffraction_db || 0) / 1, decimals: 1)
}
end
# ── Propagation analysis ──────────────────────────────────────────
defp build_propagation_analysis(contact, hrrr, terrain, elevation_profile, soundings) do