Files
OpsLog/frontend/src/lib/spot.ts
T

83 lines
3.8 KiB
TypeScript

// Shared helpers used by the cluster table and the band map — keeps the
// mode-inference logic and the status-cache key in one place so both
// surfaces always read the same data.
export function cleanSpotter(s: string): string {
if (!s) return '';
const i = s.indexOf('-');
return i > 0 ? s.slice(0, i) : s;
}
// inferSpotMode picks an ADIF mode for a cluster spot. Comment text is
// the strongest hint (skimmers say "FT8", "CW 24 WPM", "RTTY"); when it
// fails we fall back to the IARU R1 band-plan segment for the frequency.
// Returns '' when nothing matches — caller should leave the rig mode
// alone instead of guessing wrong.
export function inferSpotMode(comment: string, freqHz: number): string {
const c = (comment || '').toUpperCase();
if (/\bFT8\b/.test(c)) return 'FT8';
if (/\bFT4\b/.test(c)) return 'FT4';
if (/\bJS8\b/.test(c)) return 'JS8';
if (/\bQ65\b/.test(c)) return 'Q65';
if (/\bMSK144\b/.test(c)) return 'MSK144';
if (/\bJT65\b/.test(c)) return 'JT65';
if (/\bJT9\b/.test(c)) return 'JT9';
if (/\bRTTY\b/.test(c)) return 'RTTY';
if (/\bPSK(63|125|250|500)\b/.test(c)) return RegExp.$1 ? `PSK${RegExp.$1}` : 'PSK31';
if (/\bPSK31?\b/.test(c)) return 'PSK31';
if (/\bOLIVIA\b/.test(c)) return 'OLIVIA';
if (/\bMFSK\b/.test(c)) return 'MFSK16';
if (/\bCW\b/.test(c) || /\bWPM\b/.test(c)) return 'CW';
if (/\bFM\b/.test(c)) return 'FM';
if (/\bAM\b/.test(c)) return 'AM';
if (/\b(SSB|USB|LSB)\b/.test(c)) return 'SSB';
const mhz = freqHz / 1_000_000;
type Seg = [number, number, string];
const segs: Seg[] = [
[1.8, 1.838, 'CW'], [1.838, 1.84, 'FT8'], [1.84, 2.0, 'SSB'],
[3.5, 3.58, 'CW'], [3.573, 3.576, 'FT8'], [3.58, 3.6, 'DATA'], [3.6, 4.0, 'SSB'],
[5.3, 5.5, 'SSB'],
[7.0, 7.04, 'CW'], [7.074, 7.077, 'FT8'], [7.0475, 7.0485, 'FT4'],
[7.04, 7.1, 'DATA'], [7.1, 7.3, 'SSB'],
[10.1, 10.13, 'CW'], [10.13, 10.15, 'DATA'],
[14.0, 14.07, 'CW'], [14.074, 14.077, 'FT8'], [14.08, 14.0815, 'FT4'],
[14.07, 14.1, 'DATA'], [14.1, 14.35, 'SSB'],
[18.068, 18.095, 'CW'], [18.1, 18.103, 'FT8'], [18.095, 18.11, 'DATA'], [18.11, 18.168, 'SSB'],
[21.0, 21.07, 'CW'], [21.074, 21.077, 'FT8'], [21.14, 21.143, 'FT4'],
[21.07, 21.15, 'DATA'], [21.15, 21.45, 'SSB'],
[24.89, 24.915, 'CW'], [24.915, 24.917, 'FT8'], [24.915, 24.94, 'DATA'], [24.94, 24.99, 'SSB'],
[28.0, 28.07, 'CW'], [28.074, 28.077, 'FT8'], [28.18, 28.183, 'FT4'],
[28.07, 28.3, 'DATA'], [28.3, 29.7, 'SSB'],
[50.0, 50.1, 'CW'], [50.313, 50.316, 'FT8'], [50.318, 50.321, 'FT4'],
[50.1, 50.5, 'SSB'],
[144.0, 144.15, 'CW'], [144.174, 144.177, 'FT8'], [144.15, 144.5, 'SSB'],
];
for (const [lo, hi, m] of segs) {
if (mhz >= lo && mhz < hi) return m;
}
return '';
}
// spotModeCategory buckets the fine-grained mode from inferSpotMode into
// the three families the cluster filter exposes: 'SSB' (phone: SSB/FM/AM),
// 'CW', and 'DATA' (every digital mode). Returns '' when the mode is
// unknown so callers can decide how to treat un-categorisable spots.
export function spotModeCategory(mode: string): 'SSB' | 'CW' | 'DATA' | '' {
const m = (mode || '').toUpperCase();
if (m === '') return '';
if (m === 'CW') return 'CW';
if (m === 'SSB' || m === 'USB' || m === 'LSB' || m === 'FM' || m === 'AM') return 'SSB';
// Everything else inferSpotMode can return (FT8/FT4/JS8/RTTY/PSK*/…/DATA)
// is a digital mode.
return 'DATA';
}
// spotStatusKey is the cache key for ClusterSpotStatuses results. Must be
// computed identically in the fetcher and every reader — including the
// band map and the spot table — so a CW spot's status doesn't get looked
// up under an empty-mode key (which always misses → false "new-slot").
export function spotStatusKey(call: string, band: string, comment: string, freqHz: number): string {
return `${call}|${band}|${inferSpotMode(comment, freqHz)}`;
}