feat: physical heading for ultrabeam antennas

This commit is contained in:
2026-06-16 21:25:04 +02:00
parent 01235624ee
commit 16dc864dbd
3 changed files with 64 additions and 6 deletions
+31 -3
View File
@@ -18,7 +18,9 @@ const GRATICULE = geoGraticule10();
interface Props {
bearing?: number | null; // short-path azimuth to DX (deg)
headings: number[]; // antenna heading(s) — rotor + Ultrabeam pattern
headings: number[]; // radiating heading(s) — rotor + Ultrabeam pattern
boomHeading?: number | null; // mechanical boom (rotor) azimuth, shown grey when it differs
pattern?: 'normal' | 'reverse' | 'bi' | null; // Ultrabeam pattern (for the badge)
centerLat?: number | null; // operator latitude (projection centre)
centerLon?: number | null; // operator longitude
rotorEnabled?: boolean;
@@ -36,7 +38,7 @@ function pt(az: number, radius: number): [number, number] {
return [C + radius * Math.cos(a), C + radius * Math.sin(a)];
}
export function RotorCompass({ bearing, headings, centerLat, centerLon, rotorEnabled, onGoto, onClose }: Props) {
export function RotorCompass({ bearing, headings, boomHeading, pattern, centerLat, centerLon, rotorEnabled, onGoto, onClose }: Props) {
const cardinals = useMemo(
() => [ { d: 0, l: 'N' }, { d: 45, l: 'NE' }, { d: 90, l: 'E' }, { d: 135, l: 'SE' },
{ d: 180, l: 'S' }, { d: 225, l: 'SW' }, { d: 270, l: 'W' }, { d: 315, l: 'NW' } ],
@@ -76,6 +78,18 @@ export function RotorCompass({ bearing, headings, centerLat, centerLon, rotorEna
<span className={cn('size-2 rounded-full', rotorEnabled ? 'bg-emerald-500' : 'bg-muted-foreground/40')}
title={rotorEnabled ? 'Rotator connected' : 'Rotator disabled'} />
<div className="flex-1" />
{pattern && (
<span
className={cn('px-1 py-px rounded text-[9px] font-bold tracking-wide',
pattern === 'reverse' ? 'bg-amber-200 text-amber-900'
: pattern === 'bi' ? 'bg-sky-200 text-sky-900'
: 'bg-emerald-200 text-emerald-900')}
title={pattern === 'reverse' ? 'Ultrabeam reversed — radiates opposite the boom'
: pattern === 'bi' ? 'Ultrabeam bidirectional — radiates both ways'
: 'Ultrabeam normal'}>
{pattern === 'reverse' ? 'REV' : pattern === 'bi' ? 'BI' : 'NORM'}
</span>
)}
<span className="font-mono text-sm font-bold text-emerald-700 tabular-nums">
{headLabel != null ? `${Math.round(headLabel).toString().padStart(3, '0')}°` : '—'}
</span>
@@ -123,7 +137,21 @@ export function RotorCompass({ bearing, headings, centerLat, centerLon, rotorEna
<circle cx={x} cy={y} r={3} fill="#dc2626" stroke="#fff" strokeWidth={1} />
); })()}
{/* antenna heading needle(s) — green; two when bidirectional */}
{/* mechanical boom (rotor) heading — grey dashed needle, shown when the
Ultrabeam radiates somewhere other than the boom (reverse/bi) so the
operator sees where the antenna physically points vs where it boom-sits */}
{boomHeading != null && pattern && pattern !== 'normal' && (() => {
const [x, y] = pt(boomHeading, MAP_R - 2);
return (
<g>
<title>Boom (rotor) {Math.round(boomHeading)}°</title>
<line x1={C} y1={C} x2={x} y2={y} stroke="#64748b" strokeWidth={2} strokeDasharray="3 3" strokeLinecap="round" />
<circle cx={x} cy={y} r={3} fill="#64748b" stroke="#fff" strokeWidth={1} />
</g>
);
})()}
{/* radiating heading needle(s) — green; two when bidirectional */}
{headings.map((h, i) => { const [x, y] = pt(h, MAP_R - 2); return (
<g key={i}>
<line x1={C} y1={C} x2={x} y2={y} stroke="#15803d" strokeWidth={3} strokeLinecap="round" opacity={i === 0 ? 1 : 0.55} />