Functions extracted to external file

This commit is contained in:
Szymon Porwolik
2025-11-06 21:37:00 +01:00
parent 55f117139d
commit 44ecacde4d
5 changed files with 935 additions and 815 deletions

View File

@@ -1,7 +1,6 @@
<!-- DX Waterfall Component - START -->
<?php if ($this->session->userdata('user_dxwaterfall_enable') == 'Y' && isset($manual_mode) && $manual_mode == 0) { ?>
<!-- DX Waterfall Component - JS -->
<script src="<?php echo base_url() ;?>assets/js/dxwaterfall.js?v=<?php echo floor(time() / 3600); ?>"></script>
<!-- DX Waterfall Component - JS loaded in footer after radiohelpers.js -->
<script language="javascript">
/*
DX Waterfall Language

View File

@@ -148,6 +148,7 @@
<?php } ?>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet/leaflet.geodesic.js"></script>
<script type="text/javascript" src="<?php echo base_url() ;?>assets/js/radiohelpers.js"></script>
<script type="text/javascript" src="<?php echo base_url() ;?>assets/js/dxwaterfall.js?v=<?php echo floor(time() / 3600); ?>"></script>
<script type="text/javascript" src="<?php echo base_url() ;?>assets/js/darkmodehelpers.js"></script>
<script src="<?php echo base_url(); ?>assets/js/bootstrapdialog/js/bootstrap-dialog.min.js"></script>
<script type="text/javascript" src="<?php echo base_url() ;?>assets/js/easyprint.js"></script>
@@ -367,6 +368,7 @@ function stopImpersonate_modal() {
<?php if ($this->uri->segment(1) == "qso" ) { ?>
<!-- Javascript used for QSO Notes Area -->
<script src="<?php echo base_url() ;?>assets/plugins/easymde/easymde.min.js"></script>
<script type="text/javascript" src="<?php echo base_url() ;?>assets/js/dxwaterfall.js?v=<?php echo floor(time() / 3600); ?>"></script>
<?php } ?>
<?php if ($this->uri->segment(1) == "notes" && ($this->uri->segment(2) == "view") ) { ?>

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,219 @@
/**
* 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<number>}
*/
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<string>}
*/
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': ['6m', '4m', '2m', '1.25m'],
'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
@@ -56,65 +272,118 @@ function frequencyToBandKhz(freq_khz) {
return frequencyToBand(freq_khz, 'kHz');
}
/**
* 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)
* @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 < 10000000 ? 'LSB' : 'USB'; // Below 10 MHz = LSB, above = USB
return (freqHz / 1000) < LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB';
}
const modeUpper = spotMode.toUpperCase();
// CW modes
if (modeUpper === 'CW' || modeUpper === 'A1A') {
// CW modes - return CW
if (isModeInCategory(spotMode, 'CW')) {
return 'CW';
}
// Digital modes - use RTTY as standard digital mode
const digitalModes = ['FT8', 'FT4', 'PSK', 'RTTY', 'JT65', 'JT9', 'WSPR', 'FSK', 'MFSK', 'OLIVIA', 'CONTESTI', 'DOMINO'];
for (let i = 0; i < digitalModes.length; i++) {
if (modeUpper.indexOf(digitalModes[i]) !== -1) {
return 'RTTY';
// 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';
}
// Phone modes or SSB - determine USB/LSB based on frequency
if (modeUpper.indexOf('SSB') !== -1 || modeUpper.indexOf('PHONE') !== -1 ||
modeUpper === 'USB' || modeUpper === 'LSB' || modeUpper === 'AM' || modeUpper === 'FM') {
// If already USB or LSB, use as-is
if (modeUpper === 'USB') return 'USB';
if (modeUpper === 'LSB') return 'LSB';
if (modeUpper === 'AM') return 'AM';
if (modeUpper === 'FM') return 'FM';
// Otherwise determine based on frequency
return freqHz < 10000000 ? 'LSB' : 'USB';
// Digital voice modes - use FM as closest analog
if (isModeInCategory(spotMode, 'DIGITAL_VOICE')) {
return 'FM';
}
// Default: use frequency to determine USB/LSB
return freqHz < 10000000 ? 'LSB' : 'USB';
// 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';
}
/**
* Ham 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
* 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)
*/
const BAND_GROUPS = {
'MF': ['160m'],
'HF': ['80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m'],
'VHF': ['6m', '4m', '2m', '1.25m'],
'UHF': ['70cm', '33cm', '23cm'],
'SHF': ['13cm', '9cm', '6cm', '3cm', '1.25cm', '6mm', '4mm', '2.5mm', '2mm', '1mm']
};
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)
@@ -137,6 +406,10 @@ 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')
@@ -152,35 +425,40 @@ function getModeCategory(mode) {
return modeLower;
}
const modeUpper = mode.toUpperCase();
// CW modes
if (['CW', 'CWR', 'A1A'].includes(modeUpper) || modeLower.includes('cw')) {
// CW modes - use MODE_LISTS.CW
if (isModeInCategory(mode, 'CW')) {
return 'cw';
}
// Phone modes (voice)
if (['SSB', 'LSB', 'USB', 'FM', 'AM', 'DV', 'PHONE', 'C3E', 'J3E'].includes(modeUpper)) {
// Phone modes - use MODE_LISTS.PHONE
if (isModeInCategory(mode, 'PHONE')) {
return 'phone';
}
// Digital modes
const digitalModes = ['RTTY', 'PSK', 'PSK31', 'PSK63', 'FT8', 'FT4', 'JT65', 'JT9', 'MFSK',
'OLIVIA', 'CONTESTIA', 'HELL', 'THROB', 'SSTV', 'FAX', 'PACKET', 'PACTOR',
'THOR', 'DOMINO', 'MT63', 'ROS', 'WSPR', 'VARA', 'ARDOP', 'WINMOR'];
if (digitalModes.includes(modeUpper)) {
// Digital modes - check all digital categories from MODE_LISTS
if (isDigitalCategory(mode) || isModeInCategory(mode, 'DIGITAL_VOICE')) {
return 'digi';
}
// Check for digital mode substrings
if (modeLower.includes('ft') || modeLower.includes('psk') || modeLower.includes('rtty') ||
modeLower.includes('jt') || modeLower === 'digi' || modeLower === 'data') {
// 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':
@@ -189,26 +467,329 @@ function catmode(mode) {
case 'CWU':
case 'CWL':
return 'CW';
break;
case 'RTTY-L':
case 'RTTY-U':
case 'RTTY-R':
return 'RTTY';
break;
case 'USB-D':
case 'USB-D1':
return 'USB';
break;
case 'LSB-D':
case 'LSB-D1':
return 'LSB';
break;
default:
return (mode || '');;
break;
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();
var confidence = 1; // 1 = high confidence, 0.5 = medium, 0.3 = low
// 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
}
}
/**
* Convert latitude/longitude coordinates to Maidenhead grid square locator
* @param {number} y - Latitude in decimal degrees (-90 to +90)
* @param {number} x - Longitude in decimal degrees (-180 to +180)
* @param {number} num - Precision level: 2=field, 4=square, 6=subsquare, 8=extended, 10=extended subsquare
* @returns {string} Maidenhead locator string (e.g., 'JO01ab' for 6-character precision)
*
* @example
* LatLng2Loc(51.5074, -0.1278, 6) // → 'IO91wm' (London)
* LatLng2Loc(40.7128, -74.0060, 4) // → 'FN20' (New York)
*/
function LatLng2Loc(y, x, num) {
if (x<-180) {x=x+360;}
if (x>180) {x=x-360;}
@@ -238,5 +819,4 @@ function LatLng2Loc(y, x, num) {
if (num >= 8) qthloc+=' ' + String.fromCharCode(yn[6] + 0x30) + String.fromCharCode(yn[7] + 0x30);
if (num >= 10) qthloc+=String.fromCharCode(yn[8] + 0x61) + String.fromCharCode(yn[9] + 0x61);
return qthloc;
}

View File

@@ -1,3 +1,59 @@
// ========================================
// PLATFORM DETECTION UTILITIES
// ========================================
/**
* Platform detection utilities using modern userAgentData API with fallback
*/
var PlatformDetection = {
/**
* Check if the current platform is macOS
* @returns {boolean} True if platform is macOS
*/
isMac: function() {
// Use modern userAgentData API if available, fallback to userAgent
if (navigator.userAgentData && navigator.userAgentData.platform) {
return navigator.userAgentData.platform.toUpperCase().indexOf('MAC') >= 0;
}
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
},
/**
* Check if the current platform is Windows
* @returns {boolean} True if platform is Windows
*/
isWindows: function() {
if (navigator.userAgentData && navigator.userAgentData.platform) {
return navigator.userAgentData.platform.toUpperCase().indexOf('WIN') >= 0;
}
return navigator.userAgent.toUpperCase().indexOf('WIN') >= 0;
},
/**
* Check if the current platform is Linux
* @returns {boolean} True if platform is Linux
*/
isLinux: function() {
if (navigator.userAgentData && navigator.userAgentData.platform) {
return navigator.userAgentData.platform.toUpperCase().indexOf('LINUX') >= 0;
}
return navigator.userAgent.toUpperCase().indexOf('LINUX') >= 0;
},
/**
* Check if the modifier key is pressed (Cmd on Mac, Ctrl on Windows/Linux)
* @param {Event} event - The keyboard or mouse event
* @returns {boolean} True if the platform-specific modifier key is pressed
*/
isModifierKey: function(event) {
return this.isMac() ? event.metaKey : event.ctrlKey;
}
};
// ========================================
// QSO FORM UTILITIES
// ========================================
function setRst(mode) {
if(mode == 'JT65' || mode == 'JT65B' || mode == 'JT6C' || mode == 'JTMS' || mode == 'ISCAT' || mode == 'MSK144' || mode == 'JTMSK' || mode == 'QRA64' || mode == 'FT8' || mode == 'FT4' || mode == 'JS8' || mode == 'JT9' || mode == 'JT9-1' || mode == 'ROS'){
$('#rst_sent').val('-5');
@@ -1321,6 +1377,42 @@ function showToast(title, text, type = 'bg-success text-white', delay = 3000) {
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
}
/**
* Cookie Management Utilities
*/
/**
* Set a cookie
* @param {string} name - Cookie name
* @param {string} value - Cookie value
* @param {number} days - Days until expiration
*/
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
/**
* Get a cookie value
* @param {string} name - Cookie name
* @returns {string|null} Cookie value or null if not found
*/
function getCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// DO NOT DELETE: This message is intentional and serves as developer recruitment/engagement
console.log("Ready to unleash your coding prowess and join the fun?\n\n" +
"Check out our GitHub Repository and dive into the coding adventure:\n\n" +