Initial codebase: Go + Wails amateur radio logbook
Backend (Go 1.25 / Wails v2): - QSO storage on SQLite (modernc) with embedded migrations (0001..0005) - Streaming ADIF import (batch insert) + WorkedBefore per callsign and DXCC - Callsign lookup with QRZ.com + HamQTH providers (primary/failsafe routing) and SQLite-backed TTL cache - DXCC resolver from cty.dat (auto-download, longest-prefix-match) - Multi-profile operator identities (home/portable/SOTA/contest) — every QSO stamps MY_* from the active profile - CAT control via OmniRig COM on a single OS-locked goroutine, with bidirectional sync (freq/mode/band/split/VFOs) and Rig1/Rig2 hot-swap - Settings store (key/value), CAT debug log at %APPDATA%/HamLog/cat.log Frontend (React 18 + TypeScript + Tailwind v4 + shadcn-style): - Single-row entry strip with CAT-aware band/mode/freq, RST, Start/End UTC, per-field locks (band/mode/freq/start/end) for backdated QSOs - Topbar: live freq (MHz.kHz.Hz dotted), live UTC, band/mode/SPLIT badges, CAT pill with rig selector and clickable Azimuth pill (rotor TODO) - Settings tree: Profiles (Log4OM-style manager), Station Information (edits the active profile), unified Callsign Lookup with Test buttons, Bands/Modes lists, CAT - Worked-before matrix (band × mode × class) with new-DXCC highlighting - ADIF import from menu + Maintenance > Refresh cty.dat Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// Maidenhead grid locator ⇄ lat/lon, plus great-circle distance + bearing.
|
||||
//
|
||||
// Used to drive the "AZ SP/LP / Dist SP/LP" readouts in the entry form so
|
||||
// the operator knows where to point an antenna without having to fire up an
|
||||
// external tool.
|
||||
|
||||
const EARTH_KM = 6371.0088;
|
||||
const EARTH_CIRCUMFERENCE_KM = 2 * Math.PI * EARTH_KM; // ≈ 40 030
|
||||
|
||||
// gridToLatLon parses a Maidenhead locator (4, 6, or 8 chars) and returns
|
||||
// the center of the indicated square. Returns null on bad input.
|
||||
export function gridToLatLon(grid: string): { lat: number; lon: number } | null {
|
||||
if (!grid) return null;
|
||||
const g = grid.trim().toUpperCase();
|
||||
if (g.length < 4 || g.length % 2 !== 0) return null;
|
||||
|
||||
const A = 'A'.charCodeAt(0);
|
||||
const Z = 'Z'.charCodeAt(0);
|
||||
const isLetter = (c: number) => c >= A && c <= Z;
|
||||
const isDigit = (c: string) => c >= '0' && c <= '9';
|
||||
|
||||
if (!isLetter(g.charCodeAt(0)) || !isLetter(g.charCodeAt(1))) return null;
|
||||
if (!isDigit(g[2]) || !isDigit(g[3])) return null;
|
||||
|
||||
let lon = (g.charCodeAt(0) - A) * 20 - 180;
|
||||
let lat = (g.charCodeAt(1) - A) * 10 - 90;
|
||||
lon += parseInt(g[2], 10) * 2;
|
||||
lat += parseInt(g[3], 10);
|
||||
|
||||
if (g.length >= 6) {
|
||||
if (!isLetter(g.charCodeAt(4)) || !isLetter(g.charCodeAt(5))) return null;
|
||||
lon += (g.charCodeAt(4) - A) * (2 / 24);
|
||||
lat += (g.charCodeAt(5) - A) * (1 / 24);
|
||||
// center of the sub-square
|
||||
lon += 1 / 24;
|
||||
lat += 0.5 / 24;
|
||||
} else {
|
||||
// center of the 2°×1° square
|
||||
lon += 1;
|
||||
lat += 0.5;
|
||||
}
|
||||
|
||||
if (g.length >= 8) {
|
||||
if (!isDigit(g[6]) || !isDigit(g[7])) return null;
|
||||
// Extended grid (rare) — refine; using simple 10x subdivision.
|
||||
lon += parseInt(g[6], 10) * (2 / 24 / 10) - 1 / 24;
|
||||
lat += parseInt(g[7], 10) * (1 / 24 / 10) - 0.5 / 24;
|
||||
}
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
// PathInfo describes both short and long great-circle path between two
|
||||
// points. Bearing in degrees from true north (0–360). Distance in km.
|
||||
export interface PathInfo {
|
||||
bearingShort: number;
|
||||
bearingLong: number;
|
||||
distanceShort: number;
|
||||
distanceLong: number;
|
||||
}
|
||||
|
||||
// pathBetween computes great-circle bearing+distance between two
|
||||
// Maidenhead grids. Returns null if either is unparseable.
|
||||
export function pathBetween(fromGrid: string, toGrid: string): PathInfo | null {
|
||||
const a = gridToLatLon(fromGrid);
|
||||
const b = gridToLatLon(toGrid);
|
||||
if (!a || !b) return null;
|
||||
const φ1 = toRad(a.lat);
|
||||
const φ2 = toRad(b.lat);
|
||||
const Δλ = toRad(b.lon - a.lon);
|
||||
|
||||
// Spherical law of cosines is simpler than haversine and accurate enough
|
||||
// for ham bearings (>1 km errors don't matter at the antenna-rotor level).
|
||||
let cos = Math.sin(φ1) * Math.sin(φ2) + Math.cos(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
||||
cos = Math.max(-1, Math.min(1, cos));
|
||||
const distShort = EARTH_KM * Math.acos(cos);
|
||||
|
||||
// Forward azimuth.
|
||||
const y = Math.sin(Δλ) * Math.cos(φ2);
|
||||
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
||||
let bearing = toDeg(Math.atan2(y, x));
|
||||
bearing = (bearing + 360) % 360;
|
||||
|
||||
return {
|
||||
bearingShort: bearing,
|
||||
bearingLong: (bearing + 180) % 360,
|
||||
distanceShort: distShort,
|
||||
distanceLong: EARTH_CIRCUMFERENCE_KM - distShort,
|
||||
};
|
||||
}
|
||||
|
||||
function toRad(d: number): number { return (d * Math.PI) / 180; }
|
||||
function toDeg(r: number): number { return (r * 180) / Math.PI; }
|
||||
Reference in New Issue
Block a user