diff --git a/application/views/components/dxwaterfall.php b/application/views/components/dxwaterfall.php
index 1cd77268f..6a28e706b 100644
--- a/application/views/components/dxwaterfall.php
+++ b/application/views/components/dxwaterfall.php
@@ -1,7 +1,6 @@
session->userdata('user_dxwaterfall_enable') == 'Y' && isset($manual_mode) && $manual_mode == 0) { ?>
-
-
+
+
@@ -367,6 +368,7 @@ function stopImpersonate_modal() {
uri->segment(1) == "qso" ) { ?>
+
uri->segment(1) == "notes" && ($this->uri->segment(2) == "view") ) { ?>
diff --git a/assets/js/dxwaterfall.js b/assets/js/dxwaterfall.js
index 53cc0b3b0..a8f27274e 100644
--- a/assets/js/dxwaterfall.js
+++ b/assets/js/dxwaterfall.js
@@ -1,7 +1,7 @@
// @ts-nocheck
/**
* @fileoverview DX WATERFALL for WaveLog
- * @version 0.9.2 // also change line 38
+ * @version 0.9.3 // also change line 38
* @author Wavelog Team
*
* @description
@@ -13,17 +13,10 @@
* @requires setFrequency (global function from Wavelog)
* @requires setMode (global function from Wavelog)
* @requires frequencyToBand (global function from Wavelog)
- *
- * @browserSupport
- * - Chrome 90+
- * - Firefox 88+
- * - Safari 14+
- * - Edge 90+
- *
+
* @features
* - Canvas-based visualization
* - ES6+ syntax (const/let recommended, var used for compatibility)
- * - Navigator.userAgentData (with fallback to userAgent)
* - Passive event listeners for scroll performance
*/
@@ -36,7 +29,7 @@
var DX_WATERFALL_CONSTANTS = {
// Version
- VERSION: '0.9.2', // DX Waterfall version (keep in sync with @version in file header)
+ VERSION: '0.9.3', // DX Waterfall version (keep in sync with @version in file header)
// Debug and logging
DEBUG_MODE: true, // Set to true for verbose logging, false for production
@@ -57,7 +50,6 @@ var DX_WATERFALL_CONSTANTS = {
ZOOM_ICON_FEEDBACK_MS: 150, // Visual feedback duration for zoom icons
MODE_CHANGE_SETTLE_MS: 200, // Delay for radio mode change to settle
FORM_POPULATE_DELAY_MS: 50, // Delay before populating QSO form
- SPOT_NAVIGATION_COMPLETE_MS: 100, // Delay for spot navigation completion
ZOOM_MENU_UPDATE_DELAY_MS: 150 // Delay for zoom menu update after navigation
},
@@ -68,14 +60,18 @@ var DX_WATERFALL_CONSTANTS = {
TUNING_FLAG_FALLBACK_MS: 4500, // Fallback timeout for tuning flags (1.5x poll interval)
FREQUENCY_WAIT_TIMEOUT_MS: 6000, // Initial load wait time for CAT frequency (2x poll interval)
- // WebSocket timing (low latency)
- WEBSOCKET_CONFIRM_TIMEOUT_MS: 500, // WebSocket: Fast confirmation timeout (vs 3000ms polling)
- WEBSOCKET_FALLBACK_TIMEOUT_MS: 750, // WebSocket: Fast fallback timeout (vs 1.5x poll interval)
- WEBSOCKET_COMMIT_DELAY_MS: 20, // WebSocket: Fast commit delay (vs 50ms polling)
+ // WebSocket timing (low latency - no overlay blink)
+ WEBSOCKET_CONFIRM_TIMEOUT_MS: 300, // WebSocket: Very fast confirmation timeout (vs 3000ms polling)
+ WEBSOCKET_FALLBACK_TIMEOUT_MS: 500, // WebSocket: Fast fallback timeout (vs 1.5x poll interval)
+ WEBSOCKET_COMMIT_DELAY_MS: 10, // WebSocket: Minimal commit delay (vs 50ms polling)
+ WEBSOCKET_OVERLAY_DURATION_MS: 30, // WebSocket: Not used (overlay skipped for WebSocket)
+ WEBSOCKET_OVERLAY_FALLBACK_MS: 100, // WebSocket: Not used (overlay skipped for WebSocket)
// Polling timing (standard latency)
POLLING_CONFIRM_TIMEOUT_MS: 3000, // Polling: Standard confirmation timeout
POLLING_COMMIT_DELAY_MS: 50, // Polling: Standard commit delay (from DEBOUNCE.FREQUENCY_COMMIT_SHORT_MS)
+ POLLING_OVERLAY_DURATION_MS: 250, // Polling: Standard overlay duration after frequency change
+ POLLING_OVERLAY_FALLBACK_MS: 450, // Polling: Standard overlay fallback timeout
// Auto-populate timing
TUNING_STOPPED_DELAY_MS: 1000 // Delay after user stops tuning to auto-populate spot (1 second)
@@ -209,42 +205,9 @@ var DX_WATERFALL_CONSTANTS = {
LABEL_HEIGHTS: [13, 14, 15, 17, 19] // Heights for overlap detection
},
- // Available continents for cycling
- CONTINENTS: ['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA'],
-
- // Mode classification lists (consolidated from multiple locations)
- 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']
- },
-
// Logo configuration
LOGO_FILENAME: 'assets/logo/wavelog_logo_darkly_wide.png',
- // Frequency thresholds (in kHz)
- LSB_USB_THRESHOLD_KHZ: 10000, // Below 10 MHz = LSB, above = USB
-
- // Signal bandwidth constants (in kHz)
- SIGNAL_BANDWIDTHS: {
- SSB_KHZ: 2.7, // Standard SSB bandwidth
- SSB_OFFSET_KHZ: 1.35, // Half bandwidth for offset
- AM_KHZ: 6.0, // AM bandwidth
- FM_KHZ: 12.0, // FM bandwidth (wide)
- CW_DETECTION_KHZ: 0.25 // CW detection range
- },
-
- // Static FT8 frequencies (in kHz)
- FT8_FREQUENCIES: [1840, 3573, 7074, 10136, 14074, 18100, 21074, 24915, 28074, 50313, 144174, 432065]
};
// ========================================
@@ -274,13 +237,17 @@ function getCATTimings() {
return {
confirmTimeout: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_CONFIRM_TIMEOUT_MS,
fallbackTimeout: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_FALLBACK_TIMEOUT_MS,
- commitDelay: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_COMMIT_DELAY_MS
+ commitDelay: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_COMMIT_DELAY_MS,
+ overlayDuration: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_OVERLAY_DURATION_MS,
+ overlayFallback: DX_WATERFALL_CONSTANTS.CAT.WEBSOCKET_OVERLAY_FALLBACK_MS
};
} else {
return {
confirmTimeout: DX_WATERFALL_CONSTANTS.CAT.POLLING_CONFIRM_TIMEOUT_MS,
fallbackTimeout: DX_WATERFALL_CONSTANTS.CAT.TUNING_FLAG_FALLBACK_MS,
- commitDelay: DX_WATERFALL_CONSTANTS.CAT.POLLING_COMMIT_DELAY_MS
+ commitDelay: DX_WATERFALL_CONSTANTS.CAT.POLLING_COMMIT_DELAY_MS,
+ overlayDuration: DX_WATERFALL_CONSTANTS.CAT.POLLING_OVERLAY_DURATION_MS,
+ overlayFallback: DX_WATERFALL_CONSTANTS.CAT.POLLING_OVERLAY_FALLBACK_MS
};
}
}
@@ -302,9 +269,10 @@ function handleCATFrequencyUpdate(radioFrequency, updateCallback) {
if (typeof dxWaterfall !== 'undefined' && dxWaterfall.lastValidCommittedFreq !== null && dxWaterfall.lastValidCommittedUnit) {
// Compare incoming CAT frequency with last committed value
// CAT sends frequency in Hz, convert to kHz for comparison
- var lastKhz = DX_WATERFALL_UTILS.frequency.convertToKhz(
+ var lastKhz = convertFrequency(
dxWaterfall.lastValidCommittedFreq,
- dxWaterfall.lastValidCommittedUnit
+ dxWaterfall.lastValidCommittedUnit,
+ 'kHz'
);
var incomingHz = parseFloat(radioFrequency);
var incomingKhz = incomingHz / 1000; // Convert Hz to kHz
@@ -331,10 +299,16 @@ function handleCATFrequencyUpdate(radioFrequency, updateCallback) {
if (diff <= toleranceHz) {
// Frequency matches! Radio has tuned to target
- // Clear target AFTER a short delay to ensure waterfall updates first
- // This prevents the overlay from disappearing before the marker moves
+ // For WebSocket connections, clear overlay immediately since confirmation is instant
+ // For polling, keep brief delay to ensure smooth visual update
dxWaterfall.targetFrequencyConfirmAttempts = 0;
+ var timings = getCATTimings();
+ var isWebSocket = typeof dxwaterfall_cat_state !== 'undefined' && dxwaterfall_cat_state === 'websocket';
+
+ // WebSocket: Clear immediately, Polling: Brief delay for visual smoothness
+ var clearDelay = isWebSocket ? 0 : 100;
+
// Use setTimeout to clear the target after the waterfall has updated
setTimeout(function() {
if (typeof dxWaterfall !== 'undefined') {
@@ -342,8 +316,9 @@ function handleCATFrequencyUpdate(radioFrequency, updateCallback) {
dxWaterfall.frequencyChanging = false; // Also clear frequencyChanging flag
dxWaterfall.catTuning = false; // Clear CAT tuning flag - radio is now at target
dxWaterfall.catTuningStartTime = null;
+ dxWaterfall.showingCompletionOverlay = false; // Clear overlay immediately on confirmation
}
- }, 100); // 100ms delay ensures waterfall renders at new position before overlay clears
+ }, clearDelay);
shouldSkipStaleUpdate = false; // Proceed normally - radio is at correct frequency
} else {
@@ -459,33 +434,8 @@ var DX_WATERFALL_UTILS = {
}
},
- // Frequency conversion utilities
+ // Frequency utilities
frequency: {
- hzToKhz: function(hz) {
- return hz / 1000;
- },
-
- mhzToKhz: function(mhz) {
- return mhz * 1000;
- },
-
- // Convert any frequency unit to kHz
- convertToKhz: function(value, unit) {
- var freqValue = parseFloat(value) || 0;
- switch (unit.toLowerCase()) {
- case 'hz':
- return freqValue / 1000;
- case 'khz':
- return freqValue;
- case 'mhz':
- return freqValue * 1000;
- case 'ghz':
- return freqValue * 1000000;
- default:
- return freqValue; // Default to kHz
- }
- },
-
// Validate frequency value
isValid: function(value) {
var freq = parseFloat(value) || 0;
@@ -496,18 +446,6 @@ var DX_WATERFALL_UTILS = {
parseAndValidate: function(value) {
var freq = parseFloat(value) || 0;
return { value: freq, valid: freq > 0 };
- },
-
- /**
- * Compare two frequencies with tolerance for floating point precision
- * @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
- */
- areEqual: function(freq1, freq2, tolerance) {
- tolerance = tolerance !== undefined ? tolerance : 0.001; // Default 1 Hz
- return Math.abs(freq1 - freq2) <= tolerance;
}
},
@@ -547,41 +485,6 @@ var DX_WATERFALL_UTILS = {
}
},
- // Cookie utilities
- cookie: {
- /**
- * Set a cookie
- * @param {string} name - Cookie name
- * @param {string} value - Cookie value
- * @param {number} days - Days until expiration
- */
- set: function(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
- */
- get: function(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;
- }
- },
-
// DOM selector utilities (cached for performance)
dom: {
waterfall: null,
@@ -595,36 +498,6 @@ var DX_WATERFALL_UTILS = {
}
},
- // Platform detection utilities
- platform: {
- 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;
- },
-
- isWindows: function() {
- if (navigator.userAgentData && navigator.userAgentData.platform) {
- return navigator.userAgentData.platform.toUpperCase().indexOf('WIN') >= 0;
- }
- return navigator.userAgent.toUpperCase().indexOf('WIN') >= 0;
- },
-
- 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)
- isModifierKey: function(event) {
- return this.isMac() ? event.metaKey : event.ctrlKey;
- }
- },
-
// Field mapping utilities - allows pages to remap field IDs
fieldMapping: {
/**
@@ -677,35 +550,8 @@ var DX_WATERFALL_UTILS = {
}
},
- // Mode classification utilities
+ // Mode classification utilities (now use global functions from radiohelpers.js)
modes: {
- isCw: function(mode) {
- return mode && mode.toLowerCase().includes('cw');
- },
-
- isPhone: function(mode) {
- if (!mode) return false;
- const m = mode.toLowerCase();
- return m.includes('ssb') || m.includes('lsb') || m.includes('usb') ||
- m.includes('am') || m.includes('fm') || m === 'phone';
- },
-
- isDigi: function(mode) {
- if (!mode) return false;
- const m = mode.toLowerCase();
- return m.includes('ft') || m.includes('rtty') || m.includes('psk') ||
- m.includes('jt') || m.includes('mfsk') || m.includes('olivia') ||
- m.includes('contestia') || m.includes('hell') || m.includes('throb') ||
- m.includes('sstv') || m.includes('fax') || m === 'digi' || m === 'data';
- },
-
- getModeCategory: function(mode) {
- if (this.isCw(mode)) return 'cw';
- if (this.isPhone(mode)) return 'phone';
- if (this.isDigi(mode)) return 'digi';
- return 'other';
- },
-
// Get color for a classified mode with customizable alpha
getModeColor: function(classifiedMode, alpha) {
alpha = alpha !== undefined ? alpha : 0.6; // Default 60% opacity
@@ -724,146 +570,6 @@ var DX_WATERFALL_UTILS = {
default:
return DX_WATERFALL_CONSTANTS.COLORS.OTHER_RGB;
}
- },
-
- /**
- * 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
- */
- classifyMode: function(spot) {
- if (!spot || !spot.mode || spot.mode === '') {
- return { category: 'other', submode: 'Unknown', 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 = this.classifyFromMessage(message);
- if (messageResult.category) {
- return {
- category: messageResult.category,
- submode: messageResult.submode,
- confidence: messageResult.confidence
- };
- }
-
- // Fall back to mode field classification
- return this.classifyFromMode(mode);
- },
-
- classifyFromMessage: function(message) {
- // CW detection in message
- if (message.indexOf('CW') !== -1) {
- return { category: 'cw', submode: 'CW', confidence: 1 };
- }
-
- // Digital modes from message
- var digiModes = [
- { patterns: ['FT8'], submode: 'FT8' },
- { patterns: ['FT4'], submode: 'FT4' },
- { patterns: ['RTTY'], submode: 'RTTY' },
- { patterns: ['PSK31'], submode: 'PSK31' },
- { patterns: ['PSK'], submode: 'PSK' },
- { patterns: ['JT65'], submode: 'JT65' },
- { patterns: ['JT9'], submode: 'JT9' },
- { patterns: ['WSPR'], submode: 'WSPR' },
- { patterns: ['JS8'], submode: 'JS8' }
- ];
-
- // Optimized loop - breaks early on first match
- for (var i = 0; i < digiModes.length; i++) {
- var mode = digiModes[i];
- for (var j = 0; j < mode.patterns.length; j++) {
- if (message.indexOf(mode.patterns[j]) !== -1) {
- return { category: 'digi', submode: mode.submode, confidence: 1 };
- }
- }
- }
-
- // Phone modes from message (use constants)
- var phoneModes = DX_WATERFALL_CONSTANTS.MODE_LISTS.PHONE.slice(0, 5); // LSB, USB, SSB, AM, FM
- var phonePatterns = [
- { patterns: ['LSB'], submode: 'LSB' },
- { patterns: ['USB'], submode: 'USB' },
- { patterns: ['SSB'], submode: 'SSB' },
- { patterns: ['AM'], submode: 'AM' },
- { patterns: ['FM'], submode: 'FM' }
- ];
-
- // Optimized loop - breaks early on first match
- for (var i = 0; i < phonePatterns.length; i++) {
- var mode = phonePatterns[i];
- for (var j = 0; j < mode.patterns.length; j++) {
- // Use word boundary to avoid false matches
- var pattern = '\\b' + mode.patterns[j] + '\\b';
- if (new RegExp(pattern).test(message)) {
- return { category: 'phone', submode: mode.submode, confidence: 1 };
- }
- }
- }
-
- return { category: null, submode: null, confidence: 0 };
- },
-
- classifyFromMode: function(mode) {
- // CW modes
- if (DX_WATERFALL_CONSTANTS.MODE_LISTS.CW.indexOf(mode) !== -1) {
- return { category: 'cw', submode: 'CW', confidence: 1 };
- }
-
- // Phone modes (use constants)
- if (DX_WATERFALL_CONSTANTS.MODE_LISTS.PHONE.indexOf(mode) !== -1) {
- return { category: 'phone', submode: mode, confidence: 1 };
- }
-
- // Digital modes - WSJT-X family (use constants)
- if (DX_WATERFALL_CONSTANTS.MODE_LISTS.WSJT.indexOf(mode) !== -1) {
- return { category: 'digi', submode: mode, confidence: 1 };
- }
-
- // PSK variants
- if (mode.indexOf('PSK') !== -1 || mode.indexOf('QPSK') !== -1 || mode.indexOf('8PSK') !== -1) {
- return { category: 'digi', submode: mode, confidence: 1 };
- }
-
- // Other digital modes (use constants)
- if (DX_WATERFALL_CONSTANTS.MODE_LISTS.DIGITAL_OTHER.indexOf(mode) !== -1) {
- return { category: 'digi', submode: mode, confidence: 1 };
- }
-
- // Pattern-based digital mode detection
- if (mode.indexOf('HELL') !== -1 || mode.indexOf('FSK') === 0 ||
- mode.indexOf('THOR') !== -1 || mode.indexOf('THROB') !== -1 ||
- mode.indexOf('DOM') !== -1 || mode.indexOf('VARA') !== -1) {
- return { category: 'digi', submode: mode, confidence: 1 };
- }
-
- // Unknown mode - ensure we return a valid submode string
- return { category: 'other', submode: mode || 'Unknown', confidence: 0.3 };
- },
-
- // Determine LSB/USB for SSB based on frequency
- determineSSBMode: function(frequency) {
- var freq = parseFloat(frequency) || 0;
- if (freq > 0) {
- return freq < DX_WATERFALL_CONSTANTS.LSB_USB_THRESHOLD_KHZ ? 'LSB' : 'USB';
- }
- return 'SSB';
- },
-
- // Enhanced detailed submode information using unified classification
- getDetailedSubmode: function(spot) {
- return this.classifyMode(spot);
}
},
@@ -1040,57 +746,14 @@ var DX_WATERFALL_UTILS = {
pendingPopulationTimer: null,
pendingLookupTimer: null,
- // Cached jQuery selectors for performance
- $btnReset: null,
-
- /**
- * Initialize cached selectors
- * Call this once when DOM is ready
- */
- initCache: function() {
- this.$btnReset = $('#btn_reset');
- },
-
/**
* Clear the QSO form by clicking the reset button
- * Uses cached selector for performance
+ * Note: reset_fields() in qso.js handles all field clearing including park references
*/
clearForm: function() {
- // Initialize cache if not done yet
- if (!this.$btnReset) {
- this.initCache();
- }
-
- // Explicitly clear park reference fields FIRST
- // This ensures they're cleared even if reset button doesn't fully clear them
- var parkRefFields = [
- {selector: '#sota_ref', isSelectize: true},
- {selector: '#pota_ref', isSelectize: true},
- {selector: '#wwff_ref', isSelectize: true},
- {selector: '#iota_ref', isSelectize: false}, // IOTA is not selectize
- {selector: '#darc_dok', isSelectize: true}
- ];
-
- parkRefFields.forEach(function(field) {
- var $element = $(field.selector);
- if ($element.length > 0) {
- if (field.isSelectize) {
- // For selectize fields, must call .selectize() method first to get instance
- var $select = $element.selectize();
- var selectize = $select[0].selectize;
- if (selectize) {
- selectize.clear();
- }
- } else {
- // Use standard val("") for non-selectize fields (IOTA)
- $element.val("");
- }
- }
- });
-
- // Then click the reset button to clear other fields
- if (this.$btnReset && this.$btnReset.length > 0) {
- this.$btnReset.click();
+ var $btnReset = $('#btn_reset');
+ if ($btnReset.length > 0) {
+ $btnReset.click();
}
},
@@ -1134,8 +797,9 @@ var DX_WATERFALL_UTILS = {
// Set the mode if available - determine the actual radio mode
if (spotData.mode) {
- // Use determineRadioMode to get the correct radio mode (same as clicking)
- var radioMode = DX_WATERFALL_UTILS.navigation.determineRadioMode(spotData);
+ // Use global determineRadioMode from radiohelpers.js
+ var frequencyHz = parseFloat(spotData.frequency) * 1000; // Convert kHz to Hz
+ var radioMode = determineRadioMode(spotData.mode, frequencyHz);
// Use skipTrigger=true to prevent change event race condition
setMode(radioMode, true);
}
@@ -1233,56 +897,6 @@ var DX_WATERFALL_UTILS = {
// Flag to block interference during navigation
navigating: false,
- /**
- * Determine the appropriate radio mode to set based on spot mode and frequency
- * @param {Object} spot - Spot object with mode and frequency
- * @returns {string} - The mode to set (CW, USB, LSB, RTTY, etc.)
- */
- determineRadioMode: function(spot) {
-
- if (!spot) {
- return 'USB'; // Default fallback
- }
-
- var spotMode = (spot.mode || '').toUpperCase();
- var frequency = parseFloat(spot.frequency); // Frequency in kHz
-
-
- // CW mode - always use CW
- if (DX_WATERFALL_CONSTANTS.MODE_LISTS.CW.indexOf(spotMode) !== -1) {
- return 'CW';
- }
-
- // Digital modes - use RTTY as the standard digital mode (use constants)
- var digiModes = DX_WATERFALL_CONSTANTS.MODE_LISTS.WSJT.concat(
- DX_WATERFALL_CONSTANTS.MODE_LISTS.PSK,
- DX_WATERFALL_CONSTANTS.MODE_LISTS.DIGITAL_MODES,
- DX_WATERFALL_CONSTANTS.MODE_LISTS.DIGITAL_VOICE,
- DX_WATERFALL_CONSTANTS.MODE_LISTS.DIGITAL_HF,
- DX_WATERFALL_CONSTANTS.MODE_LISTS.DIGITAL_OTHER
- );
-
- for (var i = 0; i < digiModes.length; i++) {
- if (spotMode.indexOf(digiModes[i]) !== -1) {
- return 'RTTY';
- }
- }
-
- // Phone modes - determine USB or LSB based on frequency (use constants)
- var isPhoneMode = DX_WATERFALL_CONSTANTS.MODE_LISTS.PHONE.indexOf(spotMode) !== -1;
-
- if (isPhoneMode || !spotMode) {
- // Use frequency-based determination for phone modes or unknown modes
- // Use the same logic as bandwidth drawing for consistency
- var ssbMode = DX_WATERFALL_UTILS.modes.determineSSBMode(frequency);
- return ssbMode;
- }
-
- // For any other unrecognized mode, default to USB/LSB based on frequency
- var defaultMode = DX_WATERFALL_UTILS.modes.determineSSBMode(frequency);
- return defaultMode;
- },
-
// Common navigation logic shared by all spot navigation functions
navigateToSpot: function(waterfallContext, targetSpot, targetIndex, shouldPrefill) {
// Default to false - only prefill if explicitly requested
@@ -1313,7 +927,8 @@ var DX_WATERFALL_UTILS = {
// CRITICAL: Set mode FIRST before calling setFrequency
// setFrequency reads the mode from $('#mode').val(), so the mode must be set first
- var radioMode = this.determineRadioMode(targetSpot);
+ var frequencyHz = parseFloat(targetSpot.frequency) * 1000; // Convert kHz to Hz
+ var radioMode = determineRadioMode(targetSpot.mode, frequencyHz);
// Set CAT debounce lock early to block incoming CAT updates during navigation
if (typeof setFrequency.catDebounceLock !== 'undefined') {
@@ -1541,7 +1156,7 @@ var dxWaterfall = {
// ========================================
// CONTINENT FILTERING
// ========================================
- continents: DX_WATERFALL_CONSTANTS.CONTINENTS,
+ continents: CONTINENTS, // Use global CONTINENTS constant from radiohelpers.js
continentChanging: false,
continentChangeTimer: null,
pendingContinent: null,
@@ -1571,7 +1186,7 @@ var dxWaterfall = {
pendingModeFilters: null,
modeFilterChangeTimer: null,
- ft8Frequencies: DX_WATERFALL_CONSTANTS.FT8_FREQUENCIES,
+ ft8Frequencies: FT8_FREQUENCIES, // Use global FT8_FREQUENCIES constant from radiohelpers.js
// Band plan management
bandPlans: null, // Cached band plans from database
@@ -1749,7 +1364,7 @@ var dxWaterfall = {
var newBand = $(this).val();
// Get a typical frequency for the selected band
- var bandFreq = self.getTypicalBandFrequency(newBand);
+ var bandFreq = getTypicalBandFrequency(newBand);
if (bandFreq > 0) {
// Update frequency field
@@ -1811,7 +1426,7 @@ var dxWaterfall = {
if (!window.catState.frequency && self.$freqCalculated.val()) {
var freqVal = parseFloat(self.$freqCalculated.val());
var unit = self.$qrgUnit.text() || 'kHz';
- var freqKhz = DX_WATERFALL_UTILS.frequency.convertToKhz(freqVal, unit);
+ var freqKhz = convertFrequency(freqVal, unit, 'kHz');
window.catState.frequency = freqKhz * 1000; // Convert to Hz
}
@@ -1914,8 +1529,8 @@ var dxWaterfall = {
}
// Convert both frequencies to kHz for comparison (normalize units)
- var currentKhz = DX_WATERFALL_UTILS.frequency.convertToKhz(currentInput, currentUnit);
- var lastKhz = DX_WATERFALL_UTILS.frequency.convertToKhz(this.lastValidCommittedFreq, this.lastValidCommittedUnit);
+ var currentKhz = convertFrequency(currentInput, currentUnit, 'kHz');
+ var lastKhz = convertFrequency(this.lastValidCommittedFreq, this.lastValidCommittedUnit, 'kHz');
// Compare frequencies with 1 Hz tolerance (0.001 kHz) to account for floating point errors
var tolerance = 0.001; // 1 Hz
@@ -1944,7 +1559,7 @@ var dxWaterfall = {
this.lastValidCommittedUnit = currentUnit;
// Store the committed frequency in kHz for comparison checks
- var currentFreqKhz = DX_WATERFALL_UTILS.frequency.convertToKhz(freqValue, currentUnit);
+ var currentFreqKhz = convertFrequency(freqValue, currentUnit, 'kHz');
this.committedFrequencyKHz = currentFreqKhz;
// In offline mode, populate catState with form values to act as "virtual CAT"
@@ -2044,7 +1659,7 @@ var dxWaterfall = {
this.lastQrgUnit = currentUnit;
// Convert to kHz using utility function
- this.cache.middleFreq = DX_WATERFALL_UTILS.frequency.convertToKhz(currentInput, currentUnit);
+ this.cache.middleFreq = convertFrequency(currentInput, currentUnit, 'kHz');
}
// Update split operation state and get display configuration
@@ -2173,33 +1788,48 @@ var dxWaterfall = {
// Note: refreshFrequencyCache() no longer needed here
// Waterfall reads frequency from window.catState (CAT data), not form fields
- if (this.canvas && this.ctx) {
- this.refresh();
- }
+ // Check connection type for optimized refresh strategy
+ var isWebSocket = typeof dxwaterfall_cat_state !== 'undefined' && dxwaterfall_cat_state === 'websocket';
- // Clear overlay after marker movement animation completes
- if (isCATAvailable()) {
- var self = this;
- setTimeout(function() {
- self.showingCompletionOverlay = false;
- if (self.canvas && self.ctx) {
- self.refresh();
- }
- }, 400);
+ if (isWebSocket) {
+ // WebSocket: Skip overlay entirely, single refresh is enough
+ // (overlay path is skipped in _performRefresh, so no blink)
+ this.showingCompletionOverlay = false;
+ if (this.canvas && this.ctx) {
+ this.refresh();
+ }
+ } else {
+ // Polling: Show overlay with multiple refreshes for smooth feedback
+ if (this.canvas && this.ctx) {
+ this.refresh();
+ }
- // Fallback timeout for safety
- setTimeout(function() {
- self.showingCompletionOverlay = false;
- if (self.canvas && self.ctx) {
- self.refresh();
- }
- }, 600);
- }
+ // Clear overlay after marker movement animation completes
+ if (isCATAvailable()) {
+ var self = this;
+ var timings = getCATTimings();
- // Final refresh to ensure visual consistency
- var newFreq = this.getCachedMiddleFreq();
- if (this.canvas && this.ctx) {
- this.refresh();
+ setTimeout(function() {
+ self.showingCompletionOverlay = false;
+ if (self.canvas && self.ctx) {
+ self.refresh();
+ }
+ }, timings.overlayDuration);
+
+ // Fallback timeout for safety
+ setTimeout(function() {
+ self.showingCompletionOverlay = false;
+ if (self.canvas && self.ctx) {
+ self.refresh();
+ }
+ }, timings.overlayFallback);
+ }
+
+ // Final refresh to ensure visual consistency
+ var newFreq = this.getCachedMiddleFreq();
+ if (this.canvas && this.ctx) {
+ this.refresh();
+ }
}
},
@@ -2236,7 +1866,7 @@ var dxWaterfall = {
var currentUnit = this.$qrgUnit.text() || 'kHz';
// Convert to kHz using utility function
- var currentFreqFromDOM = DX_WATERFALL_UTILS.frequency.convertToKhz(freqValue, currentUnit);
+ var currentFreqFromDOM = convertFrequency(freqValue, currentUnit, 'kHz');
// If cache is outdated, refresh it (but only if not during waterfall operations)
if (!this.cache.middleFreq || Math.abs(currentFreqFromDOM - this.cache.middleFreq) > 0.1) {
@@ -2312,7 +1942,7 @@ var dxWaterfall = {
* Save font size to cookie
*/
saveFontSizeToCookie: function() {
- DX_WATERFALL_UTILS.cookie.set(
+ setCookie(
DX_WATERFALL_CONSTANTS.COOKIE.NAME_FONT_SIZE,
this.labelSizeLevel.toString(),
DX_WATERFALL_CONSTANTS.COOKIE.EXPIRY_DAYS
@@ -2324,7 +1954,7 @@ var dxWaterfall = {
* @returns {number|null} Font size level (0-4) or null if not found
*/
loadFontSizeFromCookie: function() {
- var cookieValue = DX_WATERFALL_UTILS.cookie.get(DX_WATERFALL_CONSTANTS.COOKIE.NAME_FONT_SIZE);
+ var cookieValue = getCookie(DX_WATERFALL_CONSTANTS.COOKIE.NAME_FONT_SIZE);
if (cookieValue !== null) {
var level = parseInt(cookieValue, 10);
if (!isNaN(level) && level >= 0 && level <= 4) {
@@ -2338,7 +1968,7 @@ var dxWaterfall = {
* Save mode filters to cookie
*/
saveModeFiltersToCookie: function() {
- DX_WATERFALL_UTILS.cookie.set(
+ setCookie(
DX_WATERFALL_CONSTANTS.COOKIE.NAME_MODE_FILTERS,
JSON.stringify(this.modeFilters),
DX_WATERFALL_CONSTANTS.COOKIE.EXPIRY_DAYS
@@ -2350,7 +1980,7 @@ var dxWaterfall = {
* @returns {Object|null} Mode filters object or null if not found
*/
loadModeFiltersFromCookie: function() {
- var cookieValue = DX_WATERFALL_UTILS.cookie.get(DX_WATERFALL_CONSTANTS.COOKIE.NAME_MODE_FILTERS);
+ var cookieValue = getCookie(DX_WATERFALL_CONSTANTS.COOKIE.NAME_MODE_FILTERS);
if (cookieValue) {
try {
var filters = JSON.parse(cookieValue);
@@ -2671,6 +2301,7 @@ var dxWaterfall = {
}
// Cache miss - rebuild visible spots
+
var leftSpots = [];
var rightSpots = [];
var centerFrequency = middleFreq;
@@ -2769,32 +2400,6 @@ var dxWaterfall = {
return this.cachedBandwidthParams.params;
},
- // Optimized FT8 frequency checking using cached array
- isFT8Frequency: function(frequency) {
- return this.ft8Frequencies.some(function(freq) {
- return Math.abs(frequency - freq) < 1; // Within 1 kHz tolerance
- });
- },
-
- // Map continent to IARU region
- continentToRegion: function(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
- }
- },
-
// Load band plans from database
loadBandPlans: function() {
var self = this;
@@ -2812,7 +2417,7 @@ var dxWaterfall = {
}
// Determine region from current continent
- var region = this.continentToRegion(this.currentContinent);
+ var region = continentToRegion(this.currentContinent);
$.ajax({
url: baseUrl + 'index.php/band/get_user_bandedges?region=' + region,
@@ -2859,7 +2464,7 @@ var dxWaterfall = {
// Determine band from frequency (use center frequency)
var centerFreq = (freqFrom + freqTo) / 2;
- var band = this.getFrequencyBandFromHz(centerFreq);
+ var band = frequencyToBand(centerFreq); // Use global function from radiohelpers.js
if (band) {
// Store band ranges for limits
@@ -2895,38 +2500,12 @@ var dxWaterfall = {
return bandPlans;
},
- // Helper function to determine band from frequency in Hz
- getFrequencyBandFromHz: function(frequencyHz) {
- // Check if frequencyToBand function exists
- if (typeof frequencyToBand === 'function') {
- return frequencyToBand(frequencyHz);
- }
-
- // Fallback: simple band detection based on common amateur radio bands
- var freqMhz = frequencyHz / 1000000;
-
- if (freqMhz >= 1.8 && freqMhz < 2.0) return '160m';
- if (freqMhz >= 3.5 && freqMhz < 4.0) return '80m';
- if (freqMhz >= 7.0 && freqMhz < 7.3) return '40m';
- if (freqMhz >= 10.1 && freqMhz < 10.15) return '30m';
- if (freqMhz >= 14.0 && freqMhz < 14.35) return '20m';
- if (freqMhz >= 18.068 && freqMhz < 18.168) return '17m';
- if (freqMhz >= 21.0 && freqMhz < 21.45) return '15m';
- if (freqMhz >= 24.89 && freqMhz < 24.99) return '12m';
- if (freqMhz >= 28.0 && freqMhz < 29.7) return '10m';
- if (freqMhz >= 50.0 && freqMhz < 54.0) return '6m';
- if (freqMhz >= 144.0 && freqMhz < 148.0) return '2m';
- if (freqMhz >= 420.0 && freqMhz < 450.0) return '70cm';
-
- return null;
- },
-
// Get band limits for current band and region
getBandLimits: function() {
// Use the band we have spots for, not the form selector
// This prevents drawing wrong band limits when form is changed manually
var currentBand = this.currentSpotBand || this.getCurrentBand();
- var currentRegion = this.continentToRegion(this.currentContinent);
+ var currentRegion = continentToRegion(this.currentContinent);
var regionKey = 'region' + currentRegion;
// Check if we need to update cache
@@ -2969,20 +2548,6 @@ var dxWaterfall = {
return limits;
},
- // Get band name for a given frequency in kHz
- getFrequencyBand: function(frequencyKhz) {
- // Check if frequencyToBand function exists
- if (typeof frequencyToBand !== 'function') {
- return null;
- }
-
- // Convert kHz to Hz for frequencyToBand function
- var frequencyHz = frequencyKhz * 1000;
- var band = frequencyToBand(frequencyHz);
-
- return band && band !== '' ? band : null;
- },
-
// ========================================
// CANVAS DRAWING AND RENDERING FUNCTIONS
// ========================================
@@ -2992,7 +2557,7 @@ var dxWaterfall = {
// Use the band we have spots for, not the form selector
// This prevents drawing wrong band mode indicators when form is changed manually
var currentBand = this.currentSpotBand || this.getCurrentBand();
- var currentRegion = this.continentToRegion(this.currentContinent);
+ var currentRegion = continentToRegion(this.currentContinent);
var regionKey = 'region' + currentRegion;
// Check if we have band plans loaded
@@ -3013,7 +2578,7 @@ var dxWaterfall = {
// SAFETY CHECK: Verify frequency matches the band before drawing band edges
// This prevents drawing band edges for the wrong band during band changes
- var frequencyBand = this.getFrequencyBand(middleFreq);
+ var frequencyBand = frequencyToBandKhz(middleFreq);
if (frequencyBand !== currentBand) {
return; // Don't draw band edges if frequency doesn't match band
}
@@ -3232,7 +2797,7 @@ var dxWaterfall = {
var band = null;
if (currentFreqKhz > 0) {
- band = this.getFrequencyBand(currentFreqKhz);
+ band = frequencyToBandKhz(currentFreqKhz);
}
// If band is invalid or empty, use a default band for initial fetch
@@ -3487,37 +3052,6 @@ var dxWaterfall = {
return mode;
},
- // Get a typical frequency for a given band (in kHz)
- // Used when changing bands in offline mode to set a reasonable frequency
- getTypicalBandFrequency: function(band) {
- var 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;
- },
-
// Quick dimension update to prevent stretching - no redraw
updateDimensions: function() {
if (this.canvas) {
@@ -3866,7 +3400,7 @@ var dxWaterfall = {
var offsetKHz = bandwidthParams.offset;
// Only draw bandwidth indicator for phone and CW modes (not for digital modes)
- var modeCategory = DX_WATERFALL_UTILS.modes.getModeCategory(currentMode);
+ var modeCategory = getModeCategory(currentMode) || 'other';
if (modeCategory !== 'phone' && modeCategory !== 'cw') {
return; // No bandwidth indicator for digital modes
}
@@ -3941,8 +3475,9 @@ var dxWaterfall = {
},
/**
- * Get bandwidth parameters for a given mode and frequency
+ * Get bandwidth parameters for a given mode and frequency (WATERFALL VISUALIZATION SPECIFIC)
* Returns the signal bandwidth and frequency offset for proper signal visualization
+ * Uses getSignalBandwidth() from radiohelpers.js for bandwidth, adds offset for sideband drawing
*
* @param {string} mode - The transmission mode (e.g., 'LSB', 'USB', 'FT8', 'CW')
* @param {number} frequency - Frequency in kHz
@@ -3951,135 +3486,24 @@ var dxWaterfall = {
* - offset: Frequency offset from carrier (negative for LSB, positive for USB, 0 for centered)
*/
getBandwidthParams: function(mode, frequency) {
- var modeLC = mode.toLowerCase();
var freq = parseFloat(frequency) || 0;
- // CW mode
- if (DX_WATERFALL_UTILS.modes.isCw(mode)) {
- return { bandwidth: 0.5, offset: 0 }; // 0.5 kHz centered
- }
+ // Get bandwidth from global function (handles all modes consistently)
+ var bandwidth = getSignalBandwidth(mode);
- // WSJT-X modes
- if (modeLC === 'ft8' || modeLC === 'ft4') {
- return { bandwidth: 3.0, offset: 0 }; // 3.0 kHz centered
- }
- if (modeLC === 'jt65' || modeLC === 'jt65b' || modeLC === 'jt9' || modeLC === 'jt9-1' ||
- modeLC === 'jt6c' || modeLC === 'jt6m') {
- return { bandwidth: 2.0, offset: 0 }; // 2.0 kHz centered
- }
- if (modeLC === 'q65' || modeLC === 'qra64') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
- if (modeLC === 'fst4' || modeLC === 'fst4w') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
- if (modeLC === 'wspr') {
- return { bandwidth: 0.2, offset: 0 }; // 0.2 kHz centered (very narrow)
- }
- if (modeLC === 'msk144') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
- if (modeLC === 'iscat' || modeLC === 'iscat-a' || modeLC === 'iscat-b') {
- return { bandwidth: 2.0, offset: 0 }; // 2.0 kHz centered
- }
- if (modeLC === 'js8' || modeLC === 'jtms') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
-
- // PSK modes (all variants narrow)
- if (modeLC.indexOf('psk') !== -1 || modeLC.indexOf('qpsk') !== -1) {
- return { bandwidth: 0.5, offset: 0 }; // 0.5 kHz centered for all PSK
- }
-
- // RTTY and related
- if (modeLC === 'rtty' || modeLC === 'navtex' || modeLC === 'sitorb') {
- return { bandwidth: 0.5, offset: 0 }; // 0.5 kHz centered
- }
-
- // Hellschreiber modes
- if (modeLC.indexOf('hell') !== -1 || modeLC.indexOf('fsk') === 0) {
- return { bandwidth: 0.5, offset: 0 }; // 0.5 kHz centered
- }
-
- // THOR/THROB modes
- if (modeLC.indexOf('thor') !== -1 || modeLC.indexOf('throb') !== -1 || modeLC.indexOf('thrb') !== -1) {
- return { bandwidth: 1.0, offset: 0 }; // 1.0 kHz centered
- }
-
- // Domino modes
- if (modeLC.indexOf('dom') !== -1) {
- return { bandwidth: 1.0, offset: 0 }; // 1.0 kHz centered
- }
-
- // VARA modes (wider bandwidth)
- if (modeLC.indexOf('vara') !== -1) {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
-
- // SCAMP modes
- if (modeLC.indexOf('scamp') !== -1) {
- return { bandwidth: 1.0, offset: 0 }; // 1.0 kHz centered
- }
-
- // MFSK modes
- if (modeLC.indexOf('mfsk') !== -1) {
- return { bandwidth: 1.0, offset: 0 }; // 1.0 kHz centered
- }
-
- // FSK modes
- if (modeLC === 'fsk441') {
- return { bandwidth: 2.0, offset: 0 }; // 2.0 kHz centered
- }
-
- // Other digital modes
- if (modeLC === 'ros') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered
- }
- if (modeLC === 'pkt' || modeLC === 'packet') {
- return { bandwidth: 3.0, offset: 0 }; // 3.0 kHz centered
- }
- if (modeLC === 'sstv') {
- return { bandwidth: 3.0, offset: 0 }; // 3.0 kHz for SSTV
- }
-
- // Digital voice modes (wider)
- if (modeLC === 'dmr' || modeLC === 'dstar' || modeLC === 'c4fm' ||
- modeLC === 'freedv' || modeLC === 'm17') {
- return { bandwidth: 3.0, offset: 0 }; // 3.0 kHz centered
- }
-
- // Generic digital fallback
- if (modeLC === 'digi' || modeLC === 'dynamic') {
- return { bandwidth: 2.5, offset: 0 }; // 2.5 kHz centered for generic digital
- }
-
- // Phone modes with sideband behavior
- if (modeLC === 'lsb') {
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_KHZ, offset: -DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_OFFSET_KHZ };
- }
- if (modeLC === 'usb') {
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_KHZ, offset: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_OFFSET_KHZ };
- }
- if (modeLC === 'ssb' || modeLC === 'phone') {
- // For SSB/phone spots, determine LSB/USB based on frequency using utility
- var ssbMode = DX_WATERFALL_UTILS.modes.determineSSBMode(freq);
+ // Phone modes with sideband behavior need offset calculation
+ // Use isPhoneMode() to check if mode is phone/voice (more robust than string comparison)
+ if (isPhoneMode(mode)) {
+ var ssbMode = determineSSBMode(freq);
if (ssbMode === 'LSB') {
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_KHZ, offset: -DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_OFFSET_KHZ };
+ return { bandwidth: bandwidth, offset: -bandwidth / 2 };
} else { // USB
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_KHZ, offset: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_OFFSET_KHZ };
+ return { bandwidth: bandwidth, offset: bandwidth / 2 };
}
}
- // AM and FM (centered)
- if (modeLC === 'am') {
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.AM_KHZ, offset: 0 };
- }
- if (modeLC === 'fm') {
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.FM_KHZ, offset: 0 };
- }
-
- // Default fallback (centered SSB-width)
- return { bandwidth: DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.SSB_KHZ, offset: 0 };
+ // All other modes (CW, digital, etc.) are centered (offset = 0)
+ return { bandwidth: bandwidth, offset: 0 };
},
// Draw bandwidth indicators for DX spots
@@ -4109,7 +3533,7 @@ var dxWaterfall = {
}
// Get detailed submode information for consistent classification
- var submodeInfo = DX_WATERFALL_UTILS.modes.getDetailedSubmode(spot);
+ var submodeInfo = classifyMode(spot);
var classifiedMode = submodeInfo.category;
// Determine mode for bandwidth calculation using utility functions
@@ -4119,7 +3543,7 @@ var dxWaterfall = {
if (submodeInfo.submode) {
modeForBandwidth = submodeInfo.submode.toLowerCase();
} else {
- var utilityCategory = DX_WATERFALL_UTILS.modes.getModeCategory(spot.mode);
+ var utilityCategory = getModeCategory(spot.mode) || 'other';
if (utilityCategory === 'cw') {
modeForBandwidth = 'cw';
} else if (utilityCategory === 'phone' && modeForBandwidth !== 'lsb' && modeForBandwidth !== 'usb') {
@@ -4183,7 +3607,7 @@ var dxWaterfall = {
// For phone/ssb modes, determine actual sideband based on frequency
if (modeStr === 'phone' || modeStr === 'ssb') {
var freq = parseFloat(spotFreq);
- sidebandType = DX_WATERFALL_UTILS.modes.determineSSBMode(freq).toLowerCase();
+ sidebandType = determineSSBMode(freq).toLowerCase();
} else if (modeStr === 'lsb' || modeStr === 'usb') {
sidebandType = modeStr;
}
@@ -4749,44 +4173,40 @@ var dxWaterfall = {
// Check if we need to do initial fetch or band has changed via CAT
// Skip during CAT operations to prevent interference
if (!this.catTuning && !this.frequencyChanging) {
- // Force initial fetch if we haven't done one yet (even with invalid frequency/band)
- if (!this.initialFetchDone) {
- // If we're still waiting for CAT frequency, don't fetch yet
- if (!this.waitingForCATFrequency) {
- this.initialFetchDone = true; // Set flag BEFORE fetch to prevent duplicate calls
- this.fetchDxSpots(true, false); // Initial fetch, but not user-initiated (background)
- }
- } else {
- // Check if radio has changed to a different band (via CAT frequency updates)
- // Calculate band from current frequency, not from form selector
- var currentFreqKhz = this.getCachedMiddleFreq();
- var calculatedBand = null;
+ // Check if radio has changed to a different band (via CAT frequency updates)
+ // Calculate band from current frequency, not from form selector
+ var currentFreqKhz = this.getCachedMiddleFreq();
+ var calculatedBand = null;
- if (currentFreqKhz > 0) {
- calculatedBand = this.getFrequencyBand(currentFreqKhz);
- }
+ if (currentFreqKhz > 0) {
+ calculatedBand = frequencyToBandKhz(currentFreqKhz);
+ }
- // If we have a valid calculated band and it differs from what we have spots for, fetch new spots
- if (calculatedBand && calculatedBand !== '' && calculatedBand.toLowerCase() !== 'select' &&
- this.currentSpotBand && calculatedBand !== this.currentSpotBand) {
- // Radio frequency changed to different band - fetch spots for new band
- DX_WATERFALL_UTILS.log.debug('[DX Waterfall] Band changed via frequency: ' + this.currentSpotBand + ' → ' + calculatedBand);
+ // Check if we need to fetch spots for a different band
+ // This handles both: band changes AND initial load where currentSpotBand isn't set yet
+ if (calculatedBand && calculatedBand !== '' && calculatedBand.toLowerCase() !== 'select') {
+ // Case 1: We have no spots yet (currentSpotBand is null) - fetch for current band
+ // Case 2: Band has changed (calculatedBand !== currentSpotBand) - fetch for new band
+ if (!this.currentSpotBand || calculatedBand !== this.currentSpotBand) {
+ // Radio frequency is on a different band than what we have spots for
+ DX_WATERFALL_UTILS.log.debug('[DX Waterfall] Band mismatch - have spots for: ' + this.currentSpotBand + ', need: ' + calculatedBand);
- // IMMEDIATELY update currentSpotBand to prevent infinite loop
- // The refresh() runs 60fps, so we must update this before next cycle
- this.currentSpotBand = calculatedBand;
+ // IMMEDIATELY update currentSpotBand to prevent infinite loop
+ // The refresh() runs 60fps, so we must update this before next cycle
+ this.currentSpotBand = calculatedBand;
- // Mark that we're waiting for new band data
- this.waitingForData = true;
- this.dataReceived = false;
- this.operationStartTime = Date.now(); // Start timer for visual feedback
+ // Mark that we're waiting for new band data
+ this.waitingForData = true;
+ this.dataReceived = false;
+ this.operationStartTime = Date.now(); // Start timer for visual feedback
- // Fetch spots for new band (not user-initiated, but automatic via CAT)
- this.fetchDxSpots(true, false);
+ // Fetch spots for new band (not user-initiated, but automatic via CAT)
+ this.fetchDxSpots(true, false);
- // Invalidate band-related caches
- this.bandLimitsCache = null;
- this.cachedBandForEdges = calculatedBand;
+ // Invalidate band-related caches
+ this.bandLimitsCache = null;
+ this.cachedBandForEdges = calculatedBand;
+ }
}
}
// NOTE: Removed hasParametersChanged() check - waterfall no longer monitors form band/mode changes
@@ -4867,8 +4287,10 @@ var dxWaterfall = {
this.displayWaitingMessage();
this.updateZoomMenu(); // Update menu to show loading indicator
return; // Don't draw the normal display
- } // Check if CAT is tuning the radio with safety timeout
- if (this.catTuning) {
+ }
+
+ // Check if CAT is tuning the radio with safety timeout
+ if (this.catTuning) {
// Safety check: if CAT tuning has been true for more than fallback time, force clear it
// BUT: Don't clear if we're waiting for CAT confirmation (targetFrequencyHz is set)
if (!this.catTuningStartTime) {
@@ -4908,22 +4330,34 @@ var dxWaterfall = {
// Check if we're showing completion overlay (marker moved but hiding the animation)
if (this.showingCompletionOverlay) {
- // Draw normal waterfall content first (including moved marker)
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
- this.drawStaticNoise();
- this.drawWavelogLink();
- this.drawBandLimits();
- this.drawFrequencyRuler();
- this.drawCenterMarker();
- this.drawDxSpots();
- this.drawCenterCallsignLabel();
+ // For WebSocket connections with fast overlay timing, skip the overlay entirely
+ // to prevent visible black flash during canvas redraw
+ var isWebSocket = typeof dxwaterfall_cat_state !== 'undefined' && dxwaterfall_cat_state === 'websocket';
+
+ if (isWebSocket) {
+ // Just clear the flag and continue with normal drawing
+ // (marker has already moved, no need for overlay feedback)
+ this.showingCompletionOverlay = false;
+ // Fall through to normal drawing below
+ } else {
+ // For polling mode, show the overlay message as before
+ // Draw normal waterfall content first (including moved marker)
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+ this.drawStaticNoise();
+ this.drawWavelogLink();
+ this.drawBandLimits();
+ this.drawFrequencyRuler();
+ this.drawCenterMarker();
+ this.drawDxSpots();
+ this.drawCenterCallsignLabel();
- // Only show tuning message if CAT is actually available
- if (isCATAvailable()) {
- // Then draw overlay message on top
- this.displayChangingFrequencyMessage(lang_dxwaterfall_changing_frequency, 'MESSAGE_TEXT_WHITE');
+ // Only show tuning message if CAT is actually available
+ if (isCATAvailable()) {
+ // Then draw overlay message on top
+ this.displayChangingFrequencyMessage(lang_dxwaterfall_changing_frequency, 'MESSAGE_TEXT_WHITE');
+ }
+ return; // Don't continue with normal refresh logic
}
- return; // Don't continue with normal refresh logic
}
// Show zoom menu when data is available (only if empty or mode changed)
@@ -4980,7 +4414,6 @@ var dxWaterfall = {
this.ctx.moveTo(this.canvas.width, 0);
this.ctx.lineTo(this.canvas.width, this.canvas.height);
this.ctx.stroke();
- }
},
// Get the most relevant spot in our sideband
@@ -5008,7 +4441,7 @@ var dxWaterfall = {
// This allows spots within ±1 kHz to be detected regardless of sideband
detectionRange = 1.0; // ±1 kHz symmetric range for SSB
} else if (currentMode === 'cw') {
- detectionRange = DX_WATERFALL_CONSTANTS.SIGNAL_BANDWIDTHS.CW_DETECTION_KHZ;
+ detectionRange = SIGNAL_BANDWIDTHS.CW;
} else {
// Other modes (digital, etc.) - centered with half bandwidth
detectionRange = signalBandwidth * 0.5; // 50% of bandwidth for other modes
@@ -5166,7 +4599,7 @@ var dxWaterfall = {
// Active spot in bandwidth - show spot details
// Get detailed submode information using centralized function
- var submodeInfo = DX_WATERFALL_UTILS.modes.getDetailedSubmode(spotInfo);
+ var submodeInfo = classifyMode(spotInfo);
var modeLabel = submodeInfo.submode || spotInfo.mode || 'Unknown';
// Use detailed submode for mode field (e.g., "FT8" instead of "digi")
var modeForField = submodeInfo.submode || spotInfo.mode || '';
@@ -5647,7 +5080,7 @@ var dxWaterfall = {
// Filter spots for current band
var result = DX_WATERFALL_UTILS.spots.filterSpots(this, function(spot, spotFreq, context) {
// Validate that spot belongs to current band (prevent cross-band contamination)
- var spotBand = context.getFrequencyBand(spotFreq);
+ var spotBand = frequencyToBandKhz(spotFreq);
return spotBand === currentBand;
}, {
postProcess: function(spotObj, originalSpot) {
@@ -5895,14 +5328,15 @@ var dxWaterfall = {
// Check if a spot should be shown based on active mode filters
spotMatchesModeFilter: function(spot) {
// Use comprehensive mode classification utility directly
- var spotMode = DX_WATERFALL_UTILS.modes.classifyMode(spot).category;
+ var classification = classifyMode(spot);
+ var spotMode = classification.category;
// Use pending filters if they exist, otherwise use current filters
var filters = this.pendingModeFilters || this.modeFilters;
- // If mode is unknown/unclassified, treat as "other"
+ // If mode is unknown/unclassified, default to phone (treat as SSB)
if (!spotMode || (spotMode !== 'phone' && spotMode !== 'cw' && spotMode !== 'digi')) {
- return filters.other === true;
+ spotMode = 'phone';
}
// For digi mode spots: if digi filter is OFF, also hide spots on FT8 frequencies
@@ -5910,7 +5344,7 @@ var dxWaterfall = {
// But if digi filter is ON, show all digi spots including FT8 frequencies
if (spotMode === 'digi') {
var spotFreq = parseFloat(spot.frequency);
- var isOnFT8Freq = this.isFT8Frequency(spotFreq);
+ var isOnFT8Freq = isFT8Frequency(spotFreq, 'kHz');
// If digi filter is OFF and spot is on FT8 frequency, hide it
if (!filters.digi && isOnFT8Freq) {
@@ -5930,13 +5364,20 @@ var dxWaterfall = {
toggleModeFilter: function(modeType) {
var self = this;
+ // Prevent rapid double-clicks from causing issues
+ var now = Date.now();
+ if (this.lastFilterToggleTime && (now - this.lastFilterToggleTime) < 50) {
+ DX_WATERFALL_UTILS.log.debug('[Filter Toggle] Ignoring rapid double-click');
+ return;
+ }
+ this.lastFilterToggleTime = now;
+
// Create pending filters if they don't exist (clone current filters)
if (!this.pendingModeFilters) {
this.pendingModeFilters = {
phone: this.modeFilters.phone,
cw: this.modeFilters.cw,
- digi: this.modeFilters.digi,
- other: this.modeFilters.other
+ digi: this.modeFilters.digi
};
}
@@ -5947,14 +5388,19 @@ var dxWaterfall = {
this.modeFilters.phone = this.pendingModeFilters.phone;
this.modeFilters.cw = this.pendingModeFilters.cw;
this.modeFilters.digi = this.pendingModeFilters.digi;
- this.modeFilters.other = this.pendingModeFilters.other;
// Invalidate visible spots cache immediately for instant update
this.cache.visibleSpots = null;
this.cache.visibleSpotsParams = null;
- // Update menu immediately to show the new state
- this.updateZoomMenu();
+ // Trigger immediate refresh to show filter changes
+ // This ensures the display updates instantly without waiting for the next interval
+ if (this.canvas && this.ctx) {
+ this.refresh();
+ }
+
+ // Don't update menu here - it will be updated by the timeout handler
+ // Updating here causes the button to be recreated which can trigger duplicate events
// Clear existing timer if there is one
if (this.modeFilterChangeTimer) {
@@ -6187,7 +5633,7 @@ function setMode(mode, skipTrigger) {
// For generic PHONE/SSB, try to determine LSB/USB based on frequency
else if (modeUpper === 'PHONE' || modeUpper === 'SSB') {
var currentFreq = dxWaterfall.getCachedMiddleFreq(); // Get frequency in kHz
- var ssbMode = DX_WATERFALL_UTILS.modes.determineSSBMode(currentFreq);
+ var ssbMode = determineSSBMode(currentFreq);
if (ssbMode === 'LSB') {
// Check if LSB exists in options
if (modeSelect.find('option[value="LSB"]').length > 0) {
@@ -6339,7 +5785,7 @@ function setFrequency(frequencyInKHz, fromWaterfall) {
}
// Any other mode - default to frequency-based USB/LSB
else {
- var ssbMode = DX_WATERFALL_UTILS.modes.determineSSBMode(frequencyInKHz);
+ var ssbMode = determineSSBMode(frequencyInKHz);
catMode = ssbMode.toLowerCase();
}
}
@@ -6813,7 +6259,8 @@ function setFrequency(frequencyInKHz, fromWaterfall) {
// CRITICAL: Set mode FIRST (without triggering change event), THEN set frequency
// This ensures setFrequency() reads the correct mode from the dropdown
- var radioMode = DX_WATERFALL_UTILS.navigation.determineRadioMode(clickedSpot);
+ var frequencyHz = parseFloat(clickedSpot.frequency) * 1000; // Convert kHz to Hz
+ var radioMode = determineRadioMode(clickedSpot.mode, frequencyHz);
setMode(radioMode, true); // skipTrigger = true to prevent change event
// Now set frequency - it will read the correct mode from the dropdown
@@ -6907,7 +6354,7 @@ function setFrequency(frequencyInKHz, fromWaterfall) {
}
// Use Cmd on Mac, Ctrl on Windows/Linux
- var modKey = DX_WATERFALL_UTILS.platform.isModifierKey(e);
+ var modKey = PlatformDetection.isModifierKey(e);
// Ctrl/Cmd+Left: Previous spot
if (modKey && !e.shiftKey && e.key === 'ArrowLeft') {
diff --git a/assets/js/radiohelpers.js b/assets/js/radiohelpers.js
index 4854554fd..00b01706c 100644
--- a/assets/js/radiohelpers.js
+++ b/assets/js/radiohelpers.js
@@ -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}
+ */
+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': ['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;
-
}
diff --git a/assets/js/sections/common.js b/assets/js/sections/common.js
index 8b509d440..ec30538d6 100644
--- a/assets/js/sections/common.js
+++ b/assets/js/sections/common.js
@@ -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" +