/** * Radio Frequency and Mode Utilities * Global helper functions for frequency conversion, band/mode determination, and radio control */ // ======================================== // CONSTANTS // ======================================== /** * LSB/USB transition threshold * Below 10 MHz = LSB, above = USB for phone modes * @constant {number} */ const LSB_USB_THRESHOLD_KHZ = 10000; // 10 MHz in kHz /** * Static FT8 calling frequencies (in kHz) * Exported globally for use across modules * @constant {Array} */ const FT8_FREQUENCIES = [1840, 3573, 7074, 10136, 14074, 18100, 21074, 24915, 28074, 50313, 144174, 432065]; window.FT8_FREQUENCIES = FT8_FREQUENCIES; // Export globally /** * Mode classification lists * Comprehensive list of radio modes organized by category * Only includes commonly used modes seen on DX clusters and amateur radio * @constant {Object} */ const MODE_LISTS = { PHONE: ['SSB', 'LSB', 'USB', 'AM', 'FM', 'SAM', 'DSB', 'J3E', 'A3E', 'PHONE'], WSJT: ['FT8', 'FT4', 'JT65', 'JT65B', 'JT6C', 'JT6M', 'JT9', 'JT9-1', 'Q65', 'QRA64', 'FST4', 'FST4W', 'WSPR', 'MSK144', 'ISCAT', 'ISCAT-A', 'ISCAT-B', 'JS8', 'JTMS', 'FSK441', 'JT4', 'OPERA'], DIGITAL_OTHER: ['RTTY', 'NAVTEX', 'SITORB', 'DIGI', 'DYNAMIC', 'RTTYFSK', 'RTTYM'], PSK: ['PSK', 'QPSK', '8PSK', 'PSK31', 'PSK63', 'PSK125', 'PSK250'], DIGITAL_MODES: ['OLIVIA', 'CONTESTIA', 'THOR', 'THROB', 'MFSK', 'MFSK8', 'MFSK16', 'HELL', 'MT63', 'DOMINO', 'PACKET', 'PACTOR', 'CLOVER', 'AMTOR', 'SITOR', 'SSTV', 'FAX', 'CHIP', 'CHIP64', 'ROS'], DIGITAL_VOICE: ['DIGITALVOICE', 'DSTAR', 'C4FM', 'DMR', 'FREEDV', 'M17'], DIGITAL_HF: ['VARA', 'ARDOP'], CW: ['CW', 'A1A'] }; /** * Available continents for cycling * Standard continent codes used in amateur radio * Exported globally for use across modules * @constant {Array} */ const CONTINENTS = ['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA']; window.CONTINENTS = CONTINENTS; // Export globally /** * Signal bandwidth constants (in kHz) * Covers all modes from MODE_LISTS for comprehensive bandwidth determination * @constant {Object} */ const SIGNAL_BANDWIDTHS = { // Phone modes (voice) SSB: 2.7, LSB: 2.7, USB: 2.7, AM: 6.0, FM: 12.0, SAM: 6.0, DSB: 6.0, J3E: 2.7, A3E: 6.0, PHONE: 2.7, // CW modes CW: 0.25, A1A: 0.25, // WSJT-X family (weak signal digital) FT8: 3.0, FT4: 3.0, JT65: 2.7, JT65B: 2.7, JT6C: 2.7, JT6M: 2.7, JT9: 0.5, 'JT9-1': 0.5, Q65: 2.7, QRA64: 2.7, FST4: 3.0, FST4W: 3.0, WSPR: 0.006, MSK144: 2.4, ISCAT: 2.0, 'ISCAT-A': 2.0, 'ISCAT-B': 2.0, JS8: 0.05, JTMS: 2.0, FSK441: 4.4, JT4: 2.7, OPERA: 0.5, // PSK variants PSK: 0.5, QPSK: 0.5, '8PSK': 0.5, PSK31: 0.062, PSK63: 0.125, PSK125: 0.25, PSK250: 0.5, // RTTY and related RTTY: 0.5, NAVTEX: 0.3, SITORB: 0.5, DIGI: 2.5, DYNAMIC: 2.5, RTTYFSK: 0.5, RTTYM: 0.5, // Other digital modes OLIVIA: 2.5, CONTESTIA: 2.5, THOR: 2.3, THROB: 2.2, MFSK: 2.5, MFSK8: 0.316, MFSK16: 0.316, HELL: 2.5, MT63: 2.5, DOMINO: 0.172, PACKET: 2.5, PACTOR: 2.4, CLOVER: 2.5, AMTOR: 0.5, SITOR: 0.5, SSTV: 2.7, FAX: 2.3, CHIP: 2.5, CHIP64: 2.5, ROS: 2.5, // Digital voice DIGITALVOICE: 6.25, DSTAR: 6.25, C4FM: 6.25, DMR: 6.25, FREEDV: 1.25, M17: 9.0, // Digital HF modes VARA: 2.5, ARDOP: 2.5 }; /** * Radio band groupings by frequency range * MF = Medium Frequency (300 kHz - 3 MHz) - 160m * HF = High Frequency (3-30 MHz) - 80m through 10m * VHF = Very High Frequency (30-300 MHz) - 6m through 1.25m * UHF = Ultra High Frequency (300 MHz-3 GHz) - 70cm through 23cm * SHF = Super High Frequency (3-30 GHz) - 13cm and above * @constant {Object} */ const BAND_GROUPS = { 'MF': ['160m'], 'HF': ['80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m'], 'VHF': ['4m', '2m', '1.25m'], // Note: 6m has its own separate button in DX Cluster 'UHF': ['70cm', '33cm', '23cm'], 'SHF': ['13cm', '9cm', '6cm', '3cm', '1.25cm', '6mm', '4mm', '2.5mm', '2mm', '1mm'] }; // ======================================== // FREQUENCY CONVERSION & BAND UTILITIES // ======================================== /** * Check if a mode is in any of the MODE_LISTS categories * @param {string} mode - Mode to check (case-insensitive) * @param {string} category - Category key from MODE_LISTS ('CW', 'PHONE', 'WSJT', etc.) * @returns {boolean} True if mode is in the category */ function isModeInCategory(mode, category) { if (!mode || !MODE_LISTS[category]) return false; const modeUpper = mode.toUpperCase(); return MODE_LISTS[category].indexOf(modeUpper) !== -1; } /** * Check if mode matches any mode in a category (substring match) * @param {string} mode - Mode to check * @param {string} category - Category key from MODE_LISTS * @returns {boolean} True if mode contains any mode from category */ function isModeInCategoryPartial(mode, category) { if (!mode || !MODE_LISTS[category]) return false; const modeUpper = mode.toUpperCase(); for (let i = 0; i < MODE_LISTS[category].length; i++) { if (modeUpper.indexOf(MODE_LISTS[category][i]) !== -1) { return true; } } return false; } /** * Check if mode is in any digital category * @param {string} mode - Mode to check * @returns {boolean} True if mode is in any digital category */ function isDigitalCategory(mode) { return isModeInCategory(mode, 'WSJT') || isModeInCategory(mode, 'DIGITAL_OTHER') || isModeInCategory(mode, 'PSK') || isModeInCategory(mode, 'DIGITAL_MODES') || isModeInCategory(mode, 'DIGITAL_HF'); } /** * Convert frequency to ham radio band name * @param {number} frequency - Frequency value * @param {string} unit - Unit of frequency: 'Hz' (default) or 'kHz' * @param {number} marginKhz - Optional margin in kHz to extend band edges (default: 0) * @returns {string} Band name (e.g., '20m', '2m', '70cm') or 'All' if not in a known band */ function frequencyToBand(frequency, unit = 'Hz', marginKhz = 0) { // Convert to Hz if input is in kHz const freqHz = (unit.toLowerCase() === 'khz') ? frequency * 1000 : parseInt(frequency); // Convert margin to Hz (ensure non-negative) const marginHz = Math.max(0, marginKhz) * 1000; // MF/HF Bands (with margin) if (freqHz >= (1800000 - marginHz) && freqHz <= (2000000 + marginHz)) return '160m'; if (freqHz >= (3500000 - marginHz) && freqHz <= (4000000 + marginHz)) return '80m'; if (freqHz >= (5250000 - marginHz) && freqHz <= (5450000 + marginHz)) return '60m'; if (freqHz >= (7000000 - marginHz) && freqHz <= (7300000 + marginHz)) return '40m'; if (freqHz >= (10100000 - marginHz) && freqHz <= (10150000 + marginHz)) return '30m'; if (freqHz >= (14000000 - marginHz) && freqHz <= (14350000 + marginHz)) return '20m'; if (freqHz >= (18068000 - marginHz) && freqHz <= (18168000 + marginHz)) return '17m'; if (freqHz >= (21000000 - marginHz) && freqHz <= (21450000 + marginHz)) return '15m'; if (freqHz >= (24890000 - marginHz) && freqHz <= (24990000 + marginHz)) return '12m'; if (freqHz >= (28000000 - marginHz) && freqHz <= (29700000 + marginHz)) return '10m'; // VHF Bands (with margin) if (freqHz >= (50000000 - marginHz) && freqHz <= (54000000 + marginHz)) return '6m'; if (freqHz >= (70000000 - marginHz) && freqHz <= (71000000 + marginHz)) return '4m'; if (freqHz >= (144000000 - marginHz) && freqHz <= (148000000 + marginHz)) return '2m'; if (freqHz >= (222000000 - marginHz) && freqHz <= (225000000 + marginHz)) return '1.25m'; // UHF Bands (with margin) if (freqHz >= (420000000 - marginHz) && freqHz <= (450000000 + marginHz)) return '70cm'; if (freqHz >= (902000000 - marginHz) && freqHz <= (928000000 + marginHz)) return '33cm'; if (freqHz >= (1240000000 - marginHz) && freqHz <= (1300000000 + marginHz)) return '23cm'; // SHF Bands (with margin) if (freqHz >= (2300000000 - marginHz) && freqHz <= (2450000000 + marginHz)) return '13cm'; if (freqHz >= (3300000000 - marginHz) && freqHz <= (3500000000 + marginHz)) return '9cm'; if (freqHz >= (5650000000 - marginHz) && freqHz <= (5925000000 + marginHz)) return '6cm'; if (freqHz >= (10000000000 - marginHz) && freqHz <= (10500000000 + marginHz)) return '3cm'; if (freqHz >= (24000000000 - marginHz) && freqHz <= (24250000000 + marginHz)) return '1.25cm'; if (freqHz >= (47000000000 - marginHz) && freqHz <= (47200000000 + marginHz)) return '6mm'; if (freqHz >= (75500000000 - marginHz) && freqHz <= (81000000000 + marginHz)) return '4mm'; if (freqHz >= (119980000000 - marginHz) && freqHz <= (120020000000 + marginHz)) return '2.5mm'; if (freqHz >= (142000000000 - marginHz) && freqHz <= (149000000000 + marginHz)) return '2mm'; if (freqHz >= (241000000000 - marginHz) && freqHz <= (250000000000 + marginHz)) return '1mm'; return null; } /** * Alias for backward compatibility - converts frequency in kHz to band name * @deprecated Use frequencyToBand(frequency, 'kHz') instead * @param {number} freq_khz - Frequency in kilohertz * @param {number} marginKhz - Optional margin in kHz to extend band edges (default: 0) * @returns {string} Band name or 'All' */ function frequencyToBandKhz(freq_khz, marginKhz = 0) { return frequencyToBand(freq_khz, 'kHz', marginKhz); } /** * Get a typical (center/common) frequency for a given amateur radio band * Returns frequency in kHz - useful for band changes when no specific frequency is needed * @param {string} band - Band designation (e.g., '20m', '40m', '2m') * @returns {number} Typical frequency in kHz, or 0 if band not recognized * * @example * getTypicalBandFrequency('20m') // → 14100 (kHz) * getTypicalBandFrequency('2m') // → 144300 (kHz) */ function getTypicalBandFrequency(band) { const frequencies = { '160m': 1850, '80m': 3550, '60m': 5357, '40m': 7050, '30m': 10120, '20m': 14100, '17m': 18100, '15m': 21100, '12m': 24920, '10m': 28400, '6m': 50100, '4m': 70100, '2m': 144300, '1.25m': 222100, '70cm': 432100, '33cm': 902100, '23cm': 1296100, '13cm': 2304100, '9cm': 3456100, '6cm': 5760100, '3cm': 10368100, '1.25cm': 24048100 }; return frequencies[band] || 0; } /** * Determine appropriate radio mode based on spot mode and frequency * Uses MODE_LISTS to intelligently map any amateur radio mode to a standard CAT mode * @param {string} spotMode - Mode from DX spot (e.g., 'CW', 'SSB', 'FT8') * @param {number} freqHz - Frequency in Hz * @returns {string} Radio mode (CW, USB, LSB, RTTY, AM, FM, DIGI) */ function determineRadioMode(spotMode, freqHz) { if (!spotMode) { // No mode specified - use frequency to determine USB/LSB return (freqHz / 1000) < LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB'; } const modeUpper = spotMode.toUpperCase(); // CW modes - return CW if (isModeInCategory(spotMode, 'CW')) { return 'CW'; } // Phone modes - determine specific sideband/voice mode if (isModeInCategory(spotMode, 'PHONE')) { // Check if it's a specific CAT mode that should be preserved // USB, LSB, AM, FM are actual CAT modes, not generic SSB/PHONE if (modeUpper === 'USB' || modeUpper === 'LSB' || modeUpper === 'AM' || modeUpper === 'FM') { return modeUpper; } // For generic SSB/PHONE, determine USB/LSB based on frequency return (freqHz / 1000) < LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB'; } // Digital voice modes - use FM as closest analog if (isModeInCategory(spotMode, 'DIGITAL_VOICE')) { return 'FM'; } // All other digital modes - check if radio supports specific mode, otherwise use RTTY/DIGI // WSJT-X, PSK, RTTY, and other digital modes typically use RTTY or DIGI mode on the radio if (isDigitalCategory(spotMode)) { // Some radios support specific digital modes, check for them if (modeUpper === 'RTTY') return 'RTTY'; if (modeUpper === 'PSK') return 'PSK'; if (modeUpper === 'PKTUSB' || modeUpper === 'PKTLSB') return modeUpper; // Default to RTTY for most digital modes (most common CAT mode for digital) return 'RTTY'; } // Unknown mode - default to USB/LSB based on frequency return (freqHz / 1000) < LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB'; } /** * Determine LSB or USB based on frequency (for phone modes) * @param {number} frequency - Frequency in kHz * @returns {string} 'LSB', 'USB', or 'SSB' (fallback if frequency invalid) * * @example * determineSSBMode(7100) // → 'LSB' (below 10 MHz) * determineSSBMode(14200) // → 'USB' (above 10 MHz) */ function determineSSBMode(frequency) { var freq = parseFloat(frequency) || 0; if (freq > 0) { return freq < LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB'; } return 'SSB'; } // ======================================== // BAND GROUP UTILITIES // ======================================== /** * Map individual band to its band group (MF, HF, VHF, UHF, SHF) * @param {string} band - Band identifier (e.g., '20m', '2m', '70cm', '13cm') * @returns {string|null} Band group name or null if band not found */ function getBandGroup(band) { for (const [group, bands] of Object.entries(BAND_GROUPS)) { if (bands.includes(band)) return group; } return null; } /** * Get all bands in a band group * @param {string} group - Band group name (MF, HF, VHF, UHF, or SHF) * @returns {Array} Array of band identifiers or empty array if group not found */ function getBandsInGroup(group) { return BAND_GROUPS[group] || []; } // ======================================== // MODE CATEGORIZATION & CAT UTILITIES // ======================================== /** * Categorize amateur radio mode into phone/cw/digi for filtering * @param {string} mode - Mode name (e.g., 'USB', 'CW', 'FT8', 'phone') * @returns {string|null} Mode category: 'phone', 'cw', 'digi', or null if unknown */ function getModeCategory(mode) { if (!mode) return null; const modeLower = mode.toLowerCase(); // Check if already a category if (['phone', 'cw', 'digi'].includes(modeLower)) { return modeLower; } // CW modes - use MODE_LISTS.CW if (isModeInCategory(mode, 'CW')) { return 'cw'; } // Phone modes - use MODE_LISTS.PHONE if (isModeInCategory(mode, 'PHONE')) { return 'phone'; } // Digital modes - check all digital categories from MODE_LISTS if (isDigitalCategory(mode) || isModeInCategory(mode, 'DIGITAL_VOICE')) { return 'digi'; } // Fallback for generic digital mode strings if (modeLower === 'digi' || modeLower === 'data') { return 'digi'; } return null; } /** * Normalize CAT (Computer Aided Transceiver) mode names to standard modes * Strips radio-specific suffixes and variations to return canonical mode names * @param {string} mode - CAT mode string from radio (e.g., 'CW-U', 'USB-D1', 'RTTY-R') * @returns {string} Normalized mode name (e.g., 'CW', 'USB', 'RTTY') * * @example * catmode('CW-U') // → 'CW' * catmode('USB-D1') // → 'USB' * catmode('RTTY-R') // → 'RTTY' */ function catmode(mode) { switch ((mode || '').toUpperCase()) { case 'CW-U': case 'CW-L': case 'CW-R': case 'CWU': case 'CWL': return 'CW'; case 'RTTY-L': case 'RTTY-U': case 'RTTY-R': return 'RTTY'; case 'USB-D': case 'USB-D1': return 'USB'; case 'LSB-D': case 'LSB-D1': return 'LSB'; default: return (mode || ''); } } /** * Frequency conversion utility * Convert between Hz, kHz, and MHz * * @param {number} value - Frequency value * @param {string} fromUnit - Source unit: 'Hz', 'kHz', or 'MHz' (case-insensitive) * @param {string} [toUnit='kHz'] - Target unit: 'Hz', 'kHz', or 'MHz' (default: 'kHz') * @returns {number} Converted frequency * * @example * convertFrequency(14074000, 'Hz', 'kHz') // → 14074 * convertFrequency(14.074, 'MHz', 'Hz') // → 14074000 * convertFrequency(7074, 'kHz', 'MHz') // → 7.074 * convertFrequency(14074, 'kHz') // → 14074 (defaults to kHz) */ function convertFrequency(value, fromUnit, toUnit) { var freqValue = parseFloat(value) || 0; toUnit = toUnit || 'kHz'; // Default target is kHz // Normalize units to lowercase var from = (fromUnit || 'Hz').toLowerCase(); var to = toUnit.toLowerCase(); // If units are the same, no conversion needed if (from === to) return freqValue; // Convert to Hz first (base unit) var freqHz; switch (from) { case 'hz': freqHz = freqValue; break; case 'khz': freqHz = freqValue * 1000; break; case 'mhz': freqHz = freqValue * 1000000; break; default: freqHz = freqValue; // Assume Hz if unknown } // Convert from Hz to target unit switch (to) { case 'hz': return freqHz; case 'khz': return freqHz / 1000; case 'mhz': return freqHz / 1000000; default: return freqHz / 1000; // Default to kHz } } /** * Legacy wrapper for backward compatibility * @deprecated Use convertFrequency(value, fromUnit, 'kHz') instead */ function convertToKhz(value, unit) { return convertFrequency(value, unit || 'Hz', 'kHz'); } /** * Compare two frequencies with tolerance for floating point precision * @param {number} freq1 - First frequency * @param {number} freq2 - Second frequency * @param {string} unit - Unit: 'Hz', 'kHz', or 'MHz' (default: 'kHz') * @param {number} [tolerance] - Tolerance (default: 0.001 kHz = 1 Hz) * @returns {boolean} True if frequencies are equal within tolerance */ function areFrequenciesEqual(freq1, freq2, unit, tolerance) { unit = unit || 'kHz'; // Convert both to kHz for comparison var freq1Khz = convertFrequency(freq1, unit, 'kHz'); var freq2Khz = convertFrequency(freq2, unit, 'kHz'); // Default tolerance: 1 Hz = 0.001 kHz tolerance = tolerance !== undefined ? tolerance : 0.001; return Math.abs(freq1Khz - freq2Khz) <= tolerance; } /** * Check if a frequency is an FT8 calling frequency (within 5 kHz tolerance) * @param {number} frequency - Frequency value * @param {string} [unit='kHz'] - Unit: 'Hz', 'kHz', or 'MHz' * @returns {boolean} True if frequency is an FT8 calling frequency */ function isFT8Frequency(frequency, unit) { var freqKhz = convertFrequency(frequency, unit || 'kHz', 'kHz'); for (var i = 0; i < FT8_FREQUENCIES.length; i++) { if (Math.abs(freqKhz - FT8_FREQUENCIES[i]) < 5) return true; } return false; } // ======================================== // SIGNAL BANDWIDTH UTILITIES // ======================================== /** * Get signal bandwidth for a radio mode * @param {string} mode - Radio mode (e.g., 'USB', 'CW', 'FT8', 'AM') * @returns {number} Bandwidth in kHz */ function getSignalBandwidth(mode) { if (!mode) return SIGNAL_BANDWIDTHS.SSB; var modeUpper = mode.toUpperCase(); // Check exact matches first if (SIGNAL_BANDWIDTHS[modeUpper]) return SIGNAL_BANDWIDTHS[modeUpper]; // Fallback for substring matches (e.g., mode variations not in exact list) if (modeUpper.indexOf('CW') !== -1) return SIGNAL_BANDWIDTHS.CW; if (modeUpper.indexOf('RTTY') !== -1) return SIGNAL_BANDWIDTHS.RTTY; if (modeUpper.indexOf('PSK') !== -1) return SIGNAL_BANDWIDTHS.PSK; // Default to SSB bandwidth for phone modes return SIGNAL_BANDWIDTHS.SSB; } // ======================================== // MODE CLASSIFICATION FOR DX SPOTS // ======================================== /** * Check if mode is CW * @param {string} mode - Radio mode * @returns {boolean} True if mode is CW */ function isCwMode(mode) { return isModeInCategory(mode, 'CW') || (mode && mode.toLowerCase().includes('cw')); } /** * Check if mode is phone/voice * @param {string} mode - Radio mode * @returns {boolean} True if mode is phone/voice */ function isPhoneMode(mode) { if (!mode) return false; return isModeInCategory(mode, 'PHONE') || mode.toLowerCase() === 'phone'; } /** * Check if mode is digital * @param {string} mode - Radio mode * @returns {boolean} True if mode is digital */ function isDigiMode(mode) { if (!mode) return false; // Check all digital categories from MODE_LISTS return isDigitalCategory(mode) || isModeInCategory(mode, 'DIGITAL_VOICE') || mode.toLowerCase() === 'digi' || mode.toLowerCase() === 'data'; } /** * Comprehensive mode classification system * Classifies a DX spot into phone, CW, digi, or other categories * * @param {Object} spot - DX spot object with mode and optional message fields * @param {string} spot.mode - The transmission mode * @param {string} [spot.message] - Optional spot comment/message for additional classification hints * @returns {{category: string, submode: string, confidence: number}} Classification result * - category: 'phone', 'cw', 'digi', or 'other' * - submode: Specific mode name (e.g., 'FT8', 'USB', 'CW') * - confidence: 0-1, where 1 is high confidence, 0.3 is low */ function classifyMode(spot) { if (!spot || !spot.mode || spot.mode === '') { return { category: 'phone', submode: 'SSB', confidence: 0 }; } var mode = spot.mode.toUpperCase(); var message = (spot.message || '').toUpperCase(); // Check message first for higher accuracy var messageResult = classifyFromMessage(message); if (messageResult.category) { return { category: messageResult.category, submode: messageResult.submode, confidence: messageResult.confidence }; } // Fall back to mode field classification return classifyFromMode(mode); } /** * Classify mode from spot message text * @param {string} message - Spot message/comment * @returns {{category: string|null, submode: string|null, confidence: number}} */ function classifyFromMessage(message) { if (!message) return { category: null, submode: null, confidence: 0 }; // Check CW modes from MODE_LISTS.CW for (var i = 0; i < MODE_LISTS.CW.length; i++) { if (message.indexOf(MODE_LISTS.CW[i]) !== -1) { return { category: 'cw', submode: MODE_LISTS.CW[i], confidence: 1 }; } } // Check all digital mode categories var digitalCategories = ['WSJT', 'PSK', 'DIGITAL_OTHER', 'DIGITAL_MODES', 'DIGITAL_VOICE', 'DIGITAL_HF']; for (var cat = 0; cat < digitalCategories.length; cat++) { var categoryName = digitalCategories[cat]; var modes = MODE_LISTS[categoryName]; for (var i = 0; i < modes.length; i++) { if (message.indexOf(modes[i]) !== -1) { return { category: 'digi', submode: modes[i], confidence: 1 }; } } } // Check phone modes from MODE_LISTS.PHONE (with word boundaries for accuracy) for (var i = 0; i < MODE_LISTS.PHONE.length; i++) { var pattern = '\\b' + MODE_LISTS.PHONE[i] + '\\b'; if (new RegExp(pattern).test(message)) { return { category: 'phone', submode: MODE_LISTS.PHONE[i], confidence: 1 }; } } return { category: null, submode: null, confidence: 0 }; } /** * Classify mode from mode field * @param {string} mode - Mode string from spot * @returns {{category: string, submode: string, confidence: number}} */ function classifyFromMode(mode) { // CW modes - use MODE_LISTS.CW if (isModeInCategory(mode, 'CW')) { return { category: 'cw', submode: 'CW', confidence: 1 }; } // Phone modes - use MODE_LISTS.PHONE if (isModeInCategory(mode, 'PHONE')) { return { category: 'phone', submode: mode, confidence: 1 }; } // Digital modes - check all digital categories from MODE_LISTS if (isDigitalCategory(mode) || isModeInCategory(mode, 'DIGITAL_VOICE')) { return { category: 'digi', submode: mode, confidence: 1 }; } // Unknown mode - default to phone/SSB return { category: 'phone', submode: mode || 'SSB', confidence: 0.3 }; } /** * Compare two frequencies with tolerance * Optimized version for general frequency comparison * @param {number} freq1 - First frequency in kHz * @param {number} freq2 - Second frequency in kHz * @param {number} [tolerance=0.001] - Tolerance in kHz (default: 1 Hz) * @returns {boolean} - True if frequencies are equal within tolerance */ function areFrequenciesEqualSimple(freq1, freq2, tolerance) { tolerance = tolerance !== undefined ? tolerance : 0.001; // Default 1 Hz return Math.abs(freq1 - freq2) <= tolerance; } // ======================================== // GEOGRAPHIC UTILITIES // ======================================== /** * Map continent code to IARU region number * @param {string} continent - Two-letter continent code (EU, AF, NA, SA, AS, OC, AN) * @returns {number} IARU region number (1, 2, or 3) * * IARU Region 1: Europe, Africa, Middle East, Northern Asia * IARU Region 2: Americas (North, Central, South) * IARU Region 3: Asia-Pacific (Southern Asia, Oceania) * * @example * continentToRegion('EU') // → 1 (Europe = Region 1) * continentToRegion('NA') // → 2 (North America = Region 2) * continentToRegion('AS') // → 3 (Asia = Region 3) */ function continentToRegion(continent) { switch(continent) { case 'EU': // Europe case 'AF': // Africa return 1; // IARU Region 1 case 'NA': // North America case 'SA': // South America return 2; // IARU Region 2 case 'AS': // Asia case 'OC': // Oceania return 3; // IARU Region 3 case 'AN': // Antarctica return 1; // Default to Region 1 for Antarctica default: return 1; // Default to Region 1 if unknown } }