diff --git a/src/app/new-contact/page.tsx b/src/app/new-contact/page.tsx
index b03c799..2970bdc 100644
--- a/src/app/new-contact/page.tsx
+++ b/src/app/new-contact/page.tsx
@@ -38,7 +38,15 @@ import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { frequencyToBand, AMATEUR_BANDS } from '@/lib/bands';
import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes';
-import { gridToLatLon, distanceKm, bearingDeg, compassPoint, kmToMiles } from '@/lib/grid';
+import {
+ gridToLatLon,
+ distanceKm,
+ bearingDeg,
+ compassPoint,
+ kmToMiles,
+ longPathBearingDeg,
+ longPathKm,
+} from '@/lib/grid';
void _PageHeader;
@@ -972,6 +980,9 @@ export default function NewContactPage() {
{Math.round(kmToMiles(pathInfo.km)).toLocaleString()} mi
+
+ LP {Math.round(longPathKm(pathInfo.km)).toLocaleString()} km
+
@@ -983,6 +994,10 @@ export default function NewContactPage() {
{compassPoint(pathInfo.bearing)}
+
+ LP {Math.round(longPathBearingDeg(pathInfo.bearing))}°{' '}
+ {compassPoint(longPathBearingDeg(pathInfo.bearing))}
+
) : null}
diff --git a/src/lib/grid.ts b/src/lib/grid.ts
index 4b2aee3..83ed5e1 100644
--- a/src/lib/grid.ts
+++ b/src/lib/grid.ts
@@ -65,6 +65,13 @@ export function gridToLatLon(grid: string): LatLon | null {
return { lat, lon };
}
+const EARTH_RADIUS_KM = 6371;
+
+// Length of a full great circle around the Earth (2πR). The short path between
+// two points and its long-path complement always sum to this, so long-path
+// distance is just `EARTH_CIRCUMFERENCE_KM - shortPathKm`.
+export const EARTH_CIRCUMFERENCE_KM = 2 * Math.PI * EARTH_RADIUS_KM;
+
const toRad = (deg: number): number => (deg * Math.PI) / 180;
const toDeg = (rad: number): number => (rad * 180) / Math.PI;
@@ -75,7 +82,7 @@ export function distanceKm(
lat2: number,
lon2: number
): number {
- const R = 6371;
+ const R = EARTH_RADIUS_KM;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
@@ -113,10 +120,29 @@ export function compassPoint(bearing: number): string {
return COMPASS_POINTS[idx];
}
+// Long-path bearing: the heading around the *other* side of the globe, exactly
+// 180° opposed to the short-path bearing. HF DX frequently arrives long-path, so
+// operators swing the beam this way when the short path is dead. Normalized into
+// [0, 360) so it feeds compassPoint like any other bearing.
+export function longPathBearingDeg(shortBearing: number): number {
+ return ((shortBearing + 180) % 360 + 360) % 360;
+}
+
+// Long-path distance: the rest of the great circle once the short path is
+// removed. short + long always sums to the Earth's circumference.
+export function longPathKm(shortKm: number): number {
+ return EARTH_CIRCUMFERENCE_KM - shortKm;
+}
+
export interface PathInfo {
distanceKm: number;
bearingDeg: number;
compass: string;
+ // Long-path complement — the beam heading and arc the other way around the
+ // globe, for the HF openings that favor the long path.
+ longPathKm: number;
+ longPathBearingDeg: number;
+ longPathCompass: string;
}
// Distance/bearing between two Maidenhead locators. Returns null unless both
@@ -127,10 +153,15 @@ export function gridPath(from: string, to: string): PathInfo | null {
const b = gridToLatLon(to);
if (!a || !b) return null;
const bearing = bearingDeg(a.lat, a.lon, b.lat, b.lon);
+ const dist = distanceKm(a.lat, a.lon, b.lat, b.lon);
+ const lpBearing = longPathBearingDeg(bearing);
return {
- distanceKm: distanceKm(a.lat, a.lon, b.lat, b.lon),
+ distanceKm: dist,
bearingDeg: bearing,
compass: compassPoint(bearing),
+ longPathKm: longPathKm(dist),
+ longPathBearingDeg: lpBearing,
+ longPathCompass: compassPoint(lpBearing),
};
}
diff --git a/tests/grid.spec.ts b/tests/grid.spec.ts
index 85c69c7..d1b7474 100644
--- a/tests/grid.spec.ts
+++ b/tests/grid.spec.ts
@@ -7,6 +7,9 @@ import {
compassPoint,
gridPath,
kmToMiles,
+ longPathBearingDeg,
+ longPathKm,
+ EARTH_CIRCUMFERENCE_KM,
} from '@/lib/grid';
// Pure-function tests for the Maidenhead grid / great-circle math that powers
@@ -131,6 +134,36 @@ test.describe('compassPoint', () => {
});
});
+test.describe('longPathBearingDeg', () => {
+ test('is 180° opposed to the short-path bearing', () => {
+ expect(longPathBearingDeg(0)).toBeCloseTo(180, 6);
+ expect(longPathBearingDeg(90)).toBeCloseTo(270, 6);
+ expect(longPathBearingDeg(200)).toBeCloseTo(20, 6); // wraps past 360
+ expect(longPathBearingDeg(359)).toBeCloseTo(179, 6);
+ });
+
+ test('always returns a value in [0, 360)', () => {
+ for (const b of [0, 45, 179, 180, 270, 359.9]) {
+ const lp = longPathBearingDeg(b);
+ expect(lp).toBeGreaterThanOrEqual(0);
+ expect(lp).toBeLessThan(360);
+ }
+ });
+});
+
+test.describe('longPathKm', () => {
+ test('is the remainder of the great circle after the short path', () => {
+ expect(longPathKm(0)).toBeCloseTo(EARTH_CIRCUMFERENCE_KM, 6);
+ // A short + long path always sums to the Earth's circumference.
+ const shortKm = 5500;
+ expect(shortKm + longPathKm(shortKm)).toBeCloseTo(EARTH_CIRCUMFERENCE_KM, 6);
+ });
+
+ test('EARTH_CIRCUMFERENCE_KM is ~40030 km (2πR, R = 6371 km)', () => {
+ expect(EARTH_CIRCUMFERENCE_KM).toBeCloseTo(40030.17, 1);
+ });
+});
+
test.describe('gridPath', () => {
test('reports the transatlantic hop from Connecticut to London', () => {
// FN31 → IO91 is a classic east-coast-US to England path, ~5500 km NE.
@@ -141,6 +174,24 @@ test.describe('gridPath', () => {
expect(path!.compass).toBe('NE');
});
+ test('reports the long path as the opposite beam heading and the far arc', () => {
+ // The short path from Connecticut to London is NE; the long path leaves the
+ // antenna pointed SW, around the other side of the globe, and covers the
+ // rest of the great circle (~34500 km).
+ const path = gridPath('FN31', 'IO91');
+ expect(path).not.toBeNull();
+ expect(path!.longPathBearingDeg).toBeCloseTo(
+ (path!.bearingDeg + 180) % 360,
+ 6,
+ );
+ expect(path!.longPathCompass).toBe('SW');
+ expect(path!.distanceKm + path!.longPathKm).toBeCloseTo(
+ EARTH_CIRCUMFERENCE_KM,
+ 6,
+ );
+ expect(path!.longPathKm).toBeGreaterThan(path!.distanceKm);
+ });
+
test('returns null when either locator is invalid', () => {
expect(gridPath('FN31', 'nope')).toBeNull();
expect(gridPath('', 'IO91')).toBeNull();