Add grid toggle, use 0.25deg resolution for coverage scoring
- Toggle button for Maidenhead grid overlay (on by default) - Coverage candidates now at 0.25° resolution (~28 km) instead of 2°×1° Maidenhead grids, matching HRRR propagation grid density - Coverage rectangles render as 0.25° cells for finer detail - Grid overlay is separate from coverage coloring
This commit is contained in:
parent
79aca95bc1
commit
caa03fed53
3 changed files with 62 additions and 22 deletions
|
|
@ -19,6 +19,9 @@ export const RoverMap = {
|
||||||
this.stationMarkers.addTo(map)
|
this.stationMarkers.addTo(map)
|
||||||
this.coverageLayer.addTo(map)
|
this.coverageLayer.addTo(map)
|
||||||
|
|
||||||
|
this.gridLayer = L.layerGroup().addTo(map)
|
||||||
|
this.drawGridOverlay()
|
||||||
|
|
||||||
this.handleEvent("stations_updated", ({ stations }) => {
|
this.handleEvent("stations_updated", ({ stations }) => {
|
||||||
this.stations = stations
|
this.stations = stations
|
||||||
this.renderStations()
|
this.renderStations()
|
||||||
|
|
@ -27,6 +30,14 @@ export const RoverMap = {
|
||||||
this.handleEvent("coverage_updated", ({ grids }) => {
|
this.handleEvent("coverage_updated", ({ grids }) => {
|
||||||
this.renderCoverage(grids)
|
this.renderCoverage(grids)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.handleEvent("toggle_grids", ({ show }) => {
|
||||||
|
if (show) {
|
||||||
|
this.gridLayer.addTo(this.map)
|
||||||
|
} else {
|
||||||
|
this.map.removeLayer(this.gridLayer)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
renderStations() {
|
renderStations() {
|
||||||
|
|
@ -64,10 +75,9 @@ export const RoverMap = {
|
||||||
const maxScore = Math.max(...grids.map(g => g.coverage_score))
|
const maxScore = Math.max(...grids.map(g => g.coverage_score))
|
||||||
|
|
||||||
for (const g of grids) {
|
for (const g of grids) {
|
||||||
// 4-char Maidenhead: 2° lon × 1° lat
|
// Render as 0.25° cells centered on the candidate point
|
||||||
// Compute grid square bounds from the grid letters
|
const half = 0.125
|
||||||
const bounds = this.gridBounds(g.grid)
|
const bounds = [[g.lat - half, g.lon - half], [g.lat + half, g.lon + half]]
|
||||||
if (!bounds) continue
|
|
||||||
|
|
||||||
const normalized = maxScore > 0 ? g.coverage_score / maxScore : 0
|
const normalized = maxScore > 0 ? g.coverage_score / maxScore : 0
|
||||||
const color = this.coverageColor(normalized)
|
const color = this.coverageColor(normalized)
|
||||||
|
|
@ -149,6 +159,19 @@ export const RoverMap = {
|
||||||
return "#ff4f4f"
|
return "#ff4f4f"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
drawGridOverlay() {
|
||||||
|
for (let lat = 25; lat <= 50; lat += 1) {
|
||||||
|
L.polyline([[lat, -125], [lat, -66]], {
|
||||||
|
color: "#888", weight: 0.5, opacity: 0.3
|
||||||
|
}).addTo(this.gridLayer)
|
||||||
|
}
|
||||||
|
for (let lon = -125; lon <= -66; lon += 2) {
|
||||||
|
L.polyline([[25, lon], [50, lon]], {
|
||||||
|
color: "#888", weight: 0.5, opacity: 0.3
|
||||||
|
}).addTo(this.gridLayer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
fitBounds() {
|
fitBounds() {
|
||||||
const points = this.stations.map(s => [s.lat, s.lon])
|
const points = this.stations.map(s => [s.lat, s.lon])
|
||||||
if (points.length >= 2) {
|
if (points.length >= 2) {
|
||||||
|
|
|
||||||
|
|
@ -79,20 +79,21 @@ defmodule Microwaveprop.Rover.Coverage do
|
||||||
min_lon = max(Enum.min(lons) - range_deg, -125.0)
|
min_lon = max(Enum.min(lons) - range_deg, -125.0)
|
||||||
max_lon = min(Enum.max(lons) + range_deg, -66.0)
|
max_lon = min(Enum.max(lons) + range_deg, -66.0)
|
||||||
|
|
||||||
# Generate at 4-char Maidenhead resolution (1° lat × 2° lon)
|
# Generate candidates at 0.25° resolution (between HRRR 0.125° and Maidenhead 1-2°)
|
||||||
lat_range = Enum.to_list(trunc(min_lat)..trunc(max_lat))
|
# This gives ~16x more candidates than Maidenhead but stays manageable
|
||||||
lon_range = Enum.to_list(trunc(min_lon)..trunc(max_lon)//2)
|
step = 0.25
|
||||||
|
|
||||||
# Use center of grid square
|
for lat <- float_range(min_lat, max_lat, step),
|
||||||
for_result =
|
lon <- float_range(min_lon, max_lon, step) do
|
||||||
for lat <- lat_range, lon <- lon_range do
|
grid = Geo.latlon_to_grid4(lat, lon)
|
||||||
clat = lat + 0.5
|
{grid, Float.round(lat, 3), Float.round(lon, 3)}
|
||||||
clon = lon + 1.0
|
end
|
||||||
grid = Geo.latlon_to_grid4(clat, clon)
|
end
|
||||||
{grid, clat, clon}
|
|
||||||
end
|
|
||||||
|
|
||||||
Enum.uniq_by(for_result, fn {grid, _, _} -> grid end)
|
defp float_range(min, max, step) do
|
||||||
|
(Float.ceil(min / step) * step)
|
||||||
|
|> Stream.iterate(&(&1 + step))
|
||||||
|
|> Enum.take_while(&(&1 <= max))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fast_score_grid(grid, lat, lon, stations, band_mhz, max_range) do
|
defp fast_score_grid(grid, lat, lon, stations, band_mhz, max_range) do
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
selected_grid: nil,
|
selected_grid: nil,
|
||||||
resolving: false,
|
resolving: false,
|
||||||
computing: false,
|
computing: false,
|
||||||
url_loaded: false
|
url_loaded: false,
|
||||||
|
show_grids: true
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -80,6 +81,11 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
{:noreply, push_url(socket)}
|
{:noreply, push_url(socket)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("toggle_grids", _params, socket) do
|
||||||
|
show = !socket.assigns.show_grids
|
||||||
|
{:noreply, socket |> assign(show_grids: show) |> push_event("toggle_grids", %{show: show})}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("select_band", %{"band" => band_str}, socket) do
|
def handle_event("select_band", %{"band" => band_str}, socket) do
|
||||||
socket = assign(socket, band: String.to_integer(band_str), coverage: [], selected_grid: nil)
|
socket = assign(socket, band: String.to_integer(band_str), coverage: [], selected_grid: nil)
|
||||||
{:noreply, push_url(socket)}
|
{:noreply, push_url(socket)}
|
||||||
|
|
@ -254,11 +260,21 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
<div class="absolute top-2 left-2 z-[1000] w-80 max-h-[calc(100vh-1rem)] overflow-y-auto bg-base-100/95 shadow-lg rounded-box border border-base-300 p-3 flex flex-col gap-3">
|
<div class="absolute top-2 left-2 z-[1000] w-80 max-h-[calc(100vh-1rem)] overflow-y-auto bg-base-100/95 shadow-lg rounded-box border border-base-300 p-3 flex flex-col gap-3">
|
||||||
<div class="font-bold text-sm">Rover Planner</div>
|
<div class="font-bold text-sm">Rover Planner</div>
|
||||||
|
|
||||||
<select phx-change="select_band" name="band" class="select select-bordered select-sm w-full">
|
<div class="flex gap-2">
|
||||||
<%= for {label, value} <- @band_options do %>
|
<select phx-change="select_band" name="band" class="select select-bordered select-sm flex-1">
|
||||||
<option value={value} selected={value == to_string(@band)}>{label}</option>
|
<%= for {label, value} <- @band_options do %>
|
||||||
<% end %>
|
<option value={value} selected={value == to_string(@band)}>{label}</option>
|
||||||
</select>
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
phx-click="toggle_grids"
|
||||||
|
class={["btn btn-sm btn-square", if(@show_grids, do: "btn-primary", else: "btn-ghost")]}
|
||||||
|
title="Toggle grid overlay"
|
||||||
|
>
|
||||||
|
<.icon name="hero-squares-2x2" class="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form phx-submit="add_station" class="flex gap-1">
|
<form phx-submit="add_station" class="flex gap-1">
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue