Vendor Leaflet 1.9.4 (JS, CSS, marker images) and wire it into the esbuild/Tailwind asset pipeline. Create MapLive with band selector buttons, auto-refresh, and a colocated JS hook that renders propagation scores as color-coded circle markers with a legend. Stub Propagation context and BandConfig modules provide the data interface for the scoring pipeline. Add nav bar with links to Map, QSOs, and Submit pages.
87 lines
3 KiB
JavaScript
87 lines
3 KiB
JavaScript
export const PropagationMap = {
|
|
mounted() {
|
|
this.map = L.map(this.el, {
|
|
center: [38.0, -96.0],
|
|
zoom: 5,
|
|
minZoom: 4,
|
|
maxZoom: 10
|
|
})
|
|
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: "© OpenStreetMap contributors",
|
|
maxZoom: 19
|
|
}).addTo(this.map)
|
|
|
|
this.scoreLayer = L.layerGroup().addTo(this.map)
|
|
|
|
this.colorScale = [
|
|
{ min: 80, color: "#00ffa3" },
|
|
{ min: 65, color: "#7dffd4" },
|
|
{ min: 50, color: "#ffe566" },
|
|
{ min: 33, color: "#ff9044" },
|
|
{ min: 0, color: "#ff4f4f" }
|
|
]
|
|
|
|
const initialScores = JSON.parse(this.el.dataset.scores || "[]")
|
|
this.renderScores(initialScores)
|
|
|
|
this.handleEvent("update_scores", ({ scores }) => {
|
|
this.renderScores(scores)
|
|
})
|
|
|
|
const legend = L.control({ position: "bottomright" })
|
|
legend.onAdd = () => {
|
|
const div = L.DomUtil.create("div", "leaflet-legend")
|
|
div.innerHTML = `
|
|
<div style="background:white;padding:8px 12px;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.3);font-size:12px;line-height:1.8;">
|
|
<strong>Propagation</strong><br>
|
|
<span style="background:#00ffa3;width:12px;height:12px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Excellent (80-100)<br>
|
|
<span style="background:#7dffd4;width:12px;height:12px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Good (65-79)<br>
|
|
<span style="background:#ffe566;width:12px;height:12px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Marginal (50-64)<br>
|
|
<span style="background:#ff9044;width:12px;height:12px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Poor (33-49)<br>
|
|
<span style="background:#ff4f4f;width:12px;height:12px;display:inline-block;border-radius:50%;vertical-align:middle;margin-right:4px;"></span> Negligible (0-32)
|
|
</div>
|
|
`
|
|
return div
|
|
}
|
|
legend.addTo(this.map)
|
|
},
|
|
|
|
renderScores(scores) {
|
|
this.scoreLayer.clearLayers()
|
|
scores.forEach(({ lat, lon, score }) => {
|
|
const color = this.scoreColor(score)
|
|
const circle = L.circleMarker([lat, lon], {
|
|
radius: 6,
|
|
fillColor: color,
|
|
fillOpacity: 0.7,
|
|
color: color,
|
|
weight: 0
|
|
})
|
|
circle.bindPopup(
|
|
`<strong>Score: ${score}/100</strong><br>${this.scoreTier(score)}<br>` +
|
|
`<small>${lat.toFixed(3)}\u00b0N, ${Math.abs(lon).toFixed(3)}\u00b0W</small>`
|
|
)
|
|
this.scoreLayer.addLayer(circle)
|
|
})
|
|
},
|
|
|
|
scoreColor(score) {
|
|
for (const tier of this.colorScale) {
|
|
if (score >= tier.min) return tier.color
|
|
}
|
|
return "#ff4f4f"
|
|
},
|
|
|
|
scoreTier(score) {
|
|
if (score >= 80) return "EXCELLENT"
|
|
if (score >= 65) return "GOOD"
|
|
if (score >= 50) return "MARGINAL"
|
|
if (score >= 33) return "POOR"
|
|
return "NEGLIGIBLE"
|
|
},
|
|
|
|
destroyed() {
|
|
if (this.map) this.map.remove()
|
|
}
|
|
}
|