').html(callsignHtml);
+ let qrzLink = tempDiv.find('a');
+
+ if (qrzLink.length) {
+ qrzLink[0].click();
+ return;
+ }
+ }
+
+ // Default row click: prepare QSO logging with callsign, frequency, mode
+ let rowData = table.row(this).data();
+ if (!rowData) return;
+
+ let callsignHtml = rowData[3];
+ let tempDiv = $('
').html(callsignHtml);
+ let call = tempDiv.find('a').text().trim();
+ if (!call) return;
+
+ let qrg = parseFloat(rowData[1]) * 1000;
+ let mode = rowData[2];
+
+ prepareLogging(call, qrg, mode);
+ });
+
return table;
}
- function fill_list(band, de, maxAgeMinutes, cwn, mode) {
- // var table = $('.spottable').DataTable();
+ // ========================================
+ // FILTER STATE TRACKING
+ // ========================================
+
+ // Track what backend parameters were used for last data fetch
+ // NOTE: Changed architecture - only de continent affects backend now
+ // Band and Mode are now client-side filters only
+ var loadedBackendFilters = {
+ continent: 'Any'
+ };
+
+ // Initialize backend filter state from form values
+ function initializeBackendFilters() {
+ const decontSelect = $('#decontSelect').val();
+ loadedBackendFilters.continent = (decontSelect && decontSelect.length > 0) ? decontSelect[0] : 'Any';
+ }
+
+ // Track all current filter selections (both client and server-side)
+ var currentFilters = {
+ band: ['All'],
+ deContinent: ['Any'],
+ spottedContinent: ['Any'],
+ cwn: ['All'],
+ mode: ['All'],
+ additionalFlags: ['All'],
+ requiredFlags: []
+ };
+
+ // ========================================
+ // DATA CACHING & FETCH STATE
+ // ========================================
+
+ var cachedSpotData = null; // Raw spot data from last backend fetch
+ var isFetchInProgress = false; // Prevent multiple simultaneous fetches
+ var currentAjaxRequest = null; // Track active AJAX request for cancellation
+ var lastFetchParams = { // Track last successful fetch parameters
+ continent: 'Any',
+ maxAge: 60,
+ timestamp: null
+ };
+
+ // Auto-refresh timer state
+ var refreshCountdown = 60;
+ var refreshTimerInterval = null;
+
+ // ========================================
+ // STATUS BAR & UI UPDATES
+ // ========================================
+
+ // Update status bar with spot counts, filter info, and fetch status
+ function updateStatusBar(totalSpots, displayedSpots, serverFilters, clientFilters, isFetching, isInitialLoad) {
+ if (isFetching) {
+ let allFilters = [];
+ if (serverFilters && serverFilters.length > 0) {
+ allFilters = allFilters.concat(serverFilters.map(f => 'de ' + f));
+ }
+ if (clientFilters && clientFilters.length > 0) {
+ allFilters = allFilters.concat(clientFilters);
+ }
+
+ let loadingMessage = 'Loading data from DX Cluster';
+ if (allFilters.length > 0) {
+ loadingMessage += ' (' + allFilters.join(', ') + ')';
+ }
+ loadingMessage += '...';
+
+ $('#statusMessage').text(loadingMessage);
+ $('#refreshIcon').removeClass('fa-hourglass-half').addClass('fa-spinner fa-spin');
+ $('#refreshTimer').text('');
+ return;
+ }
+
+ if (lastFetchParams.timestamp === null) {
+ $('#statusMessage').text('');
+ $('#refreshTimer').text('');
+ return;
+ }
+
+ let now = new Date();
+ let timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
+ let statusMessage = totalSpots + ' spots fetched @ ' + timeStr;
+ let allFilters = [];
+
+ if (serverFilters && serverFilters.length > 0) {
+ allFilters = allFilters.concat(serverFilters.map(f => 'de ' + f));
+ }
+
+ if (clientFilters && clientFilters.length > 0) {
+ allFilters = allFilters.concat(clientFilters);
+ }
+
var table = get_dtable();
-
- // Handle multi-select values
- let bands = Array.isArray(band) ? band : [band];
- let continents = Array.isArray(de) ? de : [de];
- let cwnStatuses = Array.isArray(cwn) ? cwn : [cwn];
- let modes = Array.isArray(mode) ? mode : [mode];
-
- // If 'All' is selected or nothing selected, treat as all
- if (bands.includes('All') || bands.length === 0) {
- bands = ['All'];
- }
- if (continents.includes('Any') || continents.length === 0) {
- continents = ['Any'];
- }
- if (cwnStatuses.includes('All') || cwnStatuses.length === 0) {
- cwnStatuses = ['All'];
- }
- if (modes.includes('All') || modes.length === 0) {
- modes = ['All'];
+ var searchValue = table.search();
+ if (searchValue) {
+ allFilters.push('search: "' + searchValue + '"');
}
- // For now, use first band for API call (we'll need to make multiple calls or update API for multi-band)
- let bandForAPI = bands.includes('All') ? 'All' : bands[0];
- let continentForAPI = continents.includes('Any') ? 'Any' : continents[0];
- let modeForAPI = modes.includes('All') ? 'All' : modes[0];
+ if (allFilters.length > 0) {
+ statusMessage += ', showing ' + displayedSpots + ' (active filters: ' + allFilters.join(', ') + ')';
+ } else if (displayedSpots < totalSpots) {
+ statusMessage += ', showing ' + displayedSpots;
+ } else if (totalSpots > 0) {
+ statusMessage += ', showing all';
+ }
- if ((bandForAPI != '') && (bandForAPI !== undefined)) {
- let dxurl = dxcluster_provider + "/spots/" + bandForAPI + "/" +maxAgeMinutes + "/" + continentForAPI + "/" + modeForAPI;
- $.ajax({
- url: dxurl,
- cache: false,
- dataType: "json"
- }).done(function(dxspots) {
- table.page.len(50);
- let oldtable=table.data();
- table.clear();
- let spots2render=0;
- if (dxspots.length>0) {
- dxspots.sort(SortByQrg);
- dxspots.forEach((single) => {
- // Apply multi-select filtering
- let passesCwnFilter = cwnStatuses.includes('All');
- if (!passesCwnFilter) {
- if (cwnStatuses.includes('notwkd') && !single.worked_dxcc) passesCwnFilter = true;
- if (cwnStatuses.includes('wkd') && single.worked_dxcc) passesCwnFilter = true;
- if (cwnStatuses.includes('cnf') && single.cnfmd_dxcc) passesCwnFilter = true;
- if (cwnStatuses.includes('ucnf') && single.worked_dxcc && !single.cnfmd_dxcc) passesCwnFilter = true;
- }
- if (!passesCwnFilter) { return; }
+ let tooltipLines = ['Last fetched for:'];
+ tooltipLines.push('Band: ' + lastFetchParams.band);
+ tooltipLines.push('Continent: ' + lastFetchParams.continent);
+ tooltipLines.push('Mode: ' + lastFetchParams.mode);
+ tooltipLines.push('Max Age: ' + lastFetchParams.maxAge + ' min');
+ if (lastFetchParams.timestamp) {
+ let fetchTime = new Date(lastFetchParams.timestamp);
+ let fetchTimeStr = fetchTime.getHours().toString().padStart(2, '0') + ':' +
+ fetchTime.getMinutes().toString().padStart(2, '0') + ':' +
+ fetchTime.getSeconds().toString().padStart(2, '0');
+ tooltipLines.push('Fetched at: ' + fetchTimeStr);
+ }
- spots2render++;
- var data=[];
- if (single.cnfmd_dxcc) {
- dxcc_wked_info="text-success";
- } else if (single.worked_dxcc) {
- dxcc_wked_info="text-warning";
- } else {
- dxcc_wked_info="text-danger";
- }
- if (single.cnfmd_call) {
- wked_info="text-success";
- } else if (single.worked_call) {
- wked_info="text-warning";
- } else {
- wked_info="";
- }
- lotw_badge='';
- lclass='';
- if (single.dxcc_spotted.lotw_user) {
- $('#lotw_info').text("LoTW");
- if (single.dxcc_spotted.lotw_user > 365) {
- lclass='lotw_info_red';
- } else if (single.dxcc_spotted.lotw_user > 30) {
- lclass='lotw_info_orange';
- } else if (single.dxcc_spotted.lotw_user > 7) {
- lclass='lotw_info_yellow';
- }
- lotw_badge='
L';
- }
+ $('#statusMessage').text(statusMessage).attr('title', tooltipLines.join('\n'));
- data[0]=[];
- data[0].push(single.when_pretty);
- data[0].push(single.frequency*1);
- wked_info=((wked_info != '' ?'
' : '')+''+single.spotted+''+(wked_info != '' ? '' : ''));
- spotted=wked_info+lotw_badge;
- data[0].push(spotted);
- if (single.dxcc_spotted.flag) {
- dxcc_wked_info=((dxcc_wked_info != '' ?'
' : '')+single.dxcc_spotted.flag+' '+single.dxcc_spotted.entity+(dxcc_wked_info != '' ? '' : ''));
- } else {
- dxcc_wked_info=((dxcc_wked_info != '' ?'
' : '')+single.dxcc_spotted.entity+(dxcc_wked_info != '' ? '' : ''));
- }
- data[0].push('
'+dxcc_wked_info+'');
-
- if (single.cnfmd_continent) {
- continent_wked_info="text-success";
- } else if (single.worked_continent) {
- continent_wked_info="text-warning";
- } else {
- continent_wked_info="text-danger";
- }
- continent_wked_info = ((continent_wked_info != '' ?'
' : '')+single.dxcc_spotted.cont+(continent_wked_info != '' ? '' : ''));
-
- data[0].push(continent_wked_info);
- data[0].push(single.spotter);
- data[0].push(single.message || '');
- if ((single.worked_call) && ((single.last_wked || '') != '')) {
- data[0].push(single.last_wked.LAST_QSO+' in '+single.last_wked.LAST_MODE);
- } else {
- data[0].push('');
- }
- data[0].push(single.mode || '');
- if (oldtable.length > 0) {
- let update=false;
- oldtable.each( function (srow) {
- if (JSON.stringify(srow) === JSON.stringify(data[0])) {
- update=true;
- }
- });
- if (!update) { // Sth. Fresh? So highlight
- table.rows.add(data).draw().nodes().to$().addClass("fresh");
- } else {
- table.rows.add(data).draw();
- }
- } else {
- table.rows.add(data).draw();
- }
- });
- setTimeout(function(){ // Remove Highlights
- $(".fresh").removeClass("fresh");
- },10000);
- } else {
- table.clear();
- table.draw();
- }
- if (spots2render == 0) {
- table.clear();
- table.draw();
- }
- });
+ if (isFetching) {
+ $('#refreshIcon').removeClass('fa-hourglass-half').addClass('fa-spinner fa-spin');
+ $('#refreshTimer').text('Fetching...');
} else {
- table.clear();
- table.draw();
+ $('#refreshIcon').removeClass('fa-spinner fa-spin').addClass('fa-hourglass-half');
+ $('#refreshTimer').text('Next update in ' + refreshCountdown + 's');
}
}
+ function getDisplayedSpotCount() {
+ var table = get_dtable();
+ return table.rows({search: 'applied'}).count();
+ }
+
+ // Start/restart the auto-refresh countdown timer (60 seconds)
+ function startRefreshTimer() {
+ if (refreshTimerInterval) {
+ clearInterval(refreshTimerInterval);
+ }
+
+ refreshCountdown = 60;
+
+ refreshTimerInterval = setInterval(function() {
+ refreshCountdown--;
+ if (refreshCountdown <= 0) {
+ console.log('Timer countdown: reloading spot data with current filters');
+ let table = get_dtable();
+ table.clear();
+ fill_list(currentFilters.deContinent, dxcluster_maxage);
+ refreshCountdown = 60;
+ } else {
+ if (!isFetchInProgress && lastFetchParams.timestamp !== null) {
+ $('#refreshIcon').removeClass('fa-spinner fa-spin').addClass('fa-hourglass-half');
+ $('#refreshTimer').text('Next update in ' + refreshCountdown + 's');
+ }
+ }
+ }, 1000);
+ }
+
+ // Build array of server-side filter labels for display
+ function getServerFilterText() {
+ let filters = [];
+ // Only de continent is a server filter now
+ if (loadedBackendFilters.continent !== 'Any') {
+ filters.push(loadedBackendFilters.continent);
+ }
+ return filters;
+ }
+
+ // Build array of client-side filter labels for display
+ function getClientFilterText() {
+ let filters = [];
+
+ if (!currentFilters.cwn.includes('All')) {
+ let cwnLabels = currentFilters.cwn.map(function(status) {
+ switch(status) {
+ case 'notwkd': return 'Not worked';
+ case 'wkd': return 'Worked';
+ case 'cnf': return 'Confirmed';
+ case 'ucnf': return 'Worked, not Confirmed';
+ default: return status;
+ }
+ });
+ filters.push('DXCC: ' + cwnLabels.join('/'));
+ }
+
+ // Band is now a client filter
+ if (!currentFilters.band.includes('All')) {
+ filters.push('Band: ' + currentFilters.band.join('/'));
+ }
+
+ if (!currentFilters.spottedContinent.includes('Any')) {
+ filters.push('spotted: ' + currentFilters.spottedContinent.join('/'));
+ }
+
+ // Mode is now a client filter
+ if (!currentFilters.mode.includes('All')) {
+ let modeLabels = currentFilters.mode.map(function(m) {
+ return m.charAt(0).toUpperCase() + m.slice(1);
+ });
+ filters.push('Mode: ' + modeLabels.join('/'));
+ }
+
+ if (!currentFilters.additionalFlags.includes('All')) {
+ filters.push(currentFilters.additionalFlags.join('/'));
+ }
+
+ // Required flags - special handling (must have ALL selected flags)
+ if (currentFilters.requiredFlags && currentFilters.requiredFlags.length > 0) {
+ let requiredLabels = currentFilters.requiredFlags.map(function(flag) {
+ if (flag === 'lotw') return 'LoTW User';
+ if (flag === 'notworked') return 'Not worked before';
+ return flag;
+ });
+ filters.push('Required: ' + requiredLabels.join(' + '));
+ }
+
+ return filters;
+ }
+
+ // ========================================
+ // CLIENT-SIDE FILTERING & RENDERING
+ // ========================================
+
+ // Render spots from cached data, applying client-side filters
+ // Client filters: spottedContinent, cwn status, additionalFlags
+ function renderFilteredSpots() {
+ var table = get_dtable();
+
+ if (!cachedSpotData || cachedSpotData.length === 0) {
+ table.clear();
+ table.settings()[0].oLanguage.sEmptyTable = "No data available";
+ table.draw();
+ return;
+ }
+
+ let bands = currentFilters.band;
+ let spottedContinents = currentFilters.spottedContinent;
+ let cwnStatuses = currentFilters.cwn;
+ let modes = currentFilters.mode;
+ let flags = currentFilters.additionalFlags;
+ let requiredFlags = currentFilters.requiredFlags || [];
+
+ table.clear();
+ let oldtable = table.data();
+ let spots2render = 0;
+
+ cachedSpotData.forEach((single) => {
+ // Extract time from spot data - use 'when' field
+ let timeOnly = single.when;
+
+ // Apply required flags FIRST (must have ALL selected required flags)
+ if (requiredFlags.length > 0) {
+ for (let reqFlag of requiredFlags) {
+ if (reqFlag === 'lotw') {
+ if (!single.dxcc_spotted || !single.dxcc_spotted.lotw_user) return;
+ }
+ if (reqFlag === 'notworked') {
+ if (single.worked_call) return; // Reject if already worked
+ }
+ }
+ } // Apply CWN (Confirmed/Worked/New) filter
+ let passesCwnFilter = cwnStatuses.includes('All');
+ if (!passesCwnFilter) {
+ if (cwnStatuses.includes('notwkd') && !single.worked_dxcc) passesCwnFilter = true;
+ if (cwnStatuses.includes('wkd') && single.worked_dxcc) passesCwnFilter = true;
+ if (cwnStatuses.includes('cnf') && single.cnfmd_dxcc) passesCwnFilter = true;
+ if (cwnStatuses.includes('ucnf') && single.worked_dxcc && !single.cnfmd_dxcc) passesCwnFilter = true;
+ }
+ if (!passesCwnFilter) return;
+
+ // Apply band filter (client-side for multi-select)
+ let passesBandFilter = bands.includes('All');
+ if (!passesBandFilter) {
+ let freq_khz = single.frequency;
+ let spot_band = getBandFromFrequency(freq_khz);
+ passesBandFilter = bands.includes(spot_band);
+ }
+ if (!passesBandFilter) return;
+
+ // Apply spotted continent filter (which continent the DX station is in)
+ let passesContinentFilter = spottedContinents.includes('Any');
+ if (!passesContinentFilter) {
+ passesContinentFilter = spottedContinents.includes(single.dxcc_spotted.cont);
+ }
+ if (!passesContinentFilter) return;
+
+ // Apply mode filter (client-side for multi-select)
+ let passesModeFilter = modes.includes('All');
+ if (!passesModeFilter) {
+ let spot_mode_category = getModeCategory(single.mode);
+ // Only pass if mode has a category and it matches one of the selected filters
+ passesModeFilter = spot_mode_category && modes.includes(spot_mode_category);
+ }
+ if (!passesModeFilter) return;
+
+ // Apply additional flags filter (POTA, SOTA, WWFF, IOTA, Contest)
+ let passesFlagsFilter = flags.includes('All');
+ if (!passesFlagsFilter) {
+ for (let flag of flags) {
+ if (flag === 'SOTA' && single.dxcc_spotted && single.dxcc_spotted.sota_ref) {
+ passesFlagsFilter = true;
+ break;
+ }
+ if (flag === 'POTA' && single.dxcc_spotted && single.dxcc_spotted.pota_ref) {
+ passesFlagsFilter = true;
+ break;
+ }
+ if (flag === 'WWFF' && single.dxcc_spotted && single.dxcc_spotted.wwff_ref) {
+ passesFlagsFilter = true;
+ break;
+ }
+ if (flag === 'IOTA' && single.dxcc_spotted && single.dxcc_spotted.iota_ref) {
+ passesFlagsFilter = true;
+ break;
+ }
+ if (flag === 'Contest' && single.dxcc_spotted && single.dxcc_spotted.isContest) {
+ passesFlagsFilter = true;
+ break;
+ }
+ }
+ }
+ if (!passesFlagsFilter) return;
+
+ // All filters passed - build table row data
+ spots2render++;
+ var data = [];
+ var dxcc_wked_info, wked_info;
+
+ // Color code DXCC entity: green=confirmed, yellow=worked, red=new
+
+ if (single.cnfmd_dxcc) {
+ dxcc_wked_info = "text-success";
+ } else if (single.worked_dxcc) {
+ dxcc_wked_info = "text-warning";
+ } else {
+ dxcc_wked_info = "text-danger";
+ }
+ // Color code callsign: green=confirmed, yellow=worked
+ if (single.cnfmd_call) {
+ wked_info = "text-success";
+ } else if (single.worked_call) {
+ wked_info = "text-warning";
+ } else {
+ wked_info = "";
+ }
+
+ // Build LoTW badge with color coding based on last upload age
+ var lotw_badge = '';
+ if (single.dxcc_spotted && single.dxcc_spotted.lotw_user) {
+ let lclass = '';
+ if (single.dxcc_spotted.lotw_user > 365) {
+ lclass = 'lotw_info_red';
+ } else if (single.dxcc_spotted.lotw_user > 30) {
+ lclass = 'lotw_info_orange';
+ } else if (single.dxcc_spotted.lotw_user > 7) {
+ lclass = 'lotw_info_yellow';
+ }
+ let lotw_title = 'LoTW User. Last upload was ' + single.dxcc_spotted.lotw_user + ' days ago';
+ lotw_badge = '
' + buildBadge('success ' + lclass, '', lotw_title, 'L') + '';
+ }
+
+ // Build activity badges (POTA, SOTA, WWFF, IOTA, Contest, Worked)
+ let activity_flags = '';
+
+ if (single.dxcc_spotted && single.dxcc_spotted.pota_ref) {
+ let pota_title = 'POTA: ' + single.dxcc_spotted.pota_ref;
+ if (single.dxcc_spotted.pota_mode) {
+ pota_title += ' (' + single.dxcc_spotted.pota_mode + ')';
+ }
+ pota_title += ' - Click to view on POTA.app';
+ activity_flags += '
' + buildBadge('success', 'fa-tree', pota_title) + '';
+ }
+
+ if (single.dxcc_spotted && single.dxcc_spotted.sota_ref) {
+ let sota_title = 'SOTA: ' + single.dxcc_spotted.sota_ref + ' - Click to view on SOTL.as';
+ activity_flags += '
' + buildBadge('primary', 'fa-mountain', sota_title) + '';
+ }
+
+ if (single.dxcc_spotted && single.dxcc_spotted.wwff_ref) {
+ let wwff_title = 'WWFF: ' + single.dxcc_spotted.wwff_ref + ' - Click to view on WWFF.co';
+ activity_flags += '
' + buildBadge('success', 'fa-leaf', wwff_title) + '';
+ }
+
+ if (single.dxcc_spotted && single.dxcc_spotted.iota_ref) {
+ activity_flags += buildBadge('info', 'fa-island-tropical', 'IOTA: ' + single.dxcc_spotted.iota_ref);
+ }
+
+ if (single.dxcc_spotted && single.dxcc_spotted.isContest) {
+ activity_flags += buildBadge('warning', 'fa-trophy', 'Contest');
+ }
+
+ if (single.worked_call) {
+ let worked_title = 'Worked Before';
+ if (single.last_wked && single.last_wked.LAST_QSO && single.last_wked.LAST_MODE) {
+ worked_title = 'Worked: ' + single.last_wked.LAST_QSO + ' in ' + single.last_wked.LAST_MODE;
+ }
+ let worked_badge_type = single.cnfmd_call ? 'success' : 'warning';
+ activity_flags += buildBadge(worked_badge_type, 'fa-check-circle', worked_title, null, true);
+ }
+
+ // Build table row array
+ data[0] = [];
+ // Time column: extract time portion from ISO datetime (e.g., "2025-10-31T23:13:06.347Z" -> "23:13:06")
+ if (timeOnly) {
+ // ISO format: split by 'T' and take time part, then remove milliseconds and Z
+ if (timeOnly.includes('T')) {
+ timeOnly = timeOnly.split('T')[1].split('.')[0];
+ }
+ }
+ data[0].push(timeOnly || '');
+ // Frequency column: convert kHz to MHz with 3 decimal places
+ let freqMHz = (single.frequency / 1000).toFixed(3);
+ data[0].push(freqMHz); // Mode column: capitalize properly
+
+ let displayMode = single.mode || '';
+ if (displayMode.toLowerCase() === 'phone') displayMode = 'Phone';
+ else if (displayMode.toLowerCase() === 'cw') displayMode = 'CW';
+ else if (displayMode.toLowerCase() === 'digi') displayMode = 'Digi';
+ data[0].push(displayMode);
+
+ // Callsign column: wrap in QRZ link with color coding
+ let qrzLink = '
' + single.spotted + '';
+ wked_info = ((wked_info != '' ? '
' : '') + qrzLink + (wked_info != '' ? '' : ''));
+ var spotted = wked_info;
+ data[0].push(spotted);
+ // DXCC entity column: flag emoji + entity name with color coding
+ let dxcc_entity_full = single.dxcc_spotted.entity;
+ if (single.dxcc_spotted.flag) {
+ let flagSpan = '
' + single.dxcc_spotted.flag + '';
+ dxcc_wked_info = ((dxcc_wked_info != '' ? '
' : '') + flagSpan + ' ' + single.dxcc_spotted.entity + '' + (dxcc_wked_info != '' ? '' : ''));
+ } else {
+ dxcc_wked_info = ((dxcc_wked_info != '' ? '
' : '') + '' + single.dxcc_spotted.entity + '' + (dxcc_wked_info != '' ? '' : ''));
+ }
+ data[0].push('
' + dxcc_wked_info + '');
+
+ // Continent column: color code based on worked/confirmed status
+ var continent_wked_info;
+ if (single.cnfmd_continent) {
+ continent_wked_info = "text-success";
+ } else if (single.worked_continent) {
+ continent_wked_info = "text-warning";
+ } else {
+ continent_wked_info = "text-danger";
+ }
+ continent_wked_info = ((continent_wked_info != '' ? '
' : '') + single.dxcc_spotted.cont + (continent_wked_info != '' ? '' : ''));
+
+ data[0].push(continent_wked_info);
+ // Flags column: combine LoTW and activity badges
+ let flags_column = lotw_badge;
+ if (lotw_badge && activity_flags) {
+ flags_column += ' ';
+ }
+ flags_column += activity_flags;
+ data[0].push(flags_column);
+ data[0].push(single.spotter);
+ data[0].push(single.message || '');
+
+ // Add row to table (with "fresh" class for new spots animation)
+ if (oldtable.length > 0) {
+ let update = false;
+ oldtable.each(function (srow) {
+ if (JSON.stringify(srow) === JSON.stringify(data[0])) {
+ update = true;
+ }
+ });
+ if (!update) {
+ table.rows.add(data).draw().nodes().to$().addClass("fresh");
+ } else {
+ table.rows.add(data).draw();
+ }
+ } else {
+ table.rows.add(data).draw();
+ }
+ });
+
+ // Remove "fresh" highlight after 10 seconds
+ setTimeout(function () {
+ $(".fresh").removeClass("fresh");
+ }, 10000);
+
+ if (spots2render == 0) {
+ table.clear();
+ table.settings()[0].oLanguage.sEmptyTable = "No data available";
+ table.draw();
+ }
+
+ // Parse emoji flags for proper rendering
+ if (typeof twemoji !== 'undefined') {
+ twemoji.parse(document.querySelector('.spottable'), {
+ folder: 'svg',
+ ext: '.svg'
+ });
+ }
+
+ // Add hover tooltips to all rows
+ $('.spottable tbody tr').each(function() {
+ $(this).attr('title', lang_click_to_prepare_logging);
+ });
+
+ $('[data-bs-toggle="tooltip"]').tooltip();
+
+ let displayedCount = spots2render || 0;
+
+ // Update status bar after render completes
+ setTimeout(function() {
+ if (!isFetchInProgress) {
+ let actualDisplayedCount = table.rows({search: 'applied'}).count();
+ updateStatusBar(cachedSpotData.length, actualDisplayedCount, getServerFilterText(), getClientFilterText(), false, false);
+ $('#refreshIcon').removeClass('fa-spinner fa-spin').addClass('fa-hourglass-half');
+ $('#refreshTimer').text('Next update in ' + refreshCountdown + 's');
+ }
+ }, 100);
+ }
+
+ // ========================================
+ // BACKEND DATA FETCH
+ // ========================================
+
+ // Fetch spot data from DX cluster API
+ // Backend filters: band, de continent (where spotter is), mode
+ // Client filters applied after fetch: cwn, spotted continent, additionalFlags
+ function fill_list(de, maxAgeMinutes) {
+ var table = get_dtable();
+
+ // Normalize de continent parameter to array
+ let deContinent = Array.isArray(de) ? de : [de];
+ if (deContinent.includes('Any') || deContinent.length === 0) deContinent = ['Any'];
+
+ // Backend API only accepts single values for continent
+ // Band and mode are always 'All' - filtering happens client-side
+ let continentForAPI = 'Any';
+ if (deContinent.length === 1 && !deContinent.includes('Any')) continentForAPI = deContinent[0];
+
+ // Update backend filter state (only continent now)
+ loadedBackendFilters = {
+ continent: continentForAPI
+ };
+
+ lastFetchParams.continent = continentForAPI;
+ lastFetchParams.maxAge = maxAgeMinutes;
+
+ // Build API URL: /spots/{band}/{maxAge}/{continent}/{mode}
+ // Always use 'All' for band and mode - we filter client-side
+ let dxurl = dxcluster_provider + "/spots/All/" + maxAgeMinutes + "/" + continentForAPI + "/All";
+ console.log('Loading from backend: ' + dxurl);
+
+ // Cancel any in-flight request before starting new one
+ if (currentAjaxRequest) {
+ console.log('Aborting previous fetch request');
+ currentAjaxRequest.abort();
+ currentAjaxRequest = null;
+ }
+
+ isFetchInProgress = true;
+
+ updateStatusBar(0, 0, getServerFilterText(), getClientFilterText(), true, false);
+
+ currentAjaxRequest = $.ajax({
+ url: dxurl,
+ cache: false,
+ dataType: "json"
+ }).done(function(dxspots) {
+ currentAjaxRequest = null;
+ table.page.len(50);
+
+ if (dxspots.length > 0) {
+ dxspots.sort(SortByQrg); // Sort by frequency
+ cachedSpotData = dxspots;
+ } else {
+ cachedSpotData = [];
+ }
+
+ lastFetchParams.timestamp = new Date();
+ isFetchInProgress = false;
+
+ renderFilteredSpots(); // Apply client-side filters and render
+ startRefreshTimer(); // Start 60s countdown
+
+ }).fail(function(jqXHR, textStatus) {
+ currentAjaxRequest = null;
+
+ // Don't show error if user cancelled the request
+ if (textStatus === 'abort') {
+ console.log('Fetch request aborted');
+ return;
+ }
+
+ cachedSpotData = null;
+ isFetchInProgress = false;
+ table.clear();
+ table.settings()[0].oLanguage.sEmptyTable = "Error loading spots. Please try again.";
+ table.draw();
+ updateStatusBar(0, 0, getServerFilterText(), getClientFilterText(), false, false);
+ startRefreshTimer();
+ });
+ } // Highlight rows within ±20 kHz of specified frequency (for CAT integration)
function highlight_current_qrg(qrg) {
- var table=get_dtable();
- // var table=$('.spottable').DataTable();
+ var table = get_dtable();
table.rows().eq(0).each( function ( index ) {
let row = table.row( index );
var d=row.data();
@@ -211,11 +996,83 @@ $(function() {
});
}
+ // Initialize DataTable
var table=get_dtable();
- table.order([1, 'asc']);
+ table.order([1, 'asc']); // Sort by frequency column
table.clear();
- // Function to get selected values from multi-select
+ // ========================================
+ // HELPER FUNCTIONS
+ // ========================================
+
+ // Build a badge HTML string with consistent styling
+ // type: badge color (success/primary/info/warning/danger)
+ // icon: FontAwesome icon class (e.g., 'fa-tree')
+ // title: tooltip text
+ // text: optional text content instead of icon
+ // isLast: if true, uses margin: 0 instead of negative margin
+ function buildBadge(type, icon, title, text = null, isLast = false) {
+ const margin = isLast ? '0' : '0 -0.5px 0 0';
+ const fontSize = text ? '0.7rem' : '0.65rem';
+ const content = text ? text : '
';
+ return '
' + content + '';
+ }
+
+ // Map frequency (in kHz) to ham band name
+ function getBandFromFrequency(freq_khz) {
+ if (freq_khz >= 1800 && freq_khz <= 2000) return '160m';
+ if (freq_khz >= 3500 && freq_khz <= 4000) return '80m';
+ if (freq_khz >= 5250 && freq_khz <= 5450) return '60m';
+ if (freq_khz >= 7000 && freq_khz <= 7300) return '40m';
+ if (freq_khz >= 10100 && freq_khz <= 10150) return '30m';
+ if (freq_khz >= 14000 && freq_khz <= 14350) return '20m';
+ if (freq_khz >= 18068 && freq_khz <= 18168) return '17m';
+ if (freq_khz >= 21000 && freq_khz <= 21450) return '15m';
+ if (freq_khz >= 24890 && freq_khz <= 24990) return '12m';
+ if (freq_khz >= 28000 && freq_khz <= 29700) return '10m';
+ if (freq_khz >= 50000 && freq_khz <= 54000) return '6m';
+ if (freq_khz >= 70000 && freq_khz <= 71000) return '4m';
+ if (freq_khz >= 144000 && freq_khz <= 148000) return '2m';
+ if (freq_khz >= 222000 && freq_khz <= 225000) return '1.25m';
+ if (freq_khz >= 420000 && freq_khz <= 450000) return '70cm';
+ if (freq_khz >= 902000 && freq_khz <= 928000) return '33cm';
+ if (freq_khz >= 1240000 && freq_khz <= 1300000) return '23cm';
+ if (freq_khz >= 2300000 && freq_khz <= 2450000) return '13cm';
+ return 'All';
+ }
+
+ // Categorize mode as phone/cw/digi for filtering
+ function getModeCategory(mode) {
+ if (!mode) return null;
+
+ // Mode can come from server as lowercase category names (phone, cw, digi)
+ // or as actual mode names (SSB, LSB, FT8, etc.)
+ let modeLower = mode.toLowerCase();
+
+ // Check if already a category
+ if (['phone', 'cw', 'digi'].includes(modeLower)) {
+ return modeLower;
+ }
+
+ // Otherwise categorize by mode name
+ mode = mode.toUpperCase();
+
+ // Phone modes
+ if (['SSB', 'LSB', 'USB', 'FM', 'AM', 'DV'].includes(mode)) return 'phone';
+
+ // CW modes
+ if (['CW', 'CWR'].includes(mode)) return 'cw';
+
+ // Digital modes
+ if (['RTTY', 'PSK', 'PSK31', 'PSK63', 'FT8', 'FT4', 'JT65', 'JT9', 'MFSK',
+ 'OLIVIA', 'CONTESTIA', 'HELL', 'SSTV', 'FAX', 'PACKET', 'PACTOR',
+ 'THOR', 'DOMINO', 'MT63', 'ROS', 'WSPR'].includes(mode)) return 'digi';
+
+ // Return null for uncategorized modes instead of 'All'
+ return null;
+ }
+
+ // Get selected values from multi-select dropdown
function getSelectedValues(selectId) {
let values = $('#' + selectId).val();
if (!values || values.length === 0) {
@@ -224,39 +1081,130 @@ $(function() {
return values;
}
- // Function to apply filters
- function applyFilters() {
+ // ========================================
+ // SMART FILTER APPLICATION
+ // ========================================
+
+ // Intelligently decide whether to reload from backend or filter client-side
+ // Backend filter (requires new API call): de continent only
+ // Client filters (use cached data): band, mode, cwn, spotted continent, requiredFlags, additionalFlags
+ function applyFilters(forceReload = false) {
let band = getSelectedValues('band');
let de = getSelectedValues('decontSelect');
+ let continent = getSelectedValues('continentSelect');
let cwn = getSelectedValues('cwnSelect');
let mode = getSelectedValues('mode');
+ let additionalFlags = getSelectedValues('additionalFlags');
+ let requiredFlags = $('#requiredFlags').val() || [];
- table.clear();
- fill_list(band, de, dxcluster_maxage, cwn, mode);
+ let continentForAPI = 'Any';
+ if (de.length === 1 && !de.includes('Any')) {
+ continentForAPI = de[0];
+ }
+
+ console.log('applyFilters - Current backend filters:', loadedBackendFilters);
+ console.log('applyFilters - Requested backend params:', {continent: continentForAPI});
+
+ // Check if backend parameters changed (requires new data fetch)
+ // Only de continent affects backend now - band and mode are client-side only
+ let backendParamsChanged = forceReload ||
+ loadedBackendFilters.continent !== continentForAPI;
+
+ console.log('applyFilters - backendParamsChanged:', backendParamsChanged);
+
+ // Always update current filters for client-side filtering
+ currentFilters = {
+ band: band,
+ deContinent: de, // Spots FROM continent (server filter)
+ spottedContinent: continent, // Spotted STATION continent (client filter)
+ cwn: cwn,
+ mode: mode,
+ requiredFlags: requiredFlags,
+ additionalFlags: additionalFlags
+ };
+
+ if (backendParamsChanged) {
+ console.log('Reloading from backend: continent=' + continentForAPI);
+ table.clear();
+ fill_list(de, dxcluster_maxage);
+ } else {
+ console.log('Client-side filtering changed - using cached data');
+ renderFilteredSpots();
+ }
+
+ updateFilterIcon();
}
- // Initial load
- applyFilters();
+ initializeBackendFilters();
- // Auto-refresh every 60 seconds
- setInterval(function () { applyFilters(); }, 60000);
+ initFilterCheckboxes();
- // Apply filters button click
- $("#applyFiltersButton").on("click", function() {
- applyFilters();
- // Disable button after applying filters
- $(this).prop('disabled', true);
+ applyFilters(true);
+
+ // Sync button states on initial load
+ syncQuickFilterButtons();
+ updateFilterIcon();
+
+ $("#applyFiltersButtonPopup").on("click", function() {
+ applyFilters(false);
+ $('#filterDropdown').dropdown('hide');
+ });
+
+ $("#clearFiltersButton").on("click", function() {
+ $('#cwnSelect').val(['All']);
+ $('#decontSelect').val(['Any']);
+ $('#continentSelect').val(['Any']);
+ $('#band').val(['All']);
+ $('#mode').val(['All']);
+ $('#additionalFlags').val(['All']);
+ $('#requiredFlags').val([]);
+
+ // Clear text search
+ $('#spotSearchInput').val('');
+ table.search('').draw();
+
+ syncQuickFilterButtons();
+ updateFilterIcon();
+ applyFilters(true);
+ $('#filterDropdown').dropdown('hide');
+ });
+
+ // Sync button states when dropdown is shown
+ $('#filterDropdown').on('show.bs.dropdown', function() {
+ syncQuickFilterButtons();
+ });
+
+ // Sync button states when dropdown is hidden
+ $('#filterDropdown').on('hide.bs.dropdown', function() {
+ syncQuickFilterButtons();
});
- // Connect search input to DataTable
$("#spotSearchInput").on("keyup", function() {
table.search(this.value).draw();
});
- // Remove old individual change handlers and keep only for specific use cases
- // Note: Radio selector still triggers immediate update
+ $("#spotSearchInput").on("input", function() {
+ const cursorPos = this.selectionStart;
+ const oldValue = this.value;
+ const newValue = oldValue.replace(/0/g, "Ø");
+
+ if (newValue !== oldValue) {
+ this.value = newValue;
+ // Restore cursor position
+ this.setSelectionRange(cursorPos, cursorPos);
+ // Trigger search with new value
+ table.search(newValue).draw();
+ }
+ });
+
+ $("#searchIcon").on("click", function() {
+ const searchValue = $("#spotSearchInput").val();
+ if (searchValue.length > 2) {
+ table.search(searchValue).draw();
+ }
+ });
+
$("#radio").on("change", function() {
- // Radio change doesn't affect filters, handled separately
});
$("#spottertoggle").on("click", function() {
@@ -279,7 +1227,6 @@ $(function() {
};
setInterval(function () {
- // reset the pong flag if the last seen time is older than 1 second in case the qso window was closed
if (qso_window_last_seen < (Date.now()-1000)) {
pong_rcvd = false;
}
@@ -289,22 +1236,11 @@ $(function() {
let bc2qso = new BroadcastChannel('qso_wish');
var CatCallbackURL = "http://127.0.0.1:54321";
- // set some times
- let wait4pong = 2000; // we wait in max 2 seconds for the pong
- let check_intv = 100; // check every 100 ms
+ let wait4pong = 2000;
+ let check_intv = 100;
- $(document).on('click','#prepcall', function() {
+ function prepareLogging(call, qrg, mode) {
let ready_listener = true;
- let call=this.innerText;
- let qrg=''
- let mode='';
- if (this.parentNode.parentNode.className.indexOf('spotted_call')>=0) {
- qrg=this.parentNode.parentNode.parentNode.cells[1].textContent*1000;
- mode=this.parentNode.parentNode.parentNode.cells[8].textContent;
- } else {
- qrg=this.parentNode.parentNode.cells[1].textContent*1000;
- mode=this.parentNode.parentNode.cells[8].textContent;
- }
try {
irrelevant=fetch(CatCallbackURL + '/'+qrg+'/'+mode).catch(() => {
@@ -315,7 +1251,7 @@ $(function() {
let check_pong = setInterval(function() {
if (pong_rcvd || ((Date.now() - qso_window_last_seen) < wait4pong)) {
- clearInterval(check_pong); // max time reached or pong received
+ clearInterval(check_pong);
bc2qso.postMessage({ frequency: qrg, call: call });
} else {
clearInterval(check_pong);
@@ -334,7 +1270,6 @@ $(function() {
newWindow.focus();
}
- // wait for the ready message
bc2qso.onmessage = function(ev) {
if (ready_listener == true) {
if (ev.data === 'ready') {
@@ -345,6 +1280,21 @@ $(function() {
};
}
}, check_intv);
+ }
+
+ $(document).on('click','#prepcall', function() {
+ let call=this.innerText;
+ let qrg=''
+ let mode='';
+ if (this.parentNode.parentNode.className.indexOf('spotted_call')>=0) {
+ qrg=this.parentNode.parentNode.parentNode.cells[1].textContent*1000;
+ mode=this.parentNode.parentNode.parentNode.cells[2].textContent;
+ } else {
+ qrg=this.parentNode.parentNode.cells[1].textContent*1000;
+ mode=this.parentNode.parentNode.cells[2].textContent;
+ }
+
+ prepareLogging(call, qrg, mode);
});
$("#menutoggle").on("click", function() {
@@ -363,6 +1313,26 @@ $(function() {
}
});
+ $(document).on('click', '.activity-flag-link', function(e) {
+ e.stopPropagation();
+
+ let activityType = $(this).data('activity-type');
+ let ref = $(this).data('ref');
+ let url = '';
+
+ if (activityType === 'pota') {
+ url = 'https://pota.app/#/park/' + ref;
+ } else if (activityType === 'sota') {
+ url = 'https://sotl.as/summits/' + ref;
+ } else if (activityType === 'wwff') {
+ url = 'https://wwff.co/directory/?showRef=' + ref;
+ }
+
+ if (url) {
+ window.open(url, '_blank');
+ }
+ });
+
let websocket = null;
let reconnectAttempts = 0;
let websocketEnabled = false;
@@ -405,26 +1375,23 @@ $(function() {
}
function handleWebSocketData(data) {
- // Handle welcome message
if (data.type === 'welcome') {
return;
}
- // Handle radio status updates
if (data.type === 'radio_status' && data.radio && ($(".radios option:selected").val() == 'ws')) {
data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
data.cat_url = 'http://127.0.0.1:54321';
- // Cache the radio data
updateCATui(data);
}
}
$( "#radio" ).change(function() {
- if (CATInterval) { // We've a change - stop polling if active
+ if (CATInterval) {
clearInterval(CATInterval);
CATInterval=null;
}
- if (websocket) { // close possible websocket connection
+ if (websocket) {
websocket.close();
websocketEnabled = false;
}
@@ -496,9 +1463,557 @@ $(function() {
}
};
-$.fn.dataTable.moment(custom_date_format + ' HH:mm');
+ $.fn.dataTable.moment(custom_date_format + ' HH:mm');
-// If a radios selected from drop down select radio update.
-$('#radio').change();
+ $('#radio').change();
+
+ let isFullscreen = false;
+
+ $('#fullscreenToggle').on('click', function() {
+ const container = $('#bandmapContainer');
+ const icon = $('#fullscreenIcon');
+ const radioSelector = container.find('.d-flex.align-items-center.mb-3').first();
+
+ if (!isFullscreen) {
+ container.addClass('bandmap-fullscreen');
+ $('body').addClass('fullscreen-active');
+ icon.removeClass('fa-expand').addClass('fa-compress');
+ $(this).attr('title', 'Exit Fullscreen');
+
+ radioSelector.hide();
+ $('#radio_status').hide();
+ $('.messages').hide();
+
+ isFullscreen = true;
+
+ // Request browser fullscreen
+ const elem = document.documentElement;
+ if (elem.requestFullscreen) {
+ elem.requestFullscreen().catch(err => {
+ console.log('Fullscreen request failed:', err);
+ });
+ } else if (elem.webkitRequestFullscreen) { // Safari
+ elem.webkitRequestFullscreen();
+ } else if (elem.msRequestFullscreen) { // IE11
+ elem.msRequestFullscreen();
+ }
+
+ setTimeout(function() {
+ if ($.fn.DataTable.isDataTable('.spottable')) {
+ $('.spottable').DataTable().columns.adjust();
+ }
+ }, 100);
+ } else {
+ container.removeClass('bandmap-fullscreen');
+ $('body').removeClass('fullscreen-active');
+ icon.removeClass('fa-compress').addClass('fa-expand');
+ $(this).attr('title', 'Toggle Fullscreen');
+
+ radioSelector.show();
+ $('#radio_status').show();
+ $('.messages').show();
+
+ isFullscreen = false;
+
+ // Exit browser fullscreen
+ if (document.exitFullscreen) {
+ document.exitFullscreen().catch(err => {
+ console.log('Exit fullscreen failed:', err);
+ });
+ } else if (document.webkitExitFullscreen) { // Safari
+ document.webkitExitFullscreen();
+ } else if (document.msExitFullscreen) { // IE11
+ document.msExitFullscreen();
+ }
+
+ setTimeout(function() {
+ if ($.fn.DataTable.isDataTable('.spottable')) {
+ $('.spottable').DataTable().columns.adjust();
+ }
+ }, 100);
+ }
+ });
+
+ $(document).on('keydown', function(e) {
+ if (e.key === 'Escape' && isFullscreen) {
+ $('#fullscreenToggle').click();
+ }
+ });
+
+ // ========================================
+ // QUICK FILTER TOGGLE BUTTONS
+ // ========================================
+
+ // Toggle CW mode filter
+ $('#toggleCwFilter').on('click', function() {
+ let currentValues = $('#mode').val() || [];
+
+ // Remove 'All' if present
+ if (currentValues.includes('All')) {
+ currentValues = currentValues.filter(v => v !== 'All');
+ }
+
+ if (currentValues.includes('cw')) {
+ // Remove CW filter
+ currentValues = currentValues.filter(v => v !== 'cw');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ // Add CW filter
+ currentValues.push('cw');
+ }
+
+ $('#mode').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ // Toggle Digital mode filter
+ $('#toggleDigiFilter').on('click', function() {
+ let currentValues = $('#mode').val() || [];
+
+ // Remove 'All' if present
+ if (currentValues.includes('All')) {
+ currentValues = currentValues.filter(v => v !== 'All');
+ }
+
+ if (currentValues.includes('digi')) {
+ // Remove Digi filter
+ currentValues = currentValues.filter(v => v !== 'digi');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ // Add Digi filter
+ currentValues.push('digi');
+ }
+
+ $('#mode').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ // Toggle Phone mode filter
+ $('#togglePhoneFilter').on('click', function() {
+ let currentValues = $('#mode').val() || [];
+
+ // Remove 'All' if present
+ if (currentValues.includes('All')) {
+ currentValues = currentValues.filter(v => v !== 'All');
+ }
+
+ if (currentValues.includes('phone')) {
+ // Remove Phone filter
+ currentValues = currentValues.filter(v => v !== 'phone');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ // Add Phone filter
+ currentValues.push('phone');
+ }
+
+ $('#mode').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ // Band filter buttons
+ $('#toggle160mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('160m')) {
+ currentValues = currentValues.filter(v => v !== '160m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('160m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle80mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('80m')) {
+ currentValues = currentValues.filter(v => v !== '80m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('80m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle60mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('60m')) {
+ currentValues = currentValues.filter(v => v !== '60m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('60m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle40mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('40m')) {
+ currentValues = currentValues.filter(v => v !== '40m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('40m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle30mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('30m')) {
+ currentValues = currentValues.filter(v => v !== '30m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('30m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle20mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('20m')) {
+ currentValues = currentValues.filter(v => v !== '20m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('20m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle17mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('17m')) {
+ currentValues = currentValues.filter(v => v !== '17m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('17m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle15mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('15m')) {
+ currentValues = currentValues.filter(v => v !== '15m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('15m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle12mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('12m')) {
+ currentValues = currentValues.filter(v => v !== '12m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('12m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle10mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('10m')) {
+ currentValues = currentValues.filter(v => v !== '10m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('10m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle6mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('6m')) {
+ currentValues = currentValues.filter(v => v !== '6m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('6m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle4mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('4m')) {
+ currentValues = currentValues.filter(v => v !== '4m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('4m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle2mFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('2m')) {
+ currentValues = currentValues.filter(v => v !== '2m');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('2m');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle70cmFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('70cm')) {
+ currentValues = currentValues.filter(v => v !== '70cm');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('70cm');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggle23cmFilter').on('click', function() {
+ let currentValues = $('#band').val() || [];
+ if (currentValues.includes('All')) currentValues = currentValues.filter(v => v !== 'All');
+ if (currentValues.includes('23cm')) {
+ currentValues = currentValues.filter(v => v !== '23cm');
+ if (currentValues.length === 0) currentValues = ['All'];
+ } else {
+ currentValues.push('23cm');
+ }
+ $('#band').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ // Continent filter buttons (spotter's continent - de continent)
+ $('#toggleAfricaFilter').on('click', function() {
+ let currentValues = $('#decontSelect').val() || [];
+ if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any');
+ if (currentValues.includes('AF')) {
+ currentValues = currentValues.filter(v => v !== 'AF');
+ if (currentValues.length === 0) currentValues = ['Any'];
+ } else {
+ currentValues.push('AF');
+ }
+ $('#decontSelect').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggleAsiaFilter').on('click', function() {
+ let currentValues = $('#decontSelect').val() || [];
+ if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any');
+ if (currentValues.includes('AS')) {
+ currentValues = currentValues.filter(v => v !== 'AS');
+ if (currentValues.length === 0) currentValues = ['Any'];
+ } else {
+ currentValues.push('AS');
+ }
+ $('#decontSelect').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggleEuropeFilter').on('click', function() {
+ let currentValues = $('#decontSelect').val() || [];
+ if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any');
+ if (currentValues.includes('EU')) {
+ currentValues = currentValues.filter(v => v !== 'EU');
+ if (currentValues.length === 0) currentValues = ['Any'];
+ } else {
+ currentValues.push('EU');
+ }
+ $('#decontSelect').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggleNorthAmericaFilter').on('click', function() {
+ let currentValues = $('#decontSelect').val() || [];
+ if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any');
+ if (currentValues.includes('NA')) {
+ currentValues = currentValues.filter(v => v !== 'NA');
+ if (currentValues.length === 0) currentValues = ['Any'];
+ } else {
+ currentValues.push('NA');
+ }
+ $('#decontSelect').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ $('#toggleSouthAmericaFilter').on('click', function() {
+ let currentValues = $('#decontSelect').val() || [];
+ if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any');
+ if (currentValues.includes('SA')) {
+ currentValues = currentValues.filter(v => v !== 'SA');
+ if (currentValues.length === 0) currentValues = ['Any'];
+ } else {
+ currentValues.push('SA');
+ }
+ $('#decontSelect').val(currentValues).trigger('change');
+ syncQuickFilterButtons();
+ applyFilters(false);
+ });
+
+ // Toggle LoTW User filter
+ $('#toggleLotwFilter').on('click', function() {
+ let currentValues = $('#requiredFlags').val() || [];
+ let btn = $(this);
+
+ if (currentValues.includes('lotw')) {
+ // Remove LoTW filter
+ currentValues = currentValues.filter(v => v !== 'lotw');
+ btn.removeClass('btn-warning').addClass('btn-primary');
+ btn.find('i').removeClass('fa-check-circle').addClass('fa-upload');
+ } else {
+ // Add LoTW filter
+ currentValues.push('lotw');
+ btn.removeClass('btn-primary').addClass('btn-warning');
+ btn.find('i').removeClass('fa-upload').addClass('fa-check-circle');
+ }
+
+ $('#requiredFlags').val(currentValues).trigger('change');
+ applyFilters(false);
+ });
+
+ // Toggle Not Worked Before filter
+ $('#toggleNotWorkedFilter').on('click', function() {
+ let currentValues = $('#requiredFlags').val() || [];
+ let btn = $(this);
+
+ if (currentValues.includes('notworked')) {
+ // Remove Not Worked filter
+ currentValues = currentValues.filter(v => v !== 'notworked');
+ btn.removeClass('btn-warning').addClass('btn-primary');
+ btn.find('i').removeClass('fa-check-circle').addClass('fa-star');
+ } else {
+ // Add Not Worked filter
+ currentValues.push('notworked');
+ btn.removeClass('btn-primary').addClass('btn-warning');
+ btn.find('i').removeClass('fa-star').addClass('fa-check-circle');
+ }
+
+ $('#requiredFlags').val(currentValues).trigger('change');
+ applyFilters(false);
+ });
+
+ // Toggle DXCC Needed filter (not worked DXCC)
+ $('#toggleDxccNeededFilter').on('click', function() {
+ let currentValues = $('#cwnSelect').val() || [];
+ let btn = $(this);
+
+ if (currentValues.length === 1 && currentValues[0] === 'notwkd') {
+ // Remove DXCC filter - reset to All
+ currentValues = ['All'];
+ btn.removeClass('btn-warning').addClass('btn-primary');
+ btn.find('i').removeClass('fa-check-circle').addClass('fa-globe');
+ } else {
+ // Set DXCC filter to Not Worked only
+ currentValues = ['notwkd'];
+ btn.removeClass('btn-primary').addClass('btn-warning');
+ btn.find('i').removeClass('fa-globe').addClass('fa-check-circle');
+ }
+
+ $('#cwnSelect').val(currentValues).trigger('change');
+ applyFilters(false);
+ });
+
+ // Toggle Contest filter
+ $('#toggleContextFilter').on('click', function() {
+ let currentValues = $('#additionalFlags').val() || [];
+ let btn = $(this);
+
+ // Remove 'All' if present
+ if (currentValues.includes('All')) {
+ currentValues = currentValues.filter(v => v !== 'All');
+ }
+
+ if (currentValues.includes('Contest')) {
+ // Remove Contest filter
+ currentValues = currentValues.filter(v => v !== 'Contest');
+ if (currentValues.length === 0) currentValues = ['All'];
+ btn.removeClass('btn-warning').addClass('btn-primary');
+ btn.find('i').removeClass('fa-check-circle').addClass('fa-trophy');
+ } else {
+ // Add Contest filter
+ currentValues.push('Contest');
+ btn.removeClass('btn-primary').addClass('btn-warning');
+ btn.find('i').removeClass('fa-trophy').addClass('fa-check-circle');
+ }
+
+ $('#additionalFlags').val(currentValues).trigger('change');
+ applyFilters(false);
+ });
+
+ // Toggle Geo Hunter filter (POTA, SOTA, IOTA, WWFF)
+ $('#toggleGeoHunterFilter').on('click', function() {
+ let currentValues = $('#additionalFlags').val() || [];
+ let btn = $(this);
+ let geoFlags = ['POTA', 'SOTA', 'IOTA', 'WWFF'];
+
+ // Remove 'All' if present
+ if (currentValues.includes('All')) {
+ currentValues = currentValues.filter(v => v !== 'All');
+ }
+
+ // Check if any geo flag is active
+ let hasGeoFlag = geoFlags.some(flag => currentValues.includes(flag));
+
+ if (hasGeoFlag) {
+ // Remove all geo flags
+ currentValues = currentValues.filter(v => !geoFlags.includes(v));
+ if (currentValues.length === 0) currentValues = ['All'];
+ btn.removeClass('btn-warning').addClass('btn-primary');
+ btn.find('i').removeClass('fa-check-circle').addClass('fa-map-marked-alt');
+ } else {
+ // Add all geo flags
+ currentValues = currentValues.concat(geoFlags.filter(flag => !currentValues.includes(flag)));
+ btn.removeClass('btn-primary').addClass('btn-warning');
+ btn.find('i').removeClass('fa-map-marked-alt').addClass('fa-check-circle');
+ }
+
+ $('#additionalFlags').val(currentValues).trigger('change');
+ applyFilters(false);
+ });
});