Make point detail popup instant with client-side lookup and range circles
- Scores now include factors and valid_time in the viewport query, eliminating the server round-trip for popups - Click shows two range circles: solid inner (typical range) and dashed outer (max estimated range), colored by score tier - Circles disappear when popup closes - Band info pushed to client on band switch for accurate range estimates
This commit is contained in:
parent
0c295c1558
commit
2e216d4244
3 changed files with 88 additions and 34 deletions
|
|
@ -105,8 +105,18 @@ export const PropagationMap = {
|
|||
this.renderScores(scores)
|
||||
})
|
||||
|
||||
this.handleEvent("point_detail", ({ detail }) => {
|
||||
this.bandInfo = JSON.parse(this.el.dataset.bandInfo || "{}")
|
||||
this.rangeCircles = L.layerGroup().addTo(this.map)
|
||||
|
||||
this.handleEvent("update_band_info", ({ band_info }) => {
|
||||
this.bandInfo = band_info
|
||||
})
|
||||
|
||||
// Click on map to show factor breakdown + range circle — fully client-side
|
||||
this.map.on("click", (e) => {
|
||||
const detail = this.lookupPoint(e.latlng.lat, e.latlng.lng)
|
||||
if (detail) {
|
||||
this.showRangeCircles(detail)
|
||||
this.detailPopup
|
||||
.setLatLng([detail.lat, detail.lon])
|
||||
.setContent(buildPopupHTML(detail))
|
||||
|
|
@ -114,10 +124,8 @@ export const PropagationMap = {
|
|||
}
|
||||
})
|
||||
|
||||
// Click on map to get point detail
|
||||
this.map.on("click", (e) => {
|
||||
this.pushEvent("point_detail", { lat: e.latlng.lat, lon: e.latlng.lng })
|
||||
})
|
||||
// Clear range circles when popup closes
|
||||
this.map.on("popupclose", () => this.rangeCircles.clearLayers())
|
||||
|
||||
this.el.addEventListener("show-loading", () => topbar.show(300))
|
||||
|
||||
|
|
@ -153,8 +161,11 @@ export const PropagationMap = {
|
|||
if (scores.length === 0) return
|
||||
|
||||
this.gridLookup = new Map()
|
||||
scores.forEach(({ lat, lon, score }) => {
|
||||
this.gridLookup.set(`${lat.toFixed(3)},${lon.toFixed(3)}`, score)
|
||||
this.gridData = new Map()
|
||||
scores.forEach((s) => {
|
||||
const key = `${s.lat.toFixed(3)},${s.lon.toFixed(3)}`
|
||||
this.gridLookup.set(key, s.score)
|
||||
this.gridData.set(key, s)
|
||||
})
|
||||
|
||||
const self = this
|
||||
|
|
@ -243,6 +254,62 @@ export const PropagationMap = {
|
|||
return { r: last.r, g: last.g, b: last.b }
|
||||
},
|
||||
|
||||
lookupPoint(lat, lon) {
|
||||
if (!this.gridData) return null
|
||||
const step = 0.125
|
||||
const rlat = (Math.round(lat / step) * step).toFixed(3)
|
||||
const rlon = (Math.round(lon / step) * step).toFixed(3)
|
||||
const data = this.gridData.get(`${rlat},${rlon}`)
|
||||
if (!data) return null
|
||||
return { ...data, ...this.bandInfo }
|
||||
},
|
||||
|
||||
showRangeCircles(detail) {
|
||||
this.rangeCircles.clearLayers()
|
||||
const center = [detail.lat, detail.lon]
|
||||
const score = detail.score
|
||||
const tier = scoreTier(score)
|
||||
|
||||
// Compute estimated range in km based on score tier
|
||||
let rangeKm
|
||||
if (score >= 80) rangeKm = detail.exceptional_range_km
|
||||
else if (score >= 65) rangeKm = detail.extended_range_km
|
||||
else if (score >= 50) rangeKm = detail.typical_range_km
|
||||
else if (score >= 33) rangeKm = Math.round(detail.typical_range_km * 0.6)
|
||||
else rangeKm = Math.round(detail.typical_range_km * 0.3)
|
||||
|
||||
// Outer circle — max estimated range (faded)
|
||||
L.circle(center, {
|
||||
radius: rangeKm * 1000,
|
||||
color: tier.color,
|
||||
weight: 2,
|
||||
opacity: 0.6,
|
||||
fillColor: tier.color,
|
||||
fillOpacity: 0.06,
|
||||
dashArray: "6,4"
|
||||
}).addTo(this.rangeCircles)
|
||||
|
||||
// Inner circle — typical range (more visible)
|
||||
const innerKm = Math.round(rangeKm * 0.6)
|
||||
L.circle(center, {
|
||||
radius: innerKm * 1000,
|
||||
color: tier.color,
|
||||
weight: 2,
|
||||
opacity: 0.8,
|
||||
fillColor: tier.color,
|
||||
fillOpacity: 0.1
|
||||
}).addTo(this.rangeCircles)
|
||||
|
||||
// Center marker
|
||||
L.circleMarker(center, {
|
||||
radius: 5,
|
||||
color: "#fff",
|
||||
weight: 2,
|
||||
fillColor: tier.color,
|
||||
fillOpacity: 1
|
||||
}).addTo(this.rangeCircles)
|
||||
},
|
||||
|
||||
sendBounds() {
|
||||
topbar.show(300)
|
||||
const b = this.map.getBounds()
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ defmodule Microwaveprop.Propagation do
|
|||
latest_time ->
|
||||
from(gs in GridScore,
|
||||
where: gs.band_mhz == ^band_mhz and gs.valid_time == ^latest_time,
|
||||
select: %{lat: gs.lat, lon: gs.lon, score: gs.score}
|
||||
select: %{lat: gs.lat, lon: gs.lon, score: gs.score, factors: gs.factors, valid_time: gs.valid_time}
|
||||
)
|
||||
|> maybe_filter_bounds(bounds)
|
||||
|> Repo.all()
|
||||
|
|
|
|||
|
|
@ -37,36 +37,11 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
|> assign(:selected_band, band)
|
||||
|> assign(:scores, scores)
|
||||
|> push_event("update_scores", %{scores: scores})
|
||||
|> push_event("update_band_info", %{band_info: band_info(band)})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do
|
||||
band = socket.assigns.selected_band
|
||||
|
||||
case Propagation.point_detail(band, lat, lon) do
|
||||
nil ->
|
||||
{:noreply, push_event(socket, "point_detail", %{detail: nil})}
|
||||
|
||||
detail ->
|
||||
band_config = BandConfig.get(band)
|
||||
|
||||
payload = %{
|
||||
lat: detail.lat,
|
||||
lon: detail.lon,
|
||||
score: detail.score,
|
||||
factors: detail.factors,
|
||||
valid_time: DateTime.to_iso8601(detail.valid_time),
|
||||
band_label: band_config.label,
|
||||
typical_range_km: band_config.typical_range_km,
|
||||
extended_range_km: band_config.extended_range_km,
|
||||
exceptional_range_km: band_config.exceptional_range_km
|
||||
}
|
||||
|
||||
{:noreply, push_event(socket, "point_detail", %{detail: payload})}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("map_bounds", bounds, socket) do
|
||||
scores = Propagation.latest_scores(socket.assigns.selected_band, bounds)
|
||||
|
||||
|
|
@ -92,6 +67,17 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp band_info(band_mhz) do
|
||||
config = BandConfig.get(band_mhz)
|
||||
|
||||
%{
|
||||
band_label: config.label,
|
||||
typical_range_km: config.typical_range_km,
|
||||
extended_range_km: config.extended_range_km,
|
||||
exceptional_range_km: config.exceptional_range_km
|
||||
}
|
||||
end
|
||||
|
||||
defp time_ago(dt) do
|
||||
seconds = DateTime.diff(DateTime.utc_now(), dt)
|
||||
|
||||
|
|
@ -120,6 +106,7 @@ defmodule MicrowavepropWeb.MapLive do
|
|||
phx-hook="PropagationMap"
|
||||
phx-update="ignore"
|
||||
data-scores="[]"
|
||||
data-band-info={Jason.encode!(band_info(@selected_band))}
|
||||
class="absolute inset-0 z-0"
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue