prop/assets/js/beacon_map_hook.ts
Graham McIntire 1f368f9b61 Convert all JavaScript to TypeScript with type annotations
- Rename all .js files to .ts in assets/js/
- Add interfaces for hook state, data structures, and event payloads
- Add type annotations to function parameters and return types
- Create type declarations for vendor deps (Leaflet, Chart.js, Phoenix, topbar)
- Update tsconfig.json for strict TypeScript checking
- Update esbuild entry point to app.ts
2026-04-11 16:59:28 -05:00

104 lines
2.8 KiB
TypeScript

interface CoverageCell {
lat: number
lon: number
color: string
label: string
rx_dbm: number
distance_km: number
score: number
}
interface ToggleCoveragePayload {
show: boolean
}
interface BeaconMapHook extends ViewHook {
mounted(this: BeaconMapHook): void
}
export const BeaconMap = {
mounted(this: BeaconMapHook) {
const dataset = this.el.dataset
const lat = parseFloat(dataset.lat!)
const lon = parseFloat(dataset.lon!)
const label = dataset.label || ""
const onTheAir = dataset.onTheAir === "true"
const cells: CoverageCell[] = JSON.parse(dataset.cells || "[]")
const step = parseFloat(dataset.gridStep || "0.125")
const initialShowCoverage = dataset.showCoverage === "true"
const map = L.map(this.el, {
zoomControl: true,
attributionControl: false,
scrollWheelZoom: false
}).setView([lat, lon], 11)
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19
}).addTo(map)
// Build the coverage cell layer up-front but keep it off the map until
// the user toggles it on. Using a single layerGroup keeps pan/zoom fast.
const cellLayer = L.layerGroup()
const half = step / 2.0
const allBounds: [number, number][] = []
for (const c of cells) {
const sw: [number, number] = [c.lat - half, c.lon - half]
const ne: [number, number] = [c.lat + half, c.lon + half]
const rect = L.rectangle([sw, ne], {
color: c.color,
weight: 0,
fillColor: c.color,
fillOpacity: 0.55,
interactive: true
})
rect.bindTooltip(
`${c.label}<br>${c.rx_dbm} dBm<br>${c.distance_km} km · score ${c.score}`,
{sticky: true}
)
rect.addTo(cellLayer)
allBounds.push(sw, ne)
}
// Beacon marker on top.
const markerColor = onTheAir ? "#16a34a" : "#94a3b8"
const markerFill = onTheAir ? "#22c55e" : "#cbd5e1"
L.circleMarker([lat, lon], {
radius: 7,
color: markerColor,
fillColor: markerFill,
fillOpacity: 1.0,
weight: 2
}).addTo(map).bindTooltip(label, {
permanent: true,
direction: "top",
offset: [0, -10]
})
const showLayer = (): void => {
if (!map.hasLayer(cellLayer)) cellLayer.addTo(map)
if (allBounds.length > 0) {
map.fitBounds(L.latLngBounds(allBounds), {padding: [20, 20]})
map.setZoom(map.getZoom() + 2)
}
}
const hideLayer = (): void => {
if (map.hasLayer(cellLayer)) map.removeLayer(cellLayer)
map.setView([lat, lon], 11)
}
if (initialShowCoverage) showLayer()
this.handleEvent("toggle_coverage", ({show}: ToggleCoveragePayload) => {
if (show) {
showLayer()
} else {
hideLayer()
}
})
setTimeout(() => map.invalidateSize(), 50)
}
}