/**
* Intercept - Core Utility Functions
* Pure utility functions with no DOM dependencies
*/
// ============== HTML ESCAPING ==============
/**
* Escape HTML to prevent XSS
* @param {string} text - Text to escape
* @returns {string} Escaped HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Escape text for use in HTML attributes (especially onclick handlers)
* @param {string} text - Text to escape
* @returns {string} Escaped attribute value
*/
function escapeAttr(text) {
if (text === null || text === undefined) return '';
var s = String(text);
s = s.replace(/&/g, '&');
s = s.replace(/'/g, ''');
s = s.replace(/"/g, '"');
s = s.replace(//g, '>');
return s;
}
// ============== VALIDATION ==============
/**
* Validate MAC address format (XX:XX:XX:XX:XX:XX)
* @param {string} mac - MAC address to validate
* @returns {boolean} True if valid
*/
function isValidMac(mac) {
return /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(mac);
}
/**
* Validate WiFi channel (1-200 covers all bands)
* @param {string|number} ch - Channel number
* @returns {boolean} True if valid
*/
function isValidChannel(ch) {
const num = parseInt(ch, 10);
return !isNaN(num) && num >= 1 && num <= 200;
}
// ============== TIME FORMATTING ==============
/**
* Global time preferences — timezone and 12h/24h format.
* Stored in localStorage, used by all modes.
*/
const InterceptTime = (function() {
const TZ_MAP = {
'UTC': 'UTC',
'local': undefined,
'US/Eastern': 'America/New_York',
'US/Central': 'America/Chicago',
'US/Mountain': 'America/Denver',
'US/Pacific': 'America/Los_Angeles',
};
const TZ_LABELS = {
'UTC': 'UTC',
'local': '',
'US/Eastern': 'ET',
'US/Central': 'CT',
'US/Mountain': 'MT',
'US/Pacific': 'PT',
};
let _timezone = localStorage.getItem('interceptTimezone') || 'US/Eastern';
let _hour12 = (localStorage.getItem('interceptHour12') || 'true') === 'true';
const _listeners = [];
function getTimezone() { return _timezone; }
function getHour12() { return _hour12; }
function getIANA() { return TZ_MAP[_timezone]; }
function getLabel() { return TZ_LABELS[_timezone] || ''; }
function setTimezone(tz) {
if (!TZ_MAP.hasOwnProperty(tz)) return;
_timezone = tz;
localStorage.setItem('interceptTimezone', tz);
// Migrate weather-sat specific key
localStorage.setItem('wxsatTimezone', tz);
_notify();
}
function setHour12(val) {
_hour12 = !!val;
localStorage.setItem('interceptHour12', _hour12 ? 'true' : 'false');
_notify();
}
function onChange(fn) { _listeners.push(fn); }
function _notify() { _listeners.forEach(fn => { try { fn(); } catch(e) { console.error(e); } }); }
/**
* Format a Date or ISO string for the global timezone.
* @param {Date|string} input - Date object or ISO string
* @param {object} [extraOpts] - Additional Intl.DateTimeFormat options
* @returns {string}
*/
function format(input, extraOpts) {
if (!input) return '--';
try {
const date = typeof input === 'string' ? new Date(input) : input;
if (isNaN(date.getTime())) return typeof input === 'string' ? input : '--';
const opts = { hour12: _hour12, ...extraOpts };
const iana = getIANA();
if (iana) opts.timeZone = iana;
return date.toLocaleString(undefined, opts);
} catch { return typeof input === 'string' ? input : '--'; }
}
/** HH:MM (or h:MM AM/PM) */
function shortTime(input) {
return format(input, { hour: '2-digit', minute: '2-digit' });
}
/** HH:MM:SS */
function fullTime(input) {
return format(input, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
/** Mon 25, 14:30 (or 2:30 PM) */
function dateTime(input) {
return format(input, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
/** Mon 25, 2026 */
function dateOnly(input) {
const iana = getIANA();
const opts = { year: 'numeric', month: 'short', day: 'numeric' };
if (iana) opts.timeZone = iana;
try {
const date = typeof input === 'string' ? new Date(input) : input;
return date.toLocaleDateString(undefined, opts);
} catch { return '--'; }
}
/** Short label like " ET" or " UTC" for appending to times */
function tzSuffix() {
const l = getLabel();
return l ? ' ' + l : '';
}
return {
getTimezone, getHour12, getIANA, getLabel,
setTimezone, setHour12, onChange,
format, shortTime, fullTime, dateTime, dateOnly, tzSuffix,
TZ_MAP, TZ_LABELS,
};
})();
/**
* Get relative time string from timestamp
* @param {string} timestamp - Time string in HH:MM:SS format
* @returns {string} Relative time like "5s ago", "2m ago"
*/
function getRelativeTime(timestamp) {
if (!timestamp) return '';
const now = new Date();
const parts = timestamp.split(':');
const msgTime = new Date();
msgTime.setHours(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]));
const diff = Math.floor((now - msgTime) / 1000);
if (diff < 5) return 'just now';
if (diff < 60) return diff + 's ago';
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
return timestamp;
}
/**
* Format UTC time string
* @param {Date} date - Date object
* @returns {string} UTC time in HH:MM:SS format
*/
function formatUtcTime(date) {
return date.toISOString().substring(11, 19);
}
// ============== DISTANCE CALCULATIONS ==============
/**
* Calculate distance between two points in nautical miles
* Uses Haversine formula
* @param {number} lat1 - Latitude of first point
* @param {number} lon1 - Longitude of first point
* @param {number} lat2 - Latitude of second point
* @param {number} lon2 - Longitude of second point
* @returns {number} Distance in nautical miles
*/
function calculateDistanceNm(lat1, lon1, lat2, lon2) {
const R = 3440.065; // Earth radius in nautical miles
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
/**
* Calculate distance between two points in kilometers
* @param {number} lat1 - Latitude of first point
* @param {number} lon1 - Longitude of first point
* @param {number} lat2 - Latitude of second point
* @param {number} lon2 - Longitude of second point
* @returns {number} Distance in kilometers
*/
function calculateDistanceKm(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth radius in kilometers
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
// ============== FILE OPERATIONS ==============
/**
* Download content as a file
* @param {string} content - File content
* @param {string} filename - Name for the downloaded file
* @param {string} type - MIME type
*/
function downloadFile(content, filename, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// ============== FREQUENCY FORMATTING ==============
/**
* Format frequency value with proper units
* @param {number} freqMhz - Frequency in MHz
* @param {number} decimals - Number of decimal places (default 3)
* @returns {string} Formatted frequency string
*/
function formatFrequency(freqMhz, decimals = 3) {
return freqMhz.toFixed(decimals) + ' MHz';
}
/**
* Parse frequency string to MHz
* @param {string} freqStr - Frequency string (e.g., "118.0", "118.0 MHz")
* @returns {number} Frequency in MHz
*/
function parseFrequency(freqStr) {
return parseFloat(freqStr.replace(/[^\d.-]/g, ''));
}
// ============== LOCAL STORAGE HELPERS ==============
/**
* Get item from localStorage with JSON parsing
* @param {string} key - Storage key
* @param {*} defaultValue - Default value if key doesn't exist
* @returns {*} Parsed value or default
*/
function getStorageItem(key, defaultValue = null) {
const saved = localStorage.getItem(key);
if (saved === null) return defaultValue;
try {
return JSON.parse(saved);
} catch (e) {
return saved;
}
}
/**
* Set item in localStorage with JSON stringification
* @param {string} key - Storage key
* @param {*} value - Value to store
*/
function setStorageItem(key, value) {
if (typeof value === 'object') {
localStorage.setItem(key, JSON.stringify(value));
} else {
localStorage.setItem(key, value);
}
}
// ============== ARRAY/OBJECT UTILITIES ==============
/**
* Debounce function execution
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in milliseconds
* @returns {Function} Debounced function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle function execution
* @param {Function} func - Function to throttle
* @param {number} limit - Time limit in milliseconds
* @returns {Function} Throttled function
*/
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// ============== NUMBER FORMATTING ==============
/**
* Format large numbers with K/M suffixes
* @param {number} num - Number to format
* @returns {string} Formatted string
*/
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
/**
* Clamp a number between min and max
* @param {number} num - Number to clamp
* @param {number} min - Minimum value
* @param {number} max - Maximum value
* @returns {number} Clamped value
*/
function clamp(num, min, max) {
return Math.min(Math.max(num, min), max);
}
/**
* Map a value from one range to another
* @param {number} value - Value to map
* @param {number} inMin - Input range minimum
* @param {number} inMax - Input range maximum
* @param {number} outMin - Output range minimum
* @param {number} outMax - Output range maximum
* @returns {number} Mapped value
*/
function mapRange(value, inMin, inMax, outMin, outMax) {
return (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}
// ============== ICON SYSTEM ==============
// Minimal SVG icons. Each returns HTML string.
// Designed for screenshot legibility - standard symbols only.
const Icons = {
// ===== Signal Type Icons =====
wifi: function(className) {
return ``;
},
bluetooth: function(className) {
return ``;
},
cellular: function(className) {
return ``;
},
radio: function(className) {
return ``;
},
// ===== Mode Icons =====
pager: function(className) {
return ``;
},
sensor: function(className) {
return ``;
},
aircraft: function(className) {
return ``;
},
satellite: function(className) {
return ``;
},
location: function(className) {
return ``;
},
search: function(className) {
return ``;
},
meter: function(className) {
return ``;
},
scanner: function(className) {
return ``;
},
// ===== Status Icons =====
warning: function(className) {
return ``;
},
check: function(className) {
return ``;
},
x: function(className) {
return ``;
},
recording: function(className) {
return ``;
},
anomaly: function(className) {
return ``;
},
flag: function(className) {
return ``;
},
newBadge: function(className) {
return ``;
},
offline: function(className) {
return ``;
},
// ===== Device Type Icons =====
user: function(className) {
return ``;
},
drone: function(className) {
return ``;
},
military: function(className) {
return ``;
},
handshake: function(className) {
return ``;
},
// ===== Action Icons =====
refresh: function(className) {
return ``;
},
download: function(className) {
return ``;
},
export: function(className) {
return ``;
},
copy: function(className) {
return ``;
},
link: function(className) {
return ``;
},
chart: function(className) {
return ``;
},
star: function(className) {
return ``;
},
target: function(className) {
return ``;
},
settings: function(className) {
return ``;
},
// ===== Playback Controls =====
play: function(className) {
return ``;
},
pause: function(className) {
return ``;
},
stop: function(className) {
return ``;
},
headphones: function(className) {
return ``;
},
volumeOn: function(className) {
return ``;
},
volumeOff: function(className) {
return ``;
},
// ===== UI Icons =====
sun: function(className) {
return ``;
},
moon: function(className) {
return ``;
},
arrowDown: function(className) {
return ``;
},
chevronDown: function(className) {
return ``;
},
chevronRight: function(className) {
return ``;
},
chevronLeft: function(className) {
return ``;
},
mail: function(className) {
return ``;
},
loader: function(className) {
return ``;
},
bell: function(className) {
return ``;
},
// ===== Helper function =====
forSignalType: function(type, className) {
const t = (type || '').toLowerCase();
if (t.includes('wifi') || t.includes('802.11')) return this.wifi(className);
if (t.includes('bluetooth') || t.includes('bt') || t.includes('ble')) return this.bluetooth(className);
if (t.includes('cellular') || t.includes('lte') || t.includes('gsm') || t.includes('5g')) return this.cellular(className);
return this.radio(className);
}
};