Add grid squares toggle to rover planner
Extract shared Maidenhead grid overlay into maidenhead_grid.js so both the main map and rover planner use the same implementation. Wire up the toggle_grid event and checkbox on the rover page matching the main map.
This commit is contained in:
parent
803516f0b0
commit
b4594fa71f
4 changed files with 163 additions and 123 deletions
122
assets/js/maidenhead_grid.js
Normal file
122
assets/js/maidenhead_grid.js
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Maidenhead grid square overlay shared between the main propagation map
|
||||||
|
// and the rover planner map. Draws red field/square rectangles with labels.
|
||||||
|
|
||||||
|
export class GridSquare {
|
||||||
|
constructor(lat, lon) {
|
||||||
|
this.lat = lat
|
||||||
|
this.lon = ((lon % 360) + 540) % 360 - 180
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(precision = 4) {
|
||||||
|
const adjLon = this.lon + 180
|
||||||
|
const adjLat = this.lat + 90
|
||||||
|
let g = ""
|
||||||
|
g += String.fromCharCode(65 + Math.floor(adjLon / 20))
|
||||||
|
g += String.fromCharCode(65 + Math.floor(adjLat / 10))
|
||||||
|
g += Math.floor((adjLon % 20) / 2)
|
||||||
|
g += Math.floor(adjLat % 10)
|
||||||
|
if (precision > 4) {
|
||||||
|
g += String.fromCharCode(97 + Math.floor(((adjLon % 2) * 60) / 5))
|
||||||
|
g += String.fromCharCode(97 + Math.floor(((adjLat % 1) * 60) / 2.5))
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
static decode(grid) {
|
||||||
|
const f1 = grid.charCodeAt(0) - 65
|
||||||
|
const f2 = grid.charCodeAt(1) - 65
|
||||||
|
let west = f1 * 20 - 180
|
||||||
|
let south = f2 * 10 - 90
|
||||||
|
let w = 20, h = 10
|
||||||
|
|
||||||
|
if (grid.length >= 4) {
|
||||||
|
west += parseInt(grid[2]) * 2
|
||||||
|
south += parseInt(grid[3])
|
||||||
|
w = 2; h = 1
|
||||||
|
}
|
||||||
|
if (grid.length >= 6) {
|
||||||
|
west += (grid.charCodeAt(4) - 97) * 5 / 60
|
||||||
|
south += (grid.charCodeAt(5) - 97) * 2.5 / 60
|
||||||
|
w = 5 / 60; h = 2.5 / 60
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
west, east: west + w, south, north: south + h,
|
||||||
|
center: { lat: south + h / 2, lon: west + w / 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGridSquare(grid, precision, bounds, layerGroup, drawnSet, map) {
|
||||||
|
if (grid.length < 2 || drawnSet.has(grid)) return
|
||||||
|
drawnSet.add(grid)
|
||||||
|
|
||||||
|
const c = GridSquare.decode(grid)
|
||||||
|
if (c.west > bounds.getEast() || c.east < bounds.getWest() ||
|
||||||
|
c.south > bounds.getNorth() || c.north < bounds.getSouth()) return
|
||||||
|
|
||||||
|
const field = precision <= 2
|
||||||
|
|
||||||
|
layerGroup.addLayer(L.rectangle(
|
||||||
|
[[c.south, c.west], [c.north, c.east]],
|
||||||
|
{ color: "#E63946", weight: field ? 2.5 : 2, opacity: 0.8, fillOpacity: 0, interactive: false }
|
||||||
|
))
|
||||||
|
|
||||||
|
const sw = map.latLngToContainerPoint([c.south, c.west])
|
||||||
|
const ne = map.latLngToContainerPoint([c.north, c.east])
|
||||||
|
const px = Math.abs(ne.x - sw.x)
|
||||||
|
const minW = field ? 30 : 40
|
||||||
|
if (px > minW) {
|
||||||
|
const len = field ? 2 : 4
|
||||||
|
const text = grid.substring(0, len)
|
||||||
|
const fs = Math.max(10, Math.min(14, Math.round(px / (len * 1.2))))
|
||||||
|
layerGroup.addLayer(L.marker([c.center.lat, c.center.lon], {
|
||||||
|
interactive: false,
|
||||||
|
icon: L.divIcon({
|
||||||
|
className: "grid-label",
|
||||||
|
html: `<div style="font-size:${fs}px;font-weight:bold;color:#fff;background:rgba(0,0,0,0.5);padding:1px 4px;border-radius:3px;white-space:nowrap;width:fit-content;">${text}</div>`,
|
||||||
|
iconSize: [Math.round(len * fs * 0.8), Math.round(fs * 1.5)],
|
||||||
|
iconAnchor: [Math.round(len * fs * 0.4), Math.round(fs * 0.75)]
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGridOverlay(map, gridLayer) {
|
||||||
|
gridLayer.clearLayers()
|
||||||
|
const drawn = new Set()
|
||||||
|
const bounds = map.getBounds()
|
||||||
|
const zoom = map.getZoom()
|
||||||
|
const showSquares = zoom >= 7
|
||||||
|
|
||||||
|
const south = Math.max(bounds.getSouth(), -90)
|
||||||
|
const north = Math.min(bounds.getNorth(), 90)
|
||||||
|
|
||||||
|
if (!showSquares) {
|
||||||
|
const fW = Math.floor((bounds.getWest() + 180) / 20) * 20 - 180
|
||||||
|
const fE = Math.ceil((bounds.getEast() + 180) / 20) * 20 - 180
|
||||||
|
const fS = Math.floor((south + 90) / 10) * 10 - 90
|
||||||
|
const fN = Math.ceil((north + 90) / 10) * 10 - 90
|
||||||
|
for (let lon = fW; lon < fE; lon += 20) {
|
||||||
|
for (let lat = fS; lat < fN; lat += 10) {
|
||||||
|
const nLon = ((lon % 360) + 540) % 360 - 180
|
||||||
|
const g = new GridSquare(lat + 5, nLon + 10).encode(4).substring(0, 2)
|
||||||
|
drawGridSquare(g, 2, bounds, gridLayer, drawn, map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const wE = Math.floor((bounds.getWest() + 180) / 2) * 2 - 180
|
||||||
|
const eE = Math.ceil((bounds.getEast() + 180) / 2) * 2 - 180
|
||||||
|
const sE = Math.floor(south + 90) - 90
|
||||||
|
const nE = Math.ceil(north + 90) - 90
|
||||||
|
if (Math.ceil((eE - wE) / 2) * Math.ceil(nE - sE) > 500) return
|
||||||
|
|
||||||
|
for (let lon = wE; lon < eE; lon += 2) {
|
||||||
|
for (let lat = sE; lat < nE; lat += 1) {
|
||||||
|
const nLon = ((lon % 360) + 540) % 360 - 180
|
||||||
|
const g = new GridSquare(lat + 0.5, nLon + 1).encode(4)
|
||||||
|
drawGridSquare(g.substring(0, 4), 4, bounds, gridLayer, drawn, map)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import topbar from "../vendor/topbar"
|
import topbar from "../vendor/topbar"
|
||||||
|
import { updateGridOverlay } from "./maidenhead_grid"
|
||||||
|
|
||||||
const FACTOR_META = {
|
const FACTOR_META = {
|
||||||
humidity: { label: "Humidity", weight: 0.20, unit: "g/m\u00b3" },
|
humidity: { label: "Humidity", weight: 0.20, unit: "g/m\u00b3" },
|
||||||
|
|
@ -347,128 +348,6 @@ function buildPopupHTML(detail, viewshedLoading) {
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Maidenhead Grid Square ---
|
|
||||||
|
|
||||||
class GridSquare {
|
|
||||||
constructor(lat, lon) {
|
|
||||||
this.lat = lat
|
|
||||||
this.lon = ((lon % 360) + 540) % 360 - 180
|
|
||||||
}
|
|
||||||
|
|
||||||
encode(precision = 4) {
|
|
||||||
const adjLon = this.lon + 180
|
|
||||||
const adjLat = this.lat + 90
|
|
||||||
let g = ""
|
|
||||||
g += String.fromCharCode(65 + Math.floor(adjLon / 20))
|
|
||||||
g += String.fromCharCode(65 + Math.floor(adjLat / 10))
|
|
||||||
g += Math.floor((adjLon % 20) / 2)
|
|
||||||
g += Math.floor(adjLat % 10)
|
|
||||||
if (precision > 4) {
|
|
||||||
g += String.fromCharCode(97 + Math.floor(((adjLon % 2) * 60) / 5))
|
|
||||||
g += String.fromCharCode(97 + Math.floor(((adjLat % 1) * 60) / 2.5))
|
|
||||||
}
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
static decode(grid) {
|
|
||||||
const f1 = grid.charCodeAt(0) - 65
|
|
||||||
const f2 = grid.charCodeAt(1) - 65
|
|
||||||
let west = f1 * 20 - 180
|
|
||||||
let south = f2 * 10 - 90
|
|
||||||
let w = 20, h = 10
|
|
||||||
|
|
||||||
if (grid.length >= 4) {
|
|
||||||
west += parseInt(grid[2]) * 2
|
|
||||||
south += parseInt(grid[3])
|
|
||||||
w = 2; h = 1
|
|
||||||
}
|
|
||||||
if (grid.length >= 6) {
|
|
||||||
west += (grid.charCodeAt(4) - 97) * 5 / 60
|
|
||||||
south += (grid.charCodeAt(5) - 97) * 2.5 / 60
|
|
||||||
w = 5 / 60; h = 2.5 / 60
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
west, east: west + w, south, north: south + h,
|
|
||||||
center: { lat: south + h / 2, lon: west + w / 2 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawGridSquare(grid, precision, bounds, zoom, layerGroup, drawnSet, map) {
|
|
||||||
if (grid.length < 2 || drawnSet.has(grid)) return
|
|
||||||
drawnSet.add(grid)
|
|
||||||
|
|
||||||
const c = GridSquare.decode(grid)
|
|
||||||
if (c.west > bounds.getEast() || c.east < bounds.getWest() ||
|
|
||||||
c.south > bounds.getNorth() || c.north < bounds.getSouth()) return
|
|
||||||
|
|
||||||
const field = precision <= 2
|
|
||||||
|
|
||||||
layerGroup.addLayer(L.rectangle(
|
|
||||||
[[c.south, c.west], [c.north, c.east]],
|
|
||||||
{ color: "#E63946", weight: field ? 2.5 : 2, opacity: 0.8, fillOpacity: 0, interactive: false }
|
|
||||||
))
|
|
||||||
|
|
||||||
const sw = map.latLngToContainerPoint([c.south, c.west])
|
|
||||||
const ne = map.latLngToContainerPoint([c.north, c.east])
|
|
||||||
const px = Math.abs(ne.x - sw.x)
|
|
||||||
const minW = field ? 30 : 40
|
|
||||||
if (px > minW) {
|
|
||||||
const len = field ? 2 : 4
|
|
||||||
const text = grid.substring(0, len)
|
|
||||||
const fs = Math.max(10, Math.min(14, Math.round(px / (len * 1.2))))
|
|
||||||
layerGroup.addLayer(L.marker([c.center.lat, c.center.lon], {
|
|
||||||
interactive: false,
|
|
||||||
icon: L.divIcon({
|
|
||||||
className: "grid-label",
|
|
||||||
html: `<div style="font-size:${fs}px;font-weight:bold;color:#fff;background:rgba(0,0,0,0.5);padding:1px 4px;border-radius:3px;white-space:nowrap;width:fit-content;">${text}</div>`,
|
|
||||||
iconSize: [Math.round(len * fs * 0.8), Math.round(fs * 1.5)],
|
|
||||||
iconAnchor: [Math.round(len * fs * 0.4), Math.round(fs * 0.75)]
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGridOverlay(map, gridLayer) {
|
|
||||||
gridLayer.clearLayers()
|
|
||||||
const drawn = new Set()
|
|
||||||
const bounds = map.getBounds()
|
|
||||||
const zoom = map.getZoom()
|
|
||||||
const showSquares = zoom >= 7
|
|
||||||
|
|
||||||
const south = Math.max(bounds.getSouth(), -90)
|
|
||||||
const north = Math.min(bounds.getNorth(), 90)
|
|
||||||
|
|
||||||
if (!showSquares) {
|
|
||||||
const fW = Math.floor((bounds.getWest() + 180) / 20) * 20 - 180
|
|
||||||
const fE = Math.ceil((bounds.getEast() + 180) / 20) * 20 - 180
|
|
||||||
const fS = Math.floor((south + 90) / 10) * 10 - 90
|
|
||||||
const fN = Math.ceil((north + 90) / 10) * 10 - 90
|
|
||||||
for (let lon = fW; lon < fE; lon += 20) {
|
|
||||||
for (let lat = fS; lat < fN; lat += 10) {
|
|
||||||
const nLon = ((lon % 360) + 540) % 360 - 180
|
|
||||||
const g = new GridSquare(lat + 5, nLon + 10).encode(4).substring(0, 2)
|
|
||||||
drawGridSquare(g, 2, bounds, zoom, gridLayer, drawn, map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const wE = Math.floor((bounds.getWest() + 180) / 2) * 2 - 180
|
|
||||||
const eE = Math.ceil((bounds.getEast() + 180) / 2) * 2 - 180
|
|
||||||
const sE = Math.floor(south + 90) - 90
|
|
||||||
const nE = Math.ceil(north + 90) - 90
|
|
||||||
if (Math.ceil((eE - wE) / 2) * Math.ceil(nE - sE) > 500) return
|
|
||||||
|
|
||||||
for (let lon = wE; lon < eE; lon += 2) {
|
|
||||||
for (let lat = sE; lat < nE; lat += 1) {
|
|
||||||
const nLon = ((lon % 360) + 540) % 360 - 180
|
|
||||||
const g = new GridSquare(lat + 0.5, nLon + 1).encode(4)
|
|
||||||
drawGridSquare(g.substring(0, 4), 4, bounds, zoom, gridLayer, drawn, map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Hook ---
|
// --- Hook ---
|
||||||
|
|
||||||
export const PropagationMap = {
|
export const PropagationMap = {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
|
import { updateGridOverlay } from "./maidenhead_grid"
|
||||||
|
|
||||||
export const RoverMap = {
|
export const RoverMap = {
|
||||||
mounted() {
|
mounted() {
|
||||||
this.stations = []
|
this.stations = []
|
||||||
this.stationMarkers = L.layerGroup()
|
this.stationMarkers = L.layerGroup()
|
||||||
this.scoreOverlay = null
|
this.scoreOverlay = null
|
||||||
this.gridLookup = new Map()
|
this.gridLookup = new Map()
|
||||||
|
this.gridLayer = L.layerGroup()
|
||||||
|
this.gridVisible = false
|
||||||
|
|
||||||
this.colorScale = [
|
this.colorScale = [
|
||||||
{ min: 80, r: 0, g: 255, b: 163 },
|
{ min: 80, r: 0, g: 255, b: 163 },
|
||||||
|
|
@ -44,6 +48,9 @@ export const RoverMap = {
|
||||||
|
|
||||||
map.on("moveend", () => {
|
map.on("moveend", () => {
|
||||||
pushBounds()
|
pushBounds()
|
||||||
|
if (this.gridVisible) {
|
||||||
|
updateGridOverlay(map, this.gridLayer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.handleEvent("update_scores", ({ scores }) => {
|
this.handleEvent("update_scores", ({ scores }) => {
|
||||||
|
|
@ -54,6 +61,16 @@ export const RoverMap = {
|
||||||
this.stations = stations
|
this.stations = stations
|
||||||
this.renderStations()
|
this.renderStations()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.handleEvent("toggle_grid", ({ visible }) => {
|
||||||
|
this.gridVisible = visible
|
||||||
|
if (visible) {
|
||||||
|
this.gridLayer.addTo(map)
|
||||||
|
updateGridOverlay(map, this.gridLayer)
|
||||||
|
} else {
|
||||||
|
this.gridLayer.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// --- Propagation score overlay (same as main map) ---
|
// --- Propagation score overlay (same as main map) ---
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
resolving: false,
|
resolving: false,
|
||||||
url_loaded: false,
|
url_loaded: false,
|
||||||
initial_scores_json: Jason.encode!(initial_scores),
|
initial_scores_json: Jason.encode!(initial_scores),
|
||||||
bounds: @initial_bounds
|
bounds: @initial_bounds,
|
||||||
|
grid_visible: false
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -98,6 +99,17 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
{:noreply, push_url(socket)}
|
{:noreply, push_url(socket)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("toggle_grid", _params, socket) do
|
||||||
|
visible = !socket.assigns.grid_visible
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(:grid_visible, visible)
|
||||||
|
|> push_event("toggle_grid", %{visible: visible})
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("map_bounds", bounds, socket) do
|
def handle_event("map_bounds", bounds, socket) do
|
||||||
scores = Propagation.latest_scores(socket.assigns.band, bounds)
|
scores = Propagation.latest_scores(socket.assigns.band, bounds)
|
||||||
|
|
||||||
|
|
@ -212,6 +224,16 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer px-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-sm toggle-primary"
|
||||||
|
checked={@grid_visible}
|
||||||
|
phx-click="toggle_grid"
|
||||||
|
/>
|
||||||
|
<span class="text-sm">Grid squares</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<form phx-submit="add_station" class="flex gap-1">
|
<form phx-submit="add_station" class="flex gap-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue