- Leaflet map on contact detail pages showing both station locations - SRTM elevation profile with Chart.js: terrain, LOS, Fresnel zone - Path info: distance, azimuth, elevation angle in both directions - Compact 4-column grid layout for contact details - Contact map page with all contacts plotted - Rename QSOs to Contacts in page header - Wrap algo page in standard layout
139 lines
3.9 KiB
JavaScript
139 lines
3.9 KiB
JavaScript
import Chart from "../vendor/chart.js"
|
|
|
|
const M2FT = 3.28084
|
|
const KM2MI = 0.621371
|
|
|
|
export const ElevationProfile = {
|
|
mounted() {
|
|
const data = JSON.parse(this.el.dataset.profile)
|
|
if (!data || !data.points || data.points.length === 0) return
|
|
this.renderChart(data)
|
|
},
|
|
|
|
renderChart(data) {
|
|
const points = data.points
|
|
const distances = points.map(p => p.dist_km * KM2MI)
|
|
const elevations = points.map(p => p.elev * M2FT)
|
|
const losLine = points.map(p => p.beam * M2FT)
|
|
const fresnelLower = points.map(p => (p.beam - p.r1) * M2FT)
|
|
const showFresnel = data.freq_mhz >= 1000
|
|
|
|
const allElevs = [...elevations, ...losLine]
|
|
const minElev = Math.min(...allElevs) - 60
|
|
const maxElev = Math.max(...allElevs) + 120
|
|
|
|
const canvas = this.el.querySelector("canvas")
|
|
const ctx = canvas.getContext("2d")
|
|
|
|
this.chart = new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: distances,
|
|
datasets: [
|
|
{
|
|
label: "Terrain",
|
|
data: elevations,
|
|
borderColor: "#8B6914",
|
|
backgroundColor: "rgba(139, 105, 20, 0.3)",
|
|
fill: true,
|
|
pointRadius: 0,
|
|
borderWidth: 1.5,
|
|
tension: 0.1,
|
|
order: 3
|
|
},
|
|
{
|
|
label: "Line of Sight",
|
|
data: losLine,
|
|
borderColor: "#2563eb",
|
|
borderWidth: 2,
|
|
borderDash: [6, 3],
|
|
pointRadius: 0,
|
|
fill: false,
|
|
order: 1
|
|
},
|
|
...(showFresnel ? [
|
|
{
|
|
label: "Fresnel Zone",
|
|
data: fresnelLower,
|
|
borderColor: "rgba(239, 68, 68, 0.4)",
|
|
backgroundColor: "rgba(239, 68, 68, 0.08)",
|
|
borderWidth: 1,
|
|
borderDash: [3, 3],
|
|
pointRadius: 0,
|
|
fill: "+1",
|
|
order: 2
|
|
},
|
|
{
|
|
label: "_fresnelBase",
|
|
data: losLine,
|
|
borderWidth: 0,
|
|
pointRadius: 0,
|
|
fill: false,
|
|
order: 2
|
|
}
|
|
] : [])
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: {duration: 300},
|
|
plugins: {
|
|
legend: {
|
|
display: true,
|
|
position: "top",
|
|
labels: {
|
|
boxWidth: 12,
|
|
font: {size: 10},
|
|
filter: item => !item.text.startsWith("_")
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
title: items => items[0].label + " mi",
|
|
label: item => {
|
|
if (item.dataset.label.startsWith("_")) return null
|
|
return item.dataset.label + ": " + Math.round(item.raw) + " ft"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
title: {display: true, text: "Distance (mi)", font: {size: 10}},
|
|
afterBuildTicks: axis => {
|
|
const last = distances.length - 1
|
|
const totalMi = distances[last]
|
|
const step = Math.ceil(totalMi / 7) || 1
|
|
const ticks = []
|
|
for (let mi = 0; mi < totalMi; mi += step) {
|
|
const idx = distances.findIndex(d => d >= mi)
|
|
if (idx >= 0) ticks.push({value: idx})
|
|
}
|
|
ticks.push({value: last})
|
|
axis.ticks = ticks
|
|
},
|
|
ticks: {
|
|
font: {size: 9},
|
|
autoSkip: false,
|
|
callback: val => distances[val] !== undefined ? Math.round(distances[val]) : ""
|
|
}
|
|
},
|
|
y: {
|
|
title: {display: true, text: "Elevation (ft)", font: {size: 10}},
|
|
ticks: {font: {size: 9}},
|
|
min: Math.max(0, minElev),
|
|
max: maxElev
|
|
}
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
destroyed() {
|
|
if (this.chart) {
|
|
this.chart.destroy()
|
|
this.chart = null
|
|
}
|
|
}
|
|
}
|