From 02ce0b113a7f4d353e1df06d51e34a8da44f34b3 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Fri, 31 Oct 2025 16:13:56 +0100 Subject: [PATCH 01/51] Initial commit --- application/views/bandmap/list.php | 240 +++++++++++++++++++++++------ assets/js/sections/bandmap_list.js | 98 +++++++++--- 2 files changed, 272 insertions(+), 66 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index fbdc2fa4f..4380c36b1 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -5,6 +5,46 @@ var custom_date_format = ""; var popup_warning = ""; var lang_click_to_prepare_logging = ""; + + // Handle "All" option for multi-select dropdowns + $(document).ready(function() { + // Disable Apply Filters button initially + $('#applyFiltersButton').prop('disabled', true); + + // Function to enable Apply Filters button when changes are made + function enableApplyButton() { + $('#applyFiltersButton').prop('disabled', false); + } + + // Function to handle All option selection + function handleAllOption(selectId) { + $('#' + selectId).on('change', function() { + let selected = $(this).val() || []; + + // If "All" was just selected, deselect all others + if (selected.includes('All') || selected.includes('Any')) { + let allValue = selected.includes('All') ? 'All' : 'Any'; + if (selected.length > 1) { + // All was just selected, keep only All + $(this).val([allValue]); + } + } else if (selected.length === 0) { + // Nothing selected, select All + let allValue = selectId === 'decontSelect' ? 'Any' : 'All'; + $(this).val([allValue]); + } + + // Enable Apply Filters button + enableApplyButton(); + }); + } + + // Apply to all filter selects + handleAllOption('cwnSelect'); + handleAllOption('decontSelect'); + handleAllOption('band'); + handleAllOption('mode'); + }); @@ -52,7 +157,9 @@
-
+ + +
- - - - +
- - - - -
+ +
+
+ +
+
-

-

- +
+
diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 38761312d..601a71ff8 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -9,6 +9,8 @@ $(function() { function get_dtable () { var table = $('.spottable').DataTable({ "paging": false, + "searching": true, + "dom": 'rt', // Only show table (r) and processing (t), hide search box and other elements "retrieve": true, "language": { url: getDataTablesLanguageUrl(), @@ -45,8 +47,34 @@ $(function() { function fill_list(band, de, maxAgeMinutes, cwn, mode) { // var table = $('.spottable').DataTable(); var table = get_dtable(); - if ((band != '') && (band !== undefined)) { - let dxurl = dxcluster_provider + "/spots/" + band + "/" +maxAgeMinutes + "/" + de + "/" + mode; + + // 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']; + } + + // 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 ((bandForAPI != '') && (bandForAPI !== undefined)) { + let dxurl = dxcluster_provider + "/spots/" + bandForAPI + "/" +maxAgeMinutes + "/" + continentForAPI + "/" + modeForAPI; $.ajax({ url: dxurl, cache: false, @@ -59,10 +87,16 @@ $(function() { if (dxspots.length>0) { dxspots.sort(SortByQrg); dxspots.forEach((single) => { - if ((cwn == 'notwkd') && ((single.worked_dxcc))) { return; } - if ((cwn == 'wkd') && (!(single.worked_dxcc))) { return; } - if ((cwn == 'cnf') && (!(single.cnfmd_dxcc))) { return; } - if ((cwn == 'ucnf') && (!(single.worked_dxcc) || single.cnfmd_dxcc)) { return; } + // 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; } + spots2render++; var data=[]; if (single.cnfmd_dxcc) { @@ -180,27 +214,49 @@ $(function() { var table=get_dtable(); table.order([1, 'asc']); table.clear(); - fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); - setInterval(function () { fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); },60000); - $("#cwnSelect").on("change",function() { + // Function to get selected values from multi-select + function getSelectedValues(selectId) { + let values = $('#' + selectId).val(); + if (!values || values.length === 0) { + return ['All']; + } + return values; + } + + // Function to apply filters + function applyFilters() { + let band = getSelectedValues('band'); + let de = getSelectedValues('decontSelect'); + let cwn = getSelectedValues('cwnSelect'); + let mode = getSelectedValues('mode'); + table.clear(); - fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); + fill_list(band, de, dxcluster_maxage, cwn, mode); + } + + // Initial load + applyFilters(); + + // Auto-refresh every 60 seconds + setInterval(function () { applyFilters(); }, 60000); + + // Apply filters button click + $("#applyFiltersButton").on("click", function() { + applyFilters(); + // Disable button after applying filters + $(this).prop('disabled', true); }); - $("#decontSelect").on("change",function() { - table.clear(); - fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); + // Connect search input to DataTable + $("#spotSearchInput").on("keyup", function() { + table.search(this.value).draw(); }); - $("#band").on("change",function() { - table.clear(); - fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); - }); - - $("#mode").on("change",function() { - table.clear(); - fill_list($('#band option:selected').val(), $('#decontSelect option:selected').val(), dxcluster_maxage, $('#cwnSelect option:selected').val(), $('#mode option:selected').val()); + // Remove old individual change handlers and keep only for specific use cases + // Note: Radio selector still triggers immediate update + $("#radio").on("change", function() { + // Radio change doesn't affect filters, handled separately }); $("#spottertoggle").on("click", function() { From 7ab148d12c8c738e737e32154dce7eba4340d21b Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sat, 1 Nov 2025 01:53:52 +0100 Subject: [PATCH 02/51] First prototype ready --- application/views/bandmap/list.php | 832 ++++++++---- assets/js/sections/bandmap_list.js | 1929 +++++++++++++++++++++++++--- 2 files changed, 2338 insertions(+), 423 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 4380c36b1..85146f667 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -1,72 +1,19 @@ -
-
-
- +
-

+ +
+ + +
-
-
+ +
+ +
- -
- - + +
+
+
+ "> + + +
DX Cluster - spot list
+
+
+ + +
- - -
-
-
/
+ + + + + + + + + + + + + + + + +
UTC"> UTC [MHz]">">">">">">">">
+
- -
- - - - - - - - - - - - - - - - - -
/
- - - diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 601a71ff8..d02140800 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1,200 +1,985 @@ +/** + * @fileoverview DX CLUSTER BANDMAP for WaveLog + * @version 1.0.0 + * @author Wavelog Team + * + * @description + * Real-time DX spot filtering and display with intelligent client/server-side + * filter architecture, smart caching, and multi-criteria spot filtering. + * + * @requires jQuery + * @requires DataTables + * @requires base_url (global from Wavelog) + * @requires dxcluster_provider (global from Wavelog) + * @requires dxcluster_maxage (global from Wavelog) + * + * @browserSupport + * - Chrome 90+ + * - Firefox 88+ + * - Safari 14+ + * - Edge 90+ + * + * @features + * - Smart filter architecture (server-side: continent only; client-side: band, mode, flags) + * - Real-time spot caching and client-side filtering + * - Multi-select filters with AND/OR logic + * - Required flags (LoTW, Not Worked) with AND logic + * - Activity flags (POTA, SOTA, WWFF, IOTA, Contest) + * - Auto-refresh with 60-second countdown timer + * - DXCC status color coding (Confirmed/Worked/New) + */ + +'use strict'; + $(function() { - function SortByQrg(a, b){ - var a = a.frequency; - var b = b.frequency; - return ((a< b) ? -1 : ((a> b) ? 1 : 0)); + // ======================================== + // FILTER UI MANAGEMENT + // ======================================== + + // Check if any filters are active (not default "All"/"Any" values) + function areFiltersApplied() { + let cwnVal = $('#cwnSelect').val() || []; + let decontVal = $('#decontSelect').val() || []; + let continentVal = $('#continentSelect').val() || []; + let bandVal = $('#band').val() || []; + let modeVal = $('#mode').val() || []; + let flagsVal = $('#additionalFlags').val() || []; + let requiredVal = $('#requiredFlags').val() || []; + + // Check if anything is selected besides "All"/"Any" + let isDefaultCwn = cwnVal.length === 1 && cwnVal.includes('All'); + let isDefaultDecont = decontVal.length === 1 && decontVal.includes('Any'); + let isDefaultContinent = continentVal.length === 1 && continentVal.includes('Any'); + let isDefaultBand = bandVal.length === 1 && bandVal.includes('All'); + let isDefaultMode = modeVal.length === 1 && modeVal.includes('All'); + let isDefaultFlags = flagsVal.length === 1 && flagsVal.includes('All'); + let isDefaultRequired = requiredVal.length === 0; + + return !(isDefaultCwn && isDefaultDecont && isDefaultContinent && isDefaultBand && isDefaultMode && isDefaultFlags && isDefaultRequired); } - function get_dtable () { + // Update filter icon based on whether filters are active + function updateFilterIcon() { + if (areFiltersApplied()) { + $('#filterIcon').removeClass('fa-filter').addClass('fa-filter-circle-xmark'); + $('#filterDropdown').removeClass('btn-primary').addClass('btn-warning'); + } else { + $('#filterIcon').removeClass('fa-filter-circle-xmark').addClass('fa-filter'); + $('#filterDropdown').removeClass('btn-warning').addClass('btn-primary'); + } + } + + // Sync quick filter button states with their corresponding dropdown values + function syncQuickFilterButtons() { + let requiredFlags = $('#requiredFlags').val() || []; + let additionalFlags = $('#additionalFlags').val() || []; + let cwnValues = $('#cwnSelect').val() || []; + let modeValues = $('#mode').val() || []; + let bandValues = $('#band').val() || []; + let decontValues = $('#decontSelect').val() || []; + + // LoTW button + if (requiredFlags.includes('lotw')) { + $('#toggleLotwFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleLotwFilter i').removeClass('fa-upload').addClass('fa-check-circle'); + } else { + $('#toggleLotwFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleLotwFilter i').removeClass('fa-check-circle').addClass('fa-upload'); + } + + // Not Worked button + if (requiredFlags.includes('notworked')) { + $('#toggleNotWorkedFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleNotWorkedFilter i').removeClass('fa-star').addClass('fa-check-circle'); + } else { + $('#toggleNotWorkedFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleNotWorkedFilter i').removeClass('fa-check-circle').addClass('fa-star'); + } + + // DXCC Needed button + if (cwnValues.length === 1 && cwnValues[0] === 'notwkd') { + $('#toggleDxccNeededFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleDxccNeededFilter i').removeClass('fa-globe').addClass('fa-check-circle'); + } else { + $('#toggleDxccNeededFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleDxccNeededFilter i').removeClass('fa-check-circle').addClass('fa-globe'); + } + + // Contest button + if (additionalFlags.includes('Contest')) { + $('#toggleContextFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleContextFilter i').removeClass('fa-trophy').addClass('fa-check-circle'); + } else { + $('#toggleContextFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleContextFilter i').removeClass('fa-check-circle').addClass('fa-trophy'); + } + + // Geo Hunter button + let geoFlags = ['POTA', 'SOTA', 'IOTA', 'WWFF']; + let hasGeoFlag = geoFlags.some(flag => additionalFlags.includes(flag)); + if (hasGeoFlag) { + $('#toggleGeoHunterFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleGeoHunterFilter i').removeClass('fa-map-marked-alt').addClass('fa-check-circle'); + } else { + $('#toggleGeoHunterFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleGeoHunterFilter i').removeClass('fa-check-circle').addClass('fa-map-marked-alt'); + } + + // CW mode button + if (modeValues.includes('cw')) { + $('#toggleCwFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleCwFilter i').removeClass('fa-wave-square').addClass('fa-check-circle'); + } else { + $('#toggleCwFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleCwFilter i').removeClass('fa-check-circle').addClass('fa-wave-square'); + } + + // Digi mode button + if (modeValues.includes('digi')) { + $('#toggleDigiFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleDigiFilter i').removeClass('fa-keyboard').addClass('fa-check-circle'); + } else { + $('#toggleDigiFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleDigiFilter i').removeClass('fa-check-circle').addClass('fa-keyboard'); + } + + // Phone mode button + if (modeValues.includes('phone')) { + $('#togglePhoneFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#togglePhoneFilter i').removeClass('fa-microphone').addClass('fa-check-circle'); + } else { + $('#togglePhoneFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#togglePhoneFilter i').removeClass('fa-check-circle').addClass('fa-microphone'); + } + + // Check if "All" is selected for bands, modes, and continents + let allBandsSelected = bandValues.length === 1 && bandValues.includes('All'); + let allModesSelected = modeValues.length === 1 && modeValues.includes('All'); + let allContinentsSelected = decontValues.length === 1 && decontValues.includes('Any'); + + // Band filter buttons - green if All, orange if specific band, blue if not selected + let bandButtons = ['#toggle160mFilter', '#toggle80mFilter', '#toggle60mFilter', '#toggle40mFilter', '#toggle30mFilter', + '#toggle20mFilter', '#toggle17mFilter', '#toggle15mFilter', '#toggle12mFilter', '#toggle10mFilter', + '#toggle6mFilter', '#toggle4mFilter', '#toggle2mFilter', '#toggle70cmFilter', '#toggle23cmFilter']; + let bandIds = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m', '4m', '2m', '70cm', '23cm']; + + bandButtons.forEach((btnId, index) => { + let $btn = $(btnId); + $btn.removeClass('btn-primary btn-warning btn-success'); + if (allBandsSelected) { + $btn.addClass('btn-success'); + } else if (bandValues.includes(bandIds[index])) { + $btn.addClass('btn-warning'); + } else { + $btn.addClass('btn-primary'); + } + }); + + // Mode buttons - green if All, orange if selected, blue if not + let modeButtons = [ + { id: '#toggleCwFilter', mode: 'cw', icon: 'fa-wave-square' }, + { id: '#toggleDigiFilter', mode: 'digi', icon: 'fa-keyboard' }, + { id: '#togglePhoneFilter', mode: 'phone', icon: 'fa-microphone' } + ]; + + modeButtons.forEach(btn => { + let $btn = $(btn.id); + $btn.removeClass('btn-primary btn-warning btn-success'); + let $icon = $btn.find('i'); + + if (allModesSelected) { + $btn.addClass('btn-success'); + $icon.removeClass(btn.icon).addClass('fa-check-circle'); + } else if (modeValues.includes(btn.mode)) { + $btn.addClass('btn-warning'); + $icon.removeClass(btn.icon).addClass('fa-check-circle'); + } else { + $btn.addClass('btn-primary'); + $icon.removeClass('fa-check-circle').addClass(btn.icon); + } + }); + + // Continent filter buttons - green if Any, orange if selected, blue if not + let continentButtons = [ + { id: '#toggleAfricaFilter', continent: 'AF' }, + { id: '#toggleAsiaFilter', continent: 'AS' }, + { id: '#toggleEuropeFilter', continent: 'EU' }, + { id: '#toggleNorthAmericaFilter', continent: 'NA' }, + { id: '#toggleSouthAmericaFilter', continent: 'SA' } + ]; + + continentButtons.forEach(btn => { + let $btn = $(btn.id); + $btn.removeClass('btn-primary btn-warning btn-success'); + if (allContinentsSelected) { + $btn.addClass('btn-success'); + } else if (decontValues.includes(btn.continent)) { + $btn.addClass('btn-warning'); + } else { + $btn.addClass('btn-primary'); + } + }); + } + + // Add checkbox-style indicators (☑/☐) to multi-select dropdowns + function updateSelectCheckboxes(selectId) { + let $select = $('#' + selectId); + $select.find('option').each(function() { + let $option = $(this); + let originalText = $option.data('original-text'); + + if (!originalText) { + originalText = $option.text(); + $option.data('original-text', originalText); + } + + if ($option.is(':selected')) { + $option.text('☑ ' + originalText); + } else { + $option.text('☐ ' + originalText); + } + }); + } + + // Initialize checkbox indicators for all filter selects + function initFilterCheckboxes() { + ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags', 'requiredFlags'].forEach(function(selectId) { + updateSelectCheckboxes(selectId); + $('#' + selectId).on('change', function() { + updateSelectCheckboxes(selectId); + }); + }); + } + + // Handle "All"/"Any" option behavior in multi-selects + // If "All" is selected with other options, keep only "All" + // If nothing selected, default back to "All"/"Any" + function handleAllOption(selectId) { + $('#' + selectId).on('change', function() { + let selected = $(this).val() || []; + + if (selected.includes('All') || selected.includes('Any')) { + let allValue = selected.includes('All') ? 'All' : 'Any'; + if (selected.length > 1) { + $(this).val([allValue]); + } + } else if (selected.length === 0) { + let allValue = (selectId === 'decontSelect' || selectId === 'continentSelect') ? 'Any' : 'All'; + $(this).val([allValue]); + } + + updateFilterIcon(); + }); + } + + // Apply "All" handler to all filter dropdowns + ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags'].forEach(handleAllOption); + + // Required flags filter doesn't use "All" option - handle separately + $('#requiredFlags').on('change', function() { + updateFilterIcon(); + }); + + // ======================================== + // DATATABLE CONFIGURATION + // ======================================== + + // Sort spots by frequency (ascending) + function SortByQrg(a, b) { + return a.frequency - b.frequency; + } + + // Initialize DataTables instance with custom row click handlers + function get_dtable() { var table = $('.spottable').DataTable({ - "paging": false, - "searching": true, - "dom": 'rt', // Only show table (r) and processing (t), hide search box and other elements - "retrieve": true, - "language": { + paging: false, + searching: true, + dom: 'rt', + retrieve: true, + language: { url: getDataTablesLanguageUrl(), + "emptyTable": " Loading spots...", + "zeroRecords": "No spots found" }, 'columnDefs': [ { 'targets': 1, "type":"num", 'createdCell': function (td, cellData, rowData, row, col) { - $(td).addClass("kHz"); + $(td).addClass("MHz"); } }, { 'targets': 2, - 'createdCell': function (td, cellData, rowData, row, col) { - $(td).addClass("spotted_call"); - $(td).attr( "title", lang_click_to_prepare_logging); - } - }, - { - 'targets': 8, 'createdCell': function (td, cellData, rowData, row, col) { $(td).addClass("mode"); } - }, + } ], - "language": { - url: getDataTablesLanguageUrl(), - }, - "search": { "smart": true }, + search: { smart: true }, + drawCallback: function(settings) { + // Update status bar after table is drawn (including after search) + let totalRows = cachedSpotData ? cachedSpotData.length : 0; + let displayedRows = this.api().rows({ search: 'applied' }).count(); + updateStatusBar(totalRows, displayedRows, getServerFilterText(), getClientFilterText(), false, false); + } }); + + $('.spottable tbody').off('click', 'tr').on('click', 'tr', function(e) { + // Don't trigger row click if clicking on a link + if ($(e.target).is('a') || $(e.target).closest('a').length) { + // Handle activity flag links (POTA/SOTA/WWFF) + if ($(e.target).closest('a').hasClass('activity-flag-link')) { + let activityLink = $(e.target).closest('a.activity-flag-link'); + let activityType = activityLink.data('activity-type'); + let ref = activityLink.data('ref'); + + if (activityType === 'pota' && ref) { + window.open('https://pota.app/#/park/' + ref, '_blank'); + } else if (activityType === 'sota' && ref) { + window.open('https://sotl.as/summits/' + ref, '_blank'); + } else if (activityType === 'wwff' && ref) { + window.open('https://wwff.co/directory/?showRef=' + ref, '_blank'); + } + return; + } + return; + } + + let cellIndex = $(e.target).closest('td').index(); + + // If clicking callsign column, open QRZ link directly + if (cellIndex === 3) { + let rowData = table.row(this).data(); + if (!rowData) return; + + let callsignHtml = rowData[3]; + let tempDiv = $('
').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); + }); }); From d42673dad44a914ffc5ecc1dc903fac68f7333e2 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sat, 1 Nov 2025 13:11:05 +0100 Subject: [PATCH 03/51] Minor fixes: badges and column widths --- application/views/bandmap/list.php | 24 ++--- assets/js/sections/bandmap_list.js | 138 +++++++++++------------------ 2 files changed, 66 insertions(+), 96 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 85146f667..f827b11b9 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -102,15 +102,15 @@ } /* Column widths - consolidated selectors */ - .spottable th:nth-child(1), .spottable td:nth-child(1) { min-width: 60px; width: auto; } - .spottable th:nth-child(2), .spottable td:nth-child(2) { min-width: 75px; width: auto; } - .spottable th:nth-child(3), .spottable td:nth-child(3) { min-width: 55px; width: auto; } - .spottable th:nth-child(4), .spottable td:nth-child(4) { min-width: 90px; width: auto; } - .spottable th:nth-child(5), .spottable td:nth-child(5) { min-width: 120px; max-width: 250px; width: auto; } - .spottable th:nth-child(6), .spottable td:nth-child(6) { min-width: 45px; width: auto; } - .spottable th:nth-child(7), .spottable td:nth-child(7) { min-width: 80px; width: auto; } - .spottable th:nth-child(8), .spottable td:nth-child(8) { min-width: 80px; width: auto; } - .spottable th:nth-child(9), .spottable td:nth-child(9) { min-width: 150px; width: auto; } + .spottable th:nth-child(1), .spottable td:nth-child(1) { width: 60px; } /* Time HH:MM */ + .spottable th:nth-child(2), .spottable td:nth-child(2) { width: 130px; } /* Frequency (65px × 2) */ + .spottable th:nth-child(3), .spottable td:nth-child(3) { width: 60px; } /* Mode */ + .spottable th:nth-child(4), .spottable td:nth-child(4) { width: 120px; } /* Callsign (80px × 1.5) */ + .spottable th:nth-child(5), .spottable td:nth-child(5) { width: 40px; } /* Continent */ + .spottable th:nth-child(6), .spottable td:nth-child(6) { min-width: 100px; max-width: 216px; width: auto; } /* DXCC Entity (10% smaller: 112×0.9, 240×0.9) */ + .spottable th:nth-child(7), .spottable td:nth-child(7) { width: 120px; } /* Spotter */ + .spottable th:nth-child(8), .spottable td:nth-child(8) { width: 140px; } /* Flags (70px × 2) */ + .spottable th:nth-child(9), .spottable td:nth-child(9) { width: auto; } /* Message - fills remaining space */ .spottable td { overflow: hidden; @@ -146,7 +146,7 @@ display: inline-block; } - .spottable td:nth-child(7) { + .spottable td:nth-child(8) { overflow: visible; white-space: nowrap; } @@ -666,10 +666,10 @@ [MHz]"> "> "> - "> "> - "> + "> "> + "> "> diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index d02140800..0f31a2926 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -325,30 +325,13 @@ $(function() { } }); - $('.spottable tbody').off('click', 'tr').on('click', 'tr', function(e) { - // Don't trigger row click if clicking on a link - if ($(e.target).is('a') || $(e.target).closest('a').length) { - // Handle activity flag links (POTA/SOTA/WWFF) - if ($(e.target).closest('a').hasClass('activity-flag-link')) { - let activityLink = $(e.target).closest('a.activity-flag-link'); - let activityType = activityLink.data('activity-type'); - let ref = activityLink.data('ref'); + $('.spottable tbody').off('click', 'tr').on('click', 'tr', function(e) { + // Don't trigger row click if clicking on a link (LoTW, POTA, SOTA, WWFF, QRZ, etc.) + if ($(e.target).is('a') || $(e.target).closest('a').length) { + return; + } - if (activityType === 'pota' && ref) { - window.open('https://pota.app/#/park/' + ref, '_blank'); - } else if (activityType === 'sota' && ref) { - window.open('https://sotl.as/summits/' + ref, '_blank'); - } else if (activityType === 'wwff' && ref) { - window.open('https://wwff.co/directory/?showRef=' + ref, '_blank'); - } - return; - } - return; - } - - let cellIndex = $(e.target).closest('td').index(); - - // If clicking callsign column, open QRZ link directly + let cellIndex = $(e.target).closest('td').index(); // If clicking callsign column, open QRZ link directly if (cellIndex === 3) { let rowData = table.row(this).data(); if (!rowData) return; @@ -743,29 +726,30 @@ $(function() { lotw_badge = '' + buildBadge('success ' + lclass, '', lotw_title, 'L') + ''; } - // Build activity badges (POTA, SOTA, WWFF, IOTA, Contest, Worked) - let activity_flags = ''; + // 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.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'; + let pota_url = 'https://pota.app/#/park/' + single.dxcc_spotted.pota_ref; + 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.sota_ref) { + let sota_title = 'SOTA: ' + single.dxcc_spotted.sota_ref + ' - Click to view on SOTL.as'; + let sota_url = 'https://sotl.as/summits/' + single.dxcc_spotted.sota_ref; + 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) { + if (single.dxcc_spotted && single.dxcc_spotted.wwff_ref) { + let wwff_title = 'WWFF: ' + single.dxcc_spotted.wwff_ref + ' - Click to view on WWFF.co'; + let wwff_url = 'https://wwff.co/directory/?showRef=' + single.dxcc_spotted.wwff_ref; + 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); } @@ -784,12 +768,17 @@ $(function() { // 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") + // Time column: extract time portion from ISO datetime and format as HH:MM 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]; } + // Extract only HH:MM from HH:MM:SS + if (timeOnly.includes(':')) { + let timeParts = timeOnly.split(':'); + timeOnly = timeParts[0] + ':' + timeParts[1]; + } } data[0].push(timeOnly || ''); // Frequency column: convert kHz to MHz with 3 decimal places @@ -807,6 +796,19 @@ $(function() { wked_info = ((wked_info != '' ? '' : '') + qrzLink + (wked_info != '' ? '' : '')); var spotted = wked_info; data[0].push(spotted); + + // Continent column: color code based on worked/confirmed status (moved before DXCC) + 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); + // DXCC entity column: flag emoji + entity name with color coding let dxcc_entity_full = single.dxcc_spotted.entity; if (single.dxcc_spotted.flag) { @@ -817,26 +819,14 @@ $(function() { } 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); + // Spotter column data[0].push(single.spotter); + + // Flags column: combine LoTW and activity badges + let flags_column = lotw_badge + activity_flags; + data[0].push(flags_column); + + // Message column data[0].push(single.message || ''); // Add row to table (with "fresh" class for new spots animation) @@ -1012,7 +1002,7 @@ $(function() { // 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 margin = isLast ? '0' : '0 2px 0 0'; const fontSize = text ? '0.7rem' : '0.65rem'; const content = text ? text : ''; return '' + content + ''; @@ -1243,7 +1233,7 @@ $(function() { let ready_listener = true; try { - irrelevant=fetch(CatCallbackURL + '/'+qrg+'/'+mode).catch(() => { + let irrelevant = fetch(CatCallbackURL + '/'+qrg+'/'+mode).catch(() => { openedWindow = window.open(CatCallbackURL + '/' + qrg + '/' + mode); openedWindow.close(); }); @@ -1313,26 +1303,6 @@ $(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; From e7264eb460c5d4eb55f98e35cfc99577a871a928 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sat, 1 Nov 2025 13:13:49 +0100 Subject: [PATCH 04/51] Fixed DXCC overlay --- application/views/bandmap/list.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index f827b11b9..4c9ad40f9 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -234,6 +234,16 @@ position: fixed; } + /* Ensure modals appear above fullscreen mode */ + body.fullscreen-active .modal, + body.fullscreen-active .modal-backdrop { + z-index: 10050 !important; + } + + body.fullscreen-active .modal-backdrop + .modal { + z-index: 10051 !important; + } + body.fullscreen-active #page-wrapper, body.fullscreen-active nav, body.fullscreen-active .navbar, From e3292b67dc84993e49a8d1df9397740f8854f88d Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sat, 1 Nov 2025 21:43:19 +0100 Subject: [PATCH 05/51] UI prototype update --- application/models/Dxcluster_model.php | 2 + application/views/bandmap/list.php | 143 ++++++--- assets/js/sections/bandmap_list.js | 421 ++++++++++++++++++------- 3 files changed, 410 insertions(+), 156 deletions(-) diff --git a/application/models/Dxcluster_model.php b/application/models/Dxcluster_model.php index e10787f05..a09bfbe39 100644 --- a/application/models/Dxcluster_model.php +++ b/application/models/Dxcluster_model.php @@ -110,6 +110,7 @@ class Dxcluster_model extends CI_Model { $singlespot->dxcc_spotted = (object)[ 'dxcc_id' => $dxcc['adif'], 'cont' => $dxcc['cont'], + 'cqz' => $dxcc['cqz'] ?? '', 'flag' => '', 'entity' => $dxcc['entity'] ]; @@ -119,6 +120,7 @@ class Dxcluster_model extends CI_Model { $singlespot->dxcc_spotter = (object)[ 'dxcc_id' => $dxcc['adif'], 'cont' => $dxcc['cont'], + 'cqz' => $dxcc['cqz'] ?? '', 'flag' => '', 'entity' => $dxcc['entity'] ]; diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 4c9ad40f9..ca7ebe963 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -35,7 +35,7 @@ cursor: pointer !important; } - .spottable tbody tr td:nth-child(4) a { + .spottable tbody tr td:nth-child(5) a { text-decoration: underline !important; cursor: pointer !important; } @@ -102,15 +102,26 @@ } /* Column widths - consolidated selectors */ - .spottable th:nth-child(1), .spottable td:nth-child(1) { width: 60px; } /* Time HH:MM */ - .spottable th:nth-child(2), .spottable td:nth-child(2) { width: 130px; } /* Frequency (65px × 2) */ - .spottable th:nth-child(3), .spottable td:nth-child(3) { width: 60px; } /* Mode */ - .spottable th:nth-child(4), .spottable td:nth-child(4) { width: 120px; } /* Callsign (80px × 1.5) */ - .spottable th:nth-child(5), .spottable td:nth-child(5) { width: 40px; } /* Continent */ - .spottable th:nth-child(6), .spottable td:nth-child(6) { min-width: 100px; max-width: 216px; width: auto; } /* DXCC Entity (10% smaller: 112×0.9, 240×0.9) */ - .spottable th:nth-child(7), .spottable td:nth-child(7) { width: 120px; } /* Spotter */ - .spottable th:nth-child(8), .spottable td:nth-child(8) { width: 140px; } /* Flags (70px × 2) */ - .spottable th:nth-child(9), .spottable td:nth-child(9) { width: auto; } /* Message - fills remaining space */ + .spottable th:nth-child(1), .spottable td:nth-child(1) { width: 50px; } /* Age (minutes) */ + .spottable th:nth-child(2), .spottable td:nth-child(2) { width: 50px; } /* Band */ + .spottable th:nth-child(3), .spottable td:nth-child(3) { width: 90px; } /* Frequency */ + .spottable th:nth-child(4), .spottable td:nth-child(4) { width: 60px; } /* Mode */ + .spottable th:nth-child(5), .spottable td:nth-child(5) { width: 120px; } /* Callsign */ + .spottable th:nth-child(6), .spottable td:nth-child(6) { width: 40px; } /* Continent */ + .spottable th:nth-child(7), .spottable td:nth-child(7) { width: 45px; } /* CQ Zone */ + .spottable th:nth-child(8), .spottable td:nth-child(8) { width: 50px; } /* Flag */ + .spottable th:nth-child(9), .spottable td:nth-child(9) { width: 150px; } /* Entity (DXCC name) */ + .spottable th:nth-child(10), .spottable td:nth-child(10) { width: 60px; } /* DXCC Number */ + .spottable th:nth-child(11), .spottable td:nth-child(11) { width: 120px; } /* de Callsign (Spotter) */ + .spottable th:nth-child(12), .spottable td:nth-child(12) { width: 50px; } /* de Cont */ + .spottable th:nth-child(13), .spottable td:nth-child(13) { width: 50px; } /* de CQZ */ + .spottable th:nth-child(14), .spottable td:nth-child(14) { width: 110px; } /* Special (LoTW, POTA, etc) */ + .spottable th:nth-child(15), .spottable td:nth-child(15) { min-width: 100px; width: 100%; } /* Message - fills remaining space */ + + /* Hidden class for responsive columns (controlled by JavaScript) */ + .spottable .column-hidden { + display: none !important; + } .spottable td { overflow: hidden; @@ -118,14 +129,14 @@ white-space: nowrap; } - .spottable td:nth-child(9) { + .spottable td:nth-child(15) { white-space: normal; word-wrap: break-word; overflow-wrap: break-word; font-size: calc(1rem - 4px); } - .spottable td:nth-child(5) { + .spottable td:nth-child(6), .spottable td:nth-child(12) { font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', Arial, sans-serif; overflow: hidden; text-overflow: ellipsis; @@ -146,11 +157,25 @@ display: inline-block; } - .spottable td:nth-child(8) { + .spottable td:nth-child(14) { overflow: visible; white-space: nowrap; } + /* Responsive: On smallest screens, Entity column fills remaining space */ + @media (max-width: 500px) { + .spottable { + table-layout: auto !important; + } + .spottable th:nth-child(9), .spottable td:nth-child(9) { + width: auto !important; + min-width: 150px !important; + } + .spottable th:nth-child(1), .spottable td:nth-child(1) { width: 50px !important; } /* Age */ + .spottable th:nth-child(3), .spottable td:nth-child(3) { width: 90px !important; } /* Frequency */ + .spottable th:nth-child(5), .spottable td:nth-child(5) { width: 100px !important; } /* Callsign */ + } + .spottable thead th { font-size: calc(1rem - 1px); vertical-align: middle; @@ -210,15 +235,14 @@ min-height: 0; display: flex; flex-direction: column; - overflow: hidden; + overflow: auto; padding: 0.5rem; } .bandmap-fullscreen .table-responsive { - flex: 1 1 0; + flex: 1 1 auto; min-height: 0; overflow: auto; - height: auto; } .bandmap-fullscreen .dataTables_processing { @@ -234,6 +258,12 @@ position: fixed; } + /* Ensure tooltips appear above fullscreen mode */ + body.fullscreen-active .tooltip, + .bandmap-fullscreen .tooltip { + z-index: 10003 !important; + } + /* Ensure modals appear above fullscreen mode */ body.fullscreen-active .modal, body.fullscreen-active .modal-backdrop { @@ -560,16 +590,17 @@ - - -
+ + +
@@ -618,37 +649,43 @@
- - +
+ + +
- - -
- - + + + +
" aria-label=""> @@ -672,14 +709,20 @@ - + + - - - - - - + + + + + + + + + + + diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 0f31a2926..0a3f8bab4 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -126,6 +126,15 @@ $(function() { $('#toggleGeoHunterFilter i').removeClass('fa-check-circle').addClass('fa-map-marked-alt'); } + // Fresh filter button + if (additionalFlags.includes('Fresh')) { + $('#toggleFreshFilter').removeClass('btn-primary').addClass('btn-warning'); + $('#toggleFreshFilter i').removeClass('fa-bolt').addClass('fa-check-circle'); + } else { + $('#toggleFreshFilter').removeClass('btn-warning').addClass('btn-primary'); + $('#toggleFreshFilter i').removeClass('fa-check-circle').addClass('fa-bolt'); + } + // CW mode button if (modeValues.includes('cw')) { $('#toggleCwFilter').removeClass('btn-primary').addClass('btn-warning'); @@ -304,13 +313,14 @@ $(function() { }, 'columnDefs': [ { - 'targets': 1, "type":"num", + 'targets': 2, // Frequency is now column 3 (0-indexed = 2) + "type":"num", 'createdCell': function (td, cellData, rowData, row, col) { $(td).addClass("MHz"); } }, { - 'targets': 2, + 'targets': 3, // Mode column is now column 4 (0-indexed = 3) 'createdCell': function (td, cellData, rowData, row, col) { $(td).addClass("mode"); } @@ -331,35 +341,36 @@ $(function() { return; } - let cellIndex = $(e.target).closest('td').index(); // If clicking callsign column, open QRZ link directly - if (cellIndex === 3) { - let rowData = table.row(this).data(); - if (!rowData) return; - - let callsignHtml = rowData[3]; - let tempDiv = $('
').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 cellIndex = $(e.target).closest('td').index(); + // If clicking callsign column (column 5, 0-indexed = 4), open QRZ link directly + if (cellIndex === 4) { let rowData = table.row(this).data(); if (!rowData) return; - let callsignHtml = rowData[3]; + let callsignHtml = rowData[4]; let tempDiv = $('
').html(callsignHtml); - let call = tempDiv.find('a').text().trim(); - if (!call) return; + let qrzLink = tempDiv.find('a'); - let qrg = parseFloat(rowData[1]) * 1000; - let mode = rowData[2]; + if (qrzLink.length) { + qrzLink[0].click(); + return; + } + } - prepareLogging(call, qrg, mode); - }); + // Default row click: prepare QSO logging with callsign, frequency, mode + let rowData = table.row(this).data(); + if (!rowData) return; + + let callsignHtml = rowData[4]; // Callsign is column 5 (0-indexed = 4) + let tempDiv = $('
').html(callsignHtml); + let call = tempDiv.find('a').text().trim(); + if (!call) return; + + let qrg = parseFloat(rowData[2]) * 1000; // Frequency is now column 3 (0-indexed = 2) + let mode = rowData[3]; // Mode is column 4 (0-indexed = 3) + + prepareLogging(call, qrg, mode); + }); return table; } @@ -660,7 +671,7 @@ $(function() { } if (!passesModeFilter) return; - // Apply additional flags filter (POTA, SOTA, WWFF, IOTA, Contest) + // Apply additional flags filter (POTA, SOTA, WWFF, IOTA, Contest, Fresh) let passesFlagsFilter = flags.includes('All'); if (!passesFlagsFilter) { for (let flag of flags) { @@ -684,6 +695,10 @@ $(function() { passesFlagsFilter = true; break; } + if (flag === 'Fresh' && (single.age || 0) < 5) { + passesFlagsFilter = true; + break; + } } } if (!passesFlagsFilter) return; @@ -711,25 +726,23 @@ $(function() { 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 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) { + // 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 + ')'; @@ -753,81 +766,99 @@ $(function() { 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.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; - } + // Add "Fresh" badge for spots less than 5 minutes old + let ageMinutesCheck = single.age || 0; + let isFresh = ageMinutesCheck < 5; + + 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); + // isLast is true only if fresh badge won't be added + activity_flags += buildBadge(worked_badge_type, 'fa-check-circle', worked_title, null, !isFresh); } - // Build table row array - data[0] = []; - // Time column: extract time portion from ISO datetime and format as HH:MM - 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]; - } - // Extract only HH:MM from HH:MM:SS - if (timeOnly.includes(':')) { - let timeParts = timeOnly.split(':'); - timeOnly = timeParts[0] + ':' + timeParts[1]; - } - } - data[0].push(timeOnly || ''); + if (isFresh) { + activity_flags += buildBadge('danger', 'fa-bolt', 'Fresh spot (< 5 minutes old)', null, true); + } // Build table row array + data[0] = []; // Age column: show age in minutes with auto-update attribute + let ageMinutes = single.age || 0; + let spotTimestamp = single.when ? new Date(single.when).getTime() : Date.now(); + data[0].push('' + ageMinutes + ''); + + // Band column: show band designation + data[0].push(single.band || ''); + // 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 + data[0].push(freqMHz); - 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); + // 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); + // 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); - // Continent column: color code based on worked/confirmed status (moved before DXCC) - 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); + // 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); - // 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 + ''); + // CQ Zone column: show CQ Zone (moved here, right after Cont) + data[0].push(single.dxcc_spotted.cqz || ''); - // Spotter column - data[0].push(single.spotter); + // Flag column: just the flag emoji without entity name + let flag_only = ''; + if (single.dxcc_spotted.flag) { + flag_only = '' + single.dxcc_spotted.flag + ''; + } + data[0].push(flag_only); - // Flags column: combine LoTW and activity badges - let flags_column = lotw_badge + activity_flags; - data[0].push(flags_column); + // Entity column: entity name with color coding (no flag) + let dxcc_entity_full = single.dxcc_spotted.entity; + let entity_colored = (dxcc_wked_info != '' ? '' : '') + single.dxcc_spotted.entity + (dxcc_wked_info != '' ? '' : ''); + data[0].push('' + entity_colored + ''); - // Message column - data[0].push(single.message || ''); + // DXCC Number column: show ADIF DXCC entity number with color coding + let dxcc_number = ((dxcc_wked_info != '' ? '' : '') + single.dxcc_spotted.dxcc_id + (dxcc_wked_info != '' ? '' : '')); + data[0].push(dxcc_number); + + // de Callsign column (Spotter) - clickable QRZ link + let spotterQrzLink = '' + single.spotter + ''; + data[0].push(spotterQrzLink); + + // de Cont column: spotter's continent + data[0].push(single.dxcc_spotter.cont || ''); + + // de CQZ column: spotter's CQ Zone + data[0].push(single.dxcc_spotter.cqz || ''); + + // Special column: combine LoTW and activity badges + let flags_column = lotw_badge + activity_flags; + data[0].push(flags_column); + + // Message column + data[0].push(single.message || ''); // Add row to table (with "fresh" class for new spots animation) if (oldtable.length > 0) { @@ -1159,6 +1190,31 @@ $(function() { $('#filterDropdown').dropdown('hide'); }); + // Clear Filters Quick Button (preserves De Continent) + $("#clearFiltersButtonQuick").on("click", function() { + // Preserve current De Continent selection + let currentDecont = $('#decontSelect').val(); + + // Reset all other filters + $('#cwnSelect').val(['All']).trigger('change'); + $('#continentSelect').val(['Any']).trigger('change'); + $('#band').val(['All']).trigger('change'); + $('#mode').val(['All']).trigger('change'); + $('#additionalFlags').val(['All']).trigger('change'); + $('#requiredFlags').val([]).trigger('change'); + + // Restore De Continent + $('#decontSelect').val(currentDecont).trigger('change'); + + // Clear text search + $('#spotSearchInput').val(''); + table.search('').draw(); + + syncQuickFilterButtons(); + updateFilterIcon(); + applyFilters(false); // Don't refetch from server since De Continent is preserved + }); + // Sync button states when dropdown is shown $('#filterDropdown').on('show.bs.dropdown', function() { syncQuickFilterButtons(); @@ -1986,4 +2042,157 @@ $(function() { applyFilters(false); }); + // Toggle Fresh filter (< 5 minutes) + $('#toggleFreshFilter').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('Fresh')) { + // Remove Fresh filter + currentValues = currentValues.filter(v => v !== 'Fresh'); + if (currentValues.length === 0) currentValues = ['All']; + btn.removeClass('btn-warning').addClass('btn-primary'); + btn.find('i').removeClass('fa-check-circle').addClass('fa-bolt'); + } else { + // Add Fresh filter + currentValues.push('Fresh'); + btn.removeClass('btn-primary').addClass('btn-warning'); + btn.find('i').removeClass('fa-bolt').addClass('fa-check-circle'); + } + + $('#additionalFlags').val(currentValues).trigger('change'); + applyFilters(false); + }); + + // ======================================== + // RESPONSIVE COLUMN VISIBILITY + // ======================================== + + /** + * Handle responsive column visibility based on available table width + * Dynamically shows/hides columns to optimize space usage + * + * Column indices (0-based): + * 0: Age, 1: Band, 2: Frequency, 3: Mode, 4: Callsign, 5: Continent, 6: CQZ, + * 7: Flag, 8: Entity, 9: DXCC, 10: de Callsign, 11: de Cont, 12: de CQZ, + * 13: Special, 14: Message + * + * Breakpoints: + * - Full screen or > 1374px: Show all columns + * - <= 1374px: Hide DXCC (9), CQZ (6), de CQZ (12) + * - <= 1294px: Additionally hide Band (1), Cont (5), de Cont (11) + * - <= 1024px: Additionally hide Flag (7) + * - <= 500px: Show only Age (0), Freq (2), Callsign (4), Entity (8) + */ + function handleResponsiveColumns() { + const tableContainer = $('.table-responsive'); + if (!tableContainer.length) return; + + const containerWidth = tableContainer.width(); + + // Check if in fullscreen mode + const isFullscreen = $('#bandmapContainer').hasClass('bandmap-fullscreen'); + + // Reset all columns to visible first + $('.spottable th, .spottable td').removeClass('column-hidden'); + + // If fullscreen, show all columns and exit + if (isFullscreen) { + if ($.fn.DataTable && $.fn.DataTable.isDataTable('.spottable')) { + $('.spottable').DataTable().columns.adjust(); + } + return; + } + + // Apply visibility rules based on container width + if (containerWidth <= 500) { + // Show only Age, Freq, Callsign, Entity + $('.spottable th:nth-child(2), .spottable td:nth-child(2)').addClass('column-hidden'); // Band + $('.spottable th:nth-child(4), .spottable td:nth-child(4)').addClass('column-hidden'); // Mode + $('.spottable th:nth-child(6), .spottable td:nth-child(6)').addClass('column-hidden'); // Continent + $('.spottable th:nth-child(7), .spottable td:nth-child(7)').addClass('column-hidden'); // CQZ + $('.spottable th:nth-child(8), .spottable td:nth-child(8)').addClass('column-hidden'); // Flag + $('.spottable th:nth-child(10), .spottable td:nth-child(10)').addClass('column-hidden'); // DXCC + $('.spottable th:nth-child(11), .spottable td:nth-child(11)').addClass('column-hidden'); // de Callsign + $('.spottable th:nth-child(12), .spottable td:nth-child(12)').addClass('column-hidden'); // de Cont + $('.spottable th:nth-child(13), .spottable td:nth-child(13)').addClass('column-hidden'); // de CQZ + $('.spottable th:nth-child(14), .spottable td:nth-child(14)').addClass('column-hidden'); // Special + $('.spottable th:nth-child(15), .spottable td:nth-child(15)').addClass('column-hidden'); // Message + } else if (containerWidth <= 1024) { + // Hide: DXCC, CQZ, de CQZ, Band, Cont, de Cont, Flag + $('.spottable th:nth-child(2), .spottable td:nth-child(2)').addClass('column-hidden'); // Band + $('.spottable th:nth-child(6), .spottable td:nth-child(6)').addClass('column-hidden'); // Continent + $('.spottable th:nth-child(7), .spottable td:nth-child(7)').addClass('column-hidden'); // CQZ + $('.spottable th:nth-child(8), .spottable td:nth-child(8)').addClass('column-hidden'); // Flag + $('.spottable th:nth-child(10), .spottable td:nth-child(10)').addClass('column-hidden'); // DXCC + $('.spottable th:nth-child(12), .spottable td:nth-child(12)').addClass('column-hidden'); // de Cont + $('.spottable th:nth-child(13), .spottable td:nth-child(13)').addClass('column-hidden'); // de CQZ + } else if (containerWidth <= 1294) { + // Hide: DXCC, CQZ, de CQZ, Band, Cont, de Cont + $('.spottable th:nth-child(2), .spottable td:nth-child(2)').addClass('column-hidden'); // Band + $('.spottable th:nth-child(6), .spottable td:nth-child(6)').addClass('column-hidden'); // Continent + $('.spottable th:nth-child(7), .spottable td:nth-child(7)').addClass('column-hidden'); // CQZ + $('.spottable th:nth-child(10), .spottable td:nth-child(10)').addClass('column-hidden'); // DXCC + $('.spottable th:nth-child(12), .spottable td:nth-child(12)').addClass('column-hidden'); // de Cont + $('.spottable th:nth-child(13), .spottable td:nth-child(13)').addClass('column-hidden'); // de CQZ + } else if (containerWidth <= 1374) { + // Hide: DXCC, CQZ, de CQZ + $('.spottable th:nth-child(7), .spottable td:nth-child(7)').addClass('column-hidden'); // CQZ + $('.spottable th:nth-child(10), .spottable td:nth-child(10)').addClass('column-hidden'); // DXCC + $('.spottable th:nth-child(13), .spottable td:nth-child(13)').addClass('column-hidden'); // de CQZ + } + // else: containerWidth > 1374 - show all columns (already reset above) + + // Adjust DataTable columns if initialized + if ($.fn.DataTable && $.fn.DataTable.isDataTable('.spottable')) { + $('.spottable').DataTable().columns.adjust(); + } + } + + // Initialize ResizeObserver to watch for container size changes + if (typeof ResizeObserver !== 'undefined') { + const tableContainer = document.querySelector('.table-responsive'); + if (tableContainer) { + const resizeObserver = new ResizeObserver(function(entries) { + handleResponsiveColumns(); + }); + resizeObserver.observe(tableContainer); + } + } else { + // Fallback for browsers without ResizeObserver support + $(window).on('resize', function() { + handleResponsiveColumns(); + }); + } + + // Initial call to set up column visibility + handleResponsiveColumns(); + + // ======================================== + // AGE AUTO-UPDATE + // ======================================== + + /** + * Update spot ages every minute without full table refresh + * Ages are calculated from the spot timestamp stored in data attribute + */ + function updateSpotAges() { + const now = Date.now(); + $('.spot-age').each(function() { + const spotTime = parseInt($(this).attr('data-spot-time')); + if (spotTime) { + const ageMinutes = Math.floor((now - spotTime) / 60000); + $(this).text(ageMinutes); + } + }); + } + + // Update ages every 60 seconds + setInterval(updateSpotAges, 60000); + }); From 1c43f7a2c4f93113a0ec0f92cf59a8a705958716 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sat, 1 Nov 2025 22:30:33 +0100 Subject: [PATCH 06/51] Very basic CAT tracking --- application/views/bandmap/list.php | 104 ++++++++++++++++++----------- assets/js/sections/bandmap_list.js | 68 +++++++++++++------ 2 files changed, 113 insertions(+), 59 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index ca7ebe963..ba85628f4 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -103,12 +103,12 @@ /* Column widths - consolidated selectors */ .spottable th:nth-child(1), .spottable td:nth-child(1) { width: 50px; } /* Age (minutes) */ - .spottable th:nth-child(2), .spottable td:nth-child(2) { width: 50px; } /* Band */ + .spottable th:nth-child(2), .spottable td:nth-child(2) { width: 53px; } /* Band */ .spottable th:nth-child(3), .spottable td:nth-child(3) { width: 90px; } /* Frequency */ .spottable th:nth-child(4), .spottable td:nth-child(4) { width: 60px; } /* Mode */ .spottable th:nth-child(5), .spottable td:nth-child(5) { width: 120px; } /* Callsign */ .spottable th:nth-child(6), .spottable td:nth-child(6) { width: 40px; } /* Continent */ - .spottable th:nth-child(7), .spottable td:nth-child(7) { width: 45px; } /* CQ Zone */ + .spottable th:nth-child(7), .spottable td:nth-child(7) { width: 50px; } /* CQ Zone */ .spottable th:nth-child(8), .spottable td:nth-child(8) { width: 50px; } /* Flag */ .spottable th:nth-child(9), .spottable td:nth-child(9) { width: 150px; } /* Entity (DXCC name) */ .spottable th:nth-child(10), .spottable td:nth-child(10) { width: 60px; } /* DXCC Number */ @@ -162,6 +162,17 @@ white-space: nowrap; } + /* Center alignment for specific columns */ + .spottable th:nth-child(6), .spottable td:nth-child(6), /* Continent (spotted) */ + .spottable th:nth-child(7), .spottable td:nth-child(7), /* CQ Zone (spotted) */ + .spottable th:nth-child(8), .spottable td:nth-child(8), /* Flag */ + .spottable th:nth-child(10), .spottable td:nth-child(10), /* DXCC Number */ + .spottable th:nth-child(12), .spottable td:nth-child(12), /* de Cont (spotter) */ + .spottable th:nth-child(13), .spottable td:nth-child(13) /* de CQZ (spotter) */ + { + text-align: center; + } + /* Responsive: On smallest screens, Entity column fills remaining space */ @media (max-width: 500px) { .spottable { @@ -308,6 +319,11 @@ margin-bottom: 5px; } + /* Ensure CAT tracking and search box stay right-aligned when wrapping */ + .menu-bar > div:last-child { + justify-content: flex-end; + } + .card-body.pt-1 { padding-left: 15px; padding-right: 15px; @@ -480,15 +496,14 @@
- - - - - + + + + + diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 0a3f8bab4..b980a9b2f 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -324,6 +324,10 @@ $(function() { 'createdCell': function (td, cellData, rowData, row, col) { $(td).addClass("mode"); } + }, + { + 'targets': [5, 6, 7, 11, 12, 13, 14], // Cont, CQZ, Flag, de Cont, de CQZ, Special, Message - disable sorting + 'orderable': false } ], search: { smart: true }, @@ -481,10 +485,10 @@ $(function() { } 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'); + tooltipLines.push('Band: ' + (lastFetchParams.band || 'All')); + tooltipLines.push('Continent: ' + (lastFetchParams.continent || 'All')); + tooltipLines.push('Mode: ' + (lastFetchParams.mode || 'All')); + tooltipLines.push('Max Age: ' + (lastFetchParams.maxAge || '120') + ' min'); if (lastFetchParams.timestamp) { let fetchTime = new Date(lastFetchParams.timestamp); let fetchTimeStr = fetchTime.getHours().toString().padStart(2, '0') + ':' + @@ -1281,6 +1285,7 @@ $(function() { let bc2qso = new BroadcastChannel('qso_wish'); var CatCallbackURL = "http://127.0.0.1:54321"; + var isCatTrackingEnabled = false; // Track CAT tracking button state let wait4pong = 2000; let check_intv = 100; @@ -1422,7 +1427,7 @@ $(function() { websocketEnabled = false; } if ($("#radio option:selected").val() == '0') { - $(".radio_cat_state" ).remove(); + $('#radio_status').html(''); } else if ($("#radio option:selected").val() == 'ws') { initializeWebSocketConnection(); } else { @@ -1434,23 +1439,31 @@ $(function() { function updateCATui(data) { const band = frequencyToBand(data.frequency); CatCallbackURL=data.cat_url; - if (band !== $("#band").val()) { - $("#band").val(band); - $("#band").trigger("change"); + + console.log('CAT Update - Frequency:', data.frequency, 'Band:', band, 'Tracking enabled:', isCatTrackingEnabled, 'Current band filter:', $("#band").val()); + + // Only update band filter if CAT tracking is enabled + if (isCatTrackingEnabled) { + const currentBands = $("#band").val() || []; + // Check if current selection is not just this band + if (currentBands.length !== 1 || currentBands[0] !== band) { + console.log('Updating band filter to:', band); + $("#band").val([band]); + updateSelectCheckboxes('band'); + syncQuickFilterButtons(); + applyFilters(false); + } } const minutes = Math.floor(cat_timeout_interval / 60); if(data.updated_minutes_ago > minutes) { - $(".radio_cat_state" ).remove(); - if($('.radio_timeout_error').length == 0) { - $('.messages').prepend(''); - } else { - $('.radio_timeout_error').html('Radio connection timed-out: ' + $('select.radios option:selected').text() + ' data is ' + data.updated_minutes_ago + ' minutes old.'); + if ($('#radio_status').length) { + $('#radio_status').html(''); } } else { $(".radio_timeout_error" ).remove(); - text = 'TX: '+(Math.round(parseInt(data.frequency)/100)/10000).toFixed(4)+' MHz'; + var text = 'TX: '+(Math.round(parseInt(data.frequency)/100)/10000).toFixed(4)+' MHz'; highlight_current_qrg((parseInt(data.frequency))/1000); if(data.mode != null) { text = text+''+data.mode; @@ -1458,17 +1471,15 @@ $(function() { if(data.power != null && data.power != 0) { text = text+''+data.power+' W'; } - if (! $('#radio_cat_state').length) { - $('.messages').prepend(''); - } else { - $('#radio_cat_state').html(text); + if ($('#radio_status').length) { + $('#radio_status').html(''); } } } var updateFromCAT = function() { if($('select.radios option:selected').val() != '0') { - radioID = $('select.radios option:selected').val(); + var radioID = $('select.radios option:selected').val(); $.getJSON( base_url+"index.php/radio/json/" + radioID, function( data ) { if (data.error) { @@ -2069,6 +2080,25 @@ $(function() { applyFilters(false); }); + // Toggle CAT tracking + $('#toggleCatTracking').on('click', function() { + let btn = $(this); + + if (btn.hasClass('btn-warning')) { + // Disable CAT tracking + btn.removeClass('btn-warning').addClass('btn-primary'); + btn.find('i').removeClass('fa-check-circle').addClass('fa-radio'); + isCatTrackingEnabled = false; + console.log('CAT Tracking disabled'); + } else { + // Enable CAT tracking + btn.removeClass('btn-primary').addClass('btn-warning'); + btn.find('i').removeClass('fa-radio').addClass('fa-check-circle'); + isCatTrackingEnabled = true; + console.log('CAT Tracking enabled'); + } + }); + // ======================================== // RESPONSIVE COLUMN VISIBILITY // ======================================== From af13d4a3168d54a30838d93fe7b99183489d5143 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sun, 2 Nov 2025 15:05:07 +0100 Subject: [PATCH 07/51] CAT in line with rest of the code (using shared files now) --- application/controllers/Bandmap.php | 2 +- application/views/bandmap/list.php | 142 +++++++---- application/views/interface_assets/footer.php | 8 +- assets/js/cat.js | 90 ++++--- assets/js/sections/bandmap_list.js | 240 ++++++++---------- 5 files changed, 259 insertions(+), 223 deletions(-) diff --git a/application/controllers/Bandmap.php b/application/controllers/Bandmap.php index db75c0009..c233666f4 100644 --- a/application/controllers/Bandmap.php +++ b/application/controllers/Bandmap.php @@ -35,13 +35,13 @@ class Bandmap extends CI_Controller { $this->load->model('cat'); $this->load->model('bands'); $data['radios'] = $this->cat->radios(); + $data['radio_last_updated'] = $this->cat->last_updated()->row(); $data['bands'] = $this->bands->get_user_bands_for_qso_entry(); $footerData = []; $footerData['scripts'] = [ 'assets/js/moment.min.js', 'assets/js/datetime-moment.js', - 'assets/js/sections/bandmap_list.js' ]; // Get Date format diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index ba85628f4..eb920be15 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -10,6 +10,9 @@ var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; var modKey = isMac ? 'Cmd' : 'Ctrl'; var lang_click_to_prepare_logging = " (" + modKey + "+Click )"; + + // Enable compact radio status display for bandmap page + window.CAT_COMPACT_MODE = true; -
- -
- - -
- - -
@@ -493,6 +521,9 @@
+ +
+
- +
- + + + @@ -760,3 +799,4 @@
+ diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 68754d456..27ae03ca9 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1463,6 +1463,12 @@ mymap.on('mousemove', onQsoMapMove); + +uri->segment(1) == "bandmap" && $this->uri->segment(2) == "list") { ?> + + + + uri->segment(1) == "logbook" && $this->uri->segment(2) == "view") { ?> +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ + + + + - + +
+ + +
+ +
+ + + + + +
+
+ + +
+ + +
+ + + + +
+ + + +
+
@@ -565,9 +744,11 @@
- -
- + + +
+ +
@@ -575,213 +756,48 @@
- -
- -
- - - - - -
-
+
+ + +
+ +
+ +
+
- -
- -
- - - - - - -
- - + +
+ + - - - + + + +
-
- -
- - - - - - +
" aria-label="">
-
From 3bdd888b48d405f22b026ffcbeba0a7751812d14 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sun, 2 Nov 2025 18:38:57 +0100 Subject: [PATCH 14/51] Menu reworked and css extracted --- application/views/bandmap/list.php | 562 ++--------------------------- assets/css/bandmap_list.css | 485 +++++++++++++++++++++++++ 2 files changed, 525 insertions(+), 522 deletions(-) create mode 100644 assets/css/bandmap_list.css diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 8fd022982..d44dba90f 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -15,493 +15,7 @@ window.CAT_COMPACT_MODE = true; - +
@@ -526,12 +40,35 @@
- -
UTC"> UTC">"> [MHz]">">">">">">">">">">">">">">">">">"> ">
"> "> [MHz]">">">">">">">">">">"> "> "> ">
diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index ed6d8be18..42857e233 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -233,7 +233,7 @@ table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even), background-color: var(--bs-body-bg); border-radius: 8px; margin: 0.25rem 0; - padding: 10px 15px; + padding: 6px 12px; } .status-bar-inner { @@ -253,8 +253,9 @@ table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even), } .status-bar-right { - flex: 0 0 20%; - min-width: 150px; + flex: 0 0 auto; + min-width: 100px; + margin-left: 10px; font-weight: 500; color: var(--bs-secondary); text-align: right; From a9dcdec5bf6c74b932c17a637ed24d2b66c90019 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 00:14:20 +0100 Subject: [PATCH 25/51] Improved behavior at small screens --- assets/css/bandmap_list.css | 120 ++++++++++++++++++++++++++++- assets/js/sections/bandmap_list.js | 7 ++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index 42857e233..e31f837a3 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -584,6 +584,7 @@ body.fullscreen-active { .menu-bar { margin-top: 5px; margin-bottom: 5px; + overflow: visible; } /* Ensure CAT Control and search box stay right-aligned when wrapping */ @@ -591,6 +592,12 @@ body.fullscreen-active { justify-content: flex-end; } +/* Ensure all menu rows wrap properly and show all content */ +.menu-bar .d-flex { + overflow: visible; + min-height: fit-content; +} + .card-body.pt-1 { padding-left: 15px; padding-right: 15px; @@ -710,6 +717,54 @@ select[multiple] option:checked { min-height: 100px; max-height: 150px; } + + /* Status bar and search on smaller screens */ + .status-bar { + font-size: 0.8rem; + padding: 5px 10px; + } + + .status-bar-inner { + gap: 10px; + } + + .status-bar-right { + min-width: 80px; + font-size: 0.75rem; + } + + /* Ensure menu rows wrap properly */ + .menu-bar .d-flex { + overflow: visible !important; + } + + /* Reduce card-body padding on tablets */ + .card-body.pt-1 { + padding-left: 10px; + padding-right: 10px; + } + + /* Make buttons smaller on tablets */ + .btn-sm { + font-size: 0.75rem; + padding: 0.15rem 0.35rem; + } + + /* Hide text labels in quick filter buttons on tablets - icons only */ + #toggleLotwFilter .d-none.d-sm-inline, + #toggleNewContinentFilter .d-none.d-sm-inline, + #toggleDxccNeededFilter .d-none.d-sm-inline, + #toggleNewCallsignFilter .d-none.d-sm-inline, + #toggleContestFilter .d-none.d-sm-inline, + #toggleGeoHunterFilter .d-none.d-sm-inline, + #toggleFreshFilter .d-none.d-sm-inline { + display: none !important; + } + + /* Reduce gap between button groups */ + .menu-bar .d-flex { + gap: 0.3rem !important; + } } @media (max-width: 576px) { @@ -717,4 +772,67 @@ select[multiple] option:checked { min-height: 150px; max-height: 300px; } -} \ No newline at end of file + + /* Reduce card-body padding on mobile */ + .card-body.pt-1 { + padding-left: 5px; + padding-right: 5px; + } + + /* Stack status bar and search vertically on mobile */ + .menu-bar > div:last-child > div:first-child { + flex: 1 1 100% !important; + max-width: 100% !important; + margin-bottom: 0.5rem; + } + + .menu-bar > div:last-child .input-group { + flex: 1 1 100% !important; + max-width: 100% !important; + } + + /* Make buttons even smaller on mobile */ + .btn-sm { + font-size: 0.7rem; + padding: 0.1rem 0.25rem; + } + + /* Hide text labels on quick filter buttons - show icons only */ + .d-none.d-sm-inline { + display: none !important; + } + + /* Reduce gap between elements */ + .menu-bar .d-flex { + gap: 0.2rem !important; + } + + .menu-bar .d-flex.gap-2 { + gap: 0.2rem !important; + } + + /* Reduce button group spacing */ + .btn-group { + gap: 0px; + } + + .btn-group .btn { + margin: 0; + } + + /* Ensure all rows in menu wrap properly */ + .menu-bar > div { + overflow: visible !important; + min-height: auto !important; + } + + /* Reduce status bar font size further */ + .status-bar { + font-size: 0.7rem; + padding: 3px 6px; + } + + .status-bar-right { + margin-left: 5px; + } +} diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 29aa2e944..aa753cdc1 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1990,9 +1990,16 @@ $(function() { // Build message object with backward compatibility let message = { frequency: qrg, + frequency_rx: "", // Default empty for non-split operation call: call }; + // If radio is in split mode, include the RX frequency + if (window.lastCATData && window.lastCATData.frequency_rx) { + message.frequency_rx = window.lastCATData.frequency_rx; + console.log('Split mode detected, RX frequency:', message.frequency_rx); + } + // Add reference fields if available (backward compatible - only if spotData exists) if (spotData && spotData.dxcc_spotted) { console.log('Building message with spot data:', spotData.dxcc_spotted); From 6d5bb7b46eb9533f6ef133d5fbbcc63fd98a6c80 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 00:38:30 +0100 Subject: [PATCH 26/51] One band operation with CAT - server load reduced --- assets/js/sections/bandmap_list.js | 195 ++++++++++++++++++++++------- 1 file changed, 149 insertions(+), 46 deletions(-) diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index aa753cdc1..970fc8508 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -477,8 +477,10 @@ $(function() { // 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 + // UPDATE: Band becomes backend filter when CAT Control is active (single-band fetch mode) var loadedBackendFilters = { - continent: 'Any' + continent: 'Any', + band: 'All' }; // Initialize backend filter state from form values @@ -521,6 +523,12 @@ $(function() { return spot.spotted + '_' + spot.frequency + '_' + spot.spotter; } + // Extract frequency from spot key for band determination + function getFrequencyFromKey(key) { + let parts = key.split('_'); + return parseFloat(parts[1]); // frequency is the second part + } + // Auto-refresh timer state var refreshCountdown = SPOT_REFRESH_INTERVAL; var refreshTimerInterval = null; @@ -640,7 +648,17 @@ $(function() { console.log('Timer countdown: reloading spot data with current filters'); let table = get_dtable(); table.clear(); - fill_list(currentFilters.deContinent, dxcluster_maxage); + + // Determine band for API fetch based on CAT Control state and current filter + let bandForRefresh = 'All'; + if (isCatTrackingEnabled) { + let currentBand = currentFilters.band || []; + if (currentBand.length === 1 && !currentBand.includes('All') && currentBand[0] !== '') { + bandForRefresh = currentBand[0]; + } + } + + fill_list(currentFilters.deContinent, dxcluster_maxage, bandForRefresh); refreshCountdown = SPOT_REFRESH_INTERVAL; } else { if (!isFetchInProgress && lastFetchParams.timestamp !== null) { @@ -1153,9 +1171,21 @@ $(function() { // Update badge counts on band and mode filter buttons function updateBandCountBadges() { + // Check if we fetched only a specific band (single band fetch mode) + // This happens when CAT Control is active and limited the API fetch to current band + let fetchedBand = null; + if (loadedBackendFilters && loadedBackendFilters.band && loadedBackendFilters.band !== 'All') { + fetchedBand = loadedBackendFilters.band; + } + if (!cachedSpotData || cachedSpotData.length === 0) { // Clear all badges when no data - $('.band-count-badge, .mode-count-badge').text('0'); + if (fetchedBand) { + // Set all to "-" when in single band fetch mode but no data + $('.band-count-badge, .mode-count-badge').text('-'); + } else { + $('.band-count-badge, .mode-count-badge').text('0'); + } return; } @@ -1302,37 +1332,62 @@ $(function() { } }); - // Update individual MF/HF band button badges - const mfHfBands = [ - '160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m' - ]; + // Update individual MF/HF band button badges + const mfHfBands = [ + '160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m' + ]; - mfHfBands.forEach(band => { - let count = bandCounts[band] || 0; - let $badge = $('#toggle' + band + 'Filter .band-count-badge'); - if ($badge.length === 0) { - // Badge doesn't exist yet, create it - $('#toggle' + band + 'Filter').append(' ' + count + ''); - } else { - // Update existing badge - $badge.text(count); - } - }); + mfHfBands.forEach(band => { + let count; + let displayText; - // Update band group button badges (VHF, UHF, SHF) - ['VHF', 'UHF', 'SHF'].forEach(group => { - let count = groupCounts[group] || 0; - let $badge = $('#toggle' + group + 'Filter .band-count-badge'); - if ($badge.length === 0) { - // Badge doesn't exist yet, create it - $('#toggle' + group + 'Filter').append(' ' + count + ''); - } else { - // Update existing badge - $badge.text(count); - } - }); + // If in single band fetch mode and this is not the fetched band, show "-" + if (fetchedBand && band !== fetchedBand) { + displayText = '-'; + } else { + count = bandCounts[band] || 0; + displayText = count.toString(); + } - // Update mode button badges + let $badge = $('#toggle' + band + 'Filter .band-count-badge'); + if ($badge.length === 0) { + // Badge doesn't exist yet, create it + $('#toggle' + band + 'Filter').append(' ' + displayText + ''); + } else { + // Update existing badge + $badge.text(displayText); + } + }); + + // Update band group button badges (VHF, UHF, SHF) + ['VHF', 'UHF', 'SHF'].forEach(group => { + let count; + let displayText; + + // Check if fetched band is in this group + let isActiveGroup = false; + if (fetchedBand) { + let fetchedBandGroup = getBandGroup(fetchedBand); + isActiveGroup = (fetchedBandGroup === group); + } + + // If in single band fetch mode and this is not the fetched band's group, show "-" + if (fetchedBand && !isActiveGroup) { + displayText = '-'; + } else { + count = groupCounts[group] || 0; + displayText = count.toString(); + } + + let $badge = $('#toggle' + group + 'Filter .band-count-badge'); + if ($badge.length === 0) { + // Badge doesn't exist yet, create it + $('#toggle' + group + 'Filter').append(' ' + displayText + ''); + } else { + // Update existing badge + $badge.text(displayText); + } + }); // Update mode button badges const modeButtons = ['Cw', 'Digi', 'Phone']; modeButtons.forEach(mode => { let modeKey = mode.toLowerCase(); @@ -1452,7 +1507,7 @@ $(function() { // 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) { + function fill_list(de, maxAgeMinutes, bandForAPI = 'All') { var table = get_dtable(); // Normalize de continent parameter to array @@ -1460,21 +1515,28 @@ $(function() { 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) + // bandForAPI is now passed as a parameter from applyFilters() + // Log if CAT Control influenced the band selection + if (bandForAPI !== 'All') { + console.log('Fetching specific band from server:', bandForAPI); + } + + // Update backend filter state loadedBackendFilters = { - continent: continentForAPI + continent: continentForAPI, + band: bandForAPI }; lastFetchParams.continent = continentForAPI; + lastFetchParams.band = bandForAPI; 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"; + // Mode is always 'All' - filtering happens client-side + let dxurl = dxcluster_provider + "/spots/" + bandForAPI + "/" + maxAgeMinutes + "/" + continentForAPI + "/All"; console.log('Loading from backend: ' + dxurl); // Cancel any in-flight request before starting new one @@ -1500,6 +1562,7 @@ $(function() { dxspots.sort(SortByQrg); // Sort by frequency // TTL Management: Process new spots and update TTL values + // In single-band fetch mode, only update TTL for spots in the fetched band let newSpotKeys = new Set(); // First pass: identify all spots in the new data @@ -1508,20 +1571,36 @@ $(function() { newSpotKeys.add(key); }); - // Second pass: Update TTL for all existing spots - // - Decrement all TTL values by 1 + // Second pass: Update TTL for existing spots + // - In single-band mode (bandForAPI != 'All'), only decrement TTL for spots in the fetched band + // - In all-band mode, decrement all TTL values // - If spot exists in new data, set TTL back to 1 (stays valid) // - Remove spots with TTL < -1 let ttlStats = { stillValid: 0, expiring: 0, removed: 0, added: 0 }; let expiringSpots = []; // Store spots with TTL=0 that need to be shown for (let [key, ttl] of spotTTLMap.entries()) { - let newTTL = ttl - 1; // Decrement all spots + let newTTL = ttl; + + // Only decrement TTL if: + // - We fetched all bands (bandForAPI === 'All'), OR + // - This spot is in the band we just fetched + let shouldDecrementTTL = (bandForAPI === 'All'); + if (!shouldDecrementTTL) { + // Extract frequency from the spot key and determine its band + let spotFrequency = getFrequencyFromKey(key); + let spotBand = getBandFromFrequency(spotFrequency); + shouldDecrementTTL = (spotBand === bandForAPI); + } + + if (shouldDecrementTTL) { + newTTL = ttl - 1; // Decrement only if in scope of this fetch + } if (newSpotKeys.has(key)) { newTTL = 1; // Reset to 1 if spot still exists (keeps it valid) ttlStats.stillValid++; - } else { + } else if (shouldDecrementTTL) { if (newTTL === 0) { ttlStats.expiring++; // Find the spot in previous cachedSpotData to keep it for display @@ -1719,13 +1798,27 @@ $(function() { } // If multiple continents selected, fetch 'Any' from backend and filter client-side + // Determine if we're in single-band fetch mode (CAT Control active) + // In this mode, band changes require a new server fetch + // Use the selected band filter value when CAT is active and a single band is selected + let bandForAPI = 'All'; + let isSingleBandMode = false; + if (isCatTrackingEnabled) { + // Check if a single specific band is selected (not 'All') + if (band.length === 1 && !band.includes('All') && band[0] !== '') { + bandForAPI = band[0]; + isSingleBandMode = true; + } + } + console.log('applyFilters - Current backend filters:', loadedBackendFilters); - console.log('applyFilters - Requested backend params:', {continent: continentForAPI}); + console.log('applyFilters - Requested backend params:', {continent: continentForAPI, band: bandForAPI, singleBandMode: isSingleBandMode}); // Check if backend parameters changed (requires new data fetch) - // Only de continent affects backend now - band and mode are client-side only + // In single-band mode, band selection changes also require server fetch let backendParamsChanged = forceReload || - loadedBackendFilters.continent !== continentForAPI; + loadedBackendFilters.continent !== continentForAPI || + (isSingleBandMode && loadedBackendFilters.band !== bandForAPI); console.log('applyFilters - backendParamsChanged:', backendParamsChanged); @@ -1741,9 +1834,9 @@ $(function() { }; if (backendParamsChanged) { - console.log('Reloading from backend: continent=' + continentForAPI); + console.log('Reloading from backend: continent=' + continentForAPI + ' band=' + bandForAPI); table.clear(); - fill_list(de, dxcluster_maxage); + fill_list(de, dxcluster_maxage, bandForAPI); } else { console.log('Client-side filtering changed - using cached data'); renderFilteredSpots(); @@ -3296,6 +3389,16 @@ $(function() { // Unlock table sorting unlockTableSorting(); + + // Reset band filter to 'All' and fetch all bands + const currentBands = $("#band").val() || []; + if (currentBands.length !== 1 || currentBands[0] !== 'All') { + console.log('CAT Control disabled - resetting to all bands'); + $("#band").val(['All']); + updateSelectCheckboxes('band'); + syncQuickFilterButtons(); + applyFilters(true); // Force reload to fetch all bands + } } else { // Enable CAT Control btn.removeClass('btn-secondary').addClass('btn-success'); From 38ddf8edd646fe5f3da21a747ae4551103878711 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 00:57:04 +0100 Subject: [PATCH 27/51] Visual band indicator --- assets/css/bandmap_list.css | 34 +++++++++++++++++ assets/js/sections/bandmap_list.js | 61 ++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index e31f837a3..67802dde0 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -225,6 +225,40 @@ table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even), filter: brightness(0.95); } +/* CAT Control - Nearest spot indicators when not in gradient range */ +/* Table is sorted DESC (high freq at top, low freq at bottom) */ +/* Borders point TOWARD current frequency to create visual bracket */ + +/* Spot BELOW current frequency (lower number) appears at BOTTOM of table → TOP border points UP toward you */ +table.spottable tbody tr.cat-nearest-below, +table.dataTable tbody tr.cat-nearest-below, +table tbody tr.cat-nearest-below { + border-top: 4px solid #8b5cf6 !important; + box-shadow: 0 -4px 0 0 #8b5cf6 !important; +} + +/* Apply to td cells as well for border-collapse tables */ +table.spottable tbody tr.cat-nearest-below td, +table.dataTable tbody tr.cat-nearest-below td, +table tbody tr.cat-nearest-below td { + border-top: 4px solid #8b5cf6 !important; +} + +/* Spot ABOVE current frequency (higher number) appears at TOP of table → BOTTOM border points DOWN toward you */ +table.spottable tbody tr.cat-nearest-above, +table.dataTable tbody tr.cat-nearest-above, +table tbody tr.cat-nearest-above { + border-bottom: 4px solid #8b5cf6 !important; + box-shadow: 0 4px 0 0 #8b5cf6 !important; +} + +/* Apply to td cells as well for border-collapse tables */ +table.spottable tbody tr.cat-nearest-above td, +table.dataTable tbody tr.cat-nearest-above td, +table tbody tr.cat-nearest-above td { + border-bottom: 4px solid #8b5cf6 !important; +} + /* Status bar styling */ .status-bar { font-size: 0.875rem; diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 970fc8508..a507d4497 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1154,6 +1154,11 @@ $(function() { // Update band count badges after rendering updateBandCountBadges(); + // Update CAT frequency gradient colors/borders after rendering if CAT is enabled + if (isCatTrackingEnabled && currentRadioFrequency) { + updateFrequencyGradientColors(); + } + // Update status bar after render completes setTimeout(function() { if (!isFetchInProgress) { @@ -2268,6 +2273,10 @@ $(function() { lastGradientFrequency = currentRadioFrequency; var table = get_dtable(); let coloredCount = 0; + let nearestAbove = null; // Spot above current frequency + let nearestBelow = null; // Spot below current frequency + let minDistanceAbove = Infinity; + let minDistanceBelow = Infinity; // Iterate through all visible rows table.rows({ search: 'applied' }).every(function() { @@ -2286,6 +2295,16 @@ $(function() { // Store gradient data for persistence $(row).attr('data-spot-frequency', spotFreqKhz); + // Track nearest spots above and below current frequency + const distance = spotFreqKhz - currentRadioFrequency; + if (distance > 0 && distance < minDistanceAbove) { + minDistanceAbove = distance; + nearestAbove = row; + } else if (distance < 0 && Math.abs(distance) < minDistanceBelow) { + minDistanceBelow = Math.abs(distance); + nearestBelow = row; + } + if (gradientColor) { coloredCount++; // Store and apply gradient color directly to override Bootstrap striping @@ -2295,16 +2314,42 @@ $(function() { row.style.setProperty('--bs-table-accent-bg', gradientColor, 'important'); row.style.setProperty('background-color', gradientColor, 'important'); $(row).addClass('cat-frequency-gradient'); + // Remove border markers if spot has gradient + $(row).removeClass('cat-nearest-above cat-nearest-below'); + } else { + // Remove gradient styling if outside range + $(row).removeAttr('data-gradient-color'); + $(row).removeClass('cat-frequency-gradient'); + row.style.removeProperty('--bs-table-bg'); + row.style.removeProperty('--bs-table-accent-bg'); + row.style.removeProperty('background-color'); + // Remove border markers (will be added back if needed) + $(row).removeClass('cat-nearest-above cat-nearest-below'); + } + }); + + // If no spots are colored, add purple borders to nearest spots above/below + // NOTE: Table is sorted DESC, so higher frequencies appear at TOP, lower at BOTTOM + // Borders point TOWARD current frequency to create visual bracket + if (coloredCount === 0) { + console.log('No spots colored - adding border indicators. Current freq:', currentRadioFrequency); + console.log('Nearest below:', nearestBelow, 'Distance:', minDistanceBelow, 'kHz'); + console.log('Nearest above:', nearestAbove, 'Distance:', minDistanceAbove, 'kHz'); + + // Spot BELOW current freq (lower number) appears at BOTTOM of DESC table → TOP border points UP toward you + if (nearestBelow) { + $(nearestBelow).addClass('cat-nearest-below'); + console.log('Added cat-nearest-below class (lower frequency, top border points up)'); + } + // Spot ABOVE current freq (higher number) appears at TOP of DESC table → BOTTOM border points DOWN toward you + if (nearestAbove) { + $(nearestAbove).addClass('cat-nearest-above'); + console.log('Added cat-nearest-above class (higher frequency, bottom border points down)'); + } } else { - // Remove gradient styling if outside range - $(row).removeAttr('data-gradient-color'); - $(row).removeClass('cat-frequency-gradient'); - row.style.removeProperty('--bs-table-bg'); - row.style.removeProperty('--bs-table-accent-bg'); - row.style.removeProperty('background-color'); + console.log('Spots colored:', coloredCount, '- no border indicators needed'); } - }); -} // Save reference to cat.js's updateCATui if it exists + } // Save reference to cat.js's updateCATui if it exists var catJsUpdateCATui = window.updateCATui; // Override updateCATui to add bandmap-specific behavior From 1fe25dbfd8eb2bf2920ba355b54e53029b74fdf2 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 01:00:14 +0100 Subject: [PATCH 28/51] Send mode to QSO form --- assets/js/sections/bandmap_list.js | 11 +++++++++++ assets/js/sections/qso.js | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index a507d4497..d6b695250 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -2092,6 +2092,17 @@ $(function() { call: call }; + // Add mode with fallback to SSB (backward compatible - optional field) + if (mode) { + // Determine appropriate radio mode based on spot mode and frequency + message.mode = determineRadioMode(mode, qrg); + console.log('Added mode to message:', message.mode, '(from spot mode:', mode + ')'); + } else { + // Fallback to SSB based on frequency + message.mode = qrg < 10000000 ? 'LSB' : 'USB'; + console.log('No spot mode - using fallback:', message.mode); + } + // If radio is in split mode, include the RX frequency if (window.lastCATData && window.lastCATData.frequency_rx) { message.frequency_rx = window.lastCATData.frequency_rx; diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 8a37dcf0a..67d42d4ec 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -758,6 +758,7 @@ bc.onmessage = function (ev) { console.log('Full ev.data:', ev.data); console.log('Frequency:', ev.data.frequency); console.log('Call:', ev.data.call); + console.log('Mode:', ev.data.mode); console.log('POTA ref:', ev.data.pota_ref); console.log('SOTA ref:', ev.data.sota_ref); console.log('WWFF ref:', ev.data.wwff_ref); @@ -789,6 +790,11 @@ bc.onmessage = function (ev) { $('#frequency_rx').val(ev.data.frequency_rx); $("#band_rx").val(frequencyToBand(ev.data.frequency_rx)); } + // Set mode if provided (backward compatible - optional field) + if (ev.data.mode) { + $("#mode").val(ev.data.mode); + console.log('Mode set to:', ev.data.mode); + } $("#callsign").val(ev.data.call); $("#callsign").focusout(); $("#callsign").blur(); From 4bfd21d425f84516e9c363d4980c47cbb3a40177 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 01:06:23 +0100 Subject: [PATCH 29/51] Header improvment --- application/views/bandmap/list.php | 17 ++++++++++------- assets/css/bandmap_list.css | 5 +++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 61de09e81..60ad3967f 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -32,15 +32,18 @@
DX Cluster - spot list
-
- -
- -
+ +
diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index 67802dde0..1d36fd088 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -490,6 +490,11 @@ table tbody tr.cat-nearest-above td { flex-direction: column; } +/* Reduce card header padding for more compact appearance */ +.card-header { + padding: 0.25rem 1rem !important; +} + .bandmap-fullscreen .card-header { flex: 0 0 auto; } From d55461e95fb3b63de5e0ad01a3f12a3b0f7a35c7 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 01:10:12 +0100 Subject: [PATCH 30/51] Fixed WWFF link --- assets/js/sections/bandmap_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index d6b695250..76a60ce16 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -952,8 +952,8 @@ $(function() { } if (single.dxcc_spotted && single.dxcc_spotted.wwff_ref) { - let wwff_title = 'WWFF: ' + single.dxcc_spotted.wwff_ref + ' - Click to view on WWFF.co'; - let wwff_url = 'https://wwff.co/directory/?showRef=' + single.dxcc_spotted.wwff_ref; + let wwff_title = 'WWFF: ' + single.dxcc_spotted.wwff_ref + ' - Click to view on cqgma.org'; + let wwff_url = 'https://www.cqgma.org/zinfo.php?ref=' + single.dxcc_spotted.wwff_ref; activity_flags += '' + buildBadge('success', 'fa-leaf', wwff_title) + ''; } From 9d4c9f024e61e8cd75dbe179d3e14ad06800e997 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Mon, 3 Nov 2025 01:41:47 +0100 Subject: [PATCH 31/51] Tweaked purple freq marker --- assets/css/bandmap_list.css | 19 ++++++--- assets/js/sections/bandmap_list.js | 68 ++++++++++++++++++++++++++---- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index 1d36fd088..7b540bb33 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -233,30 +233,30 @@ table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even), table.spottable tbody tr.cat-nearest-below, table.dataTable tbody tr.cat-nearest-below, table tbody tr.cat-nearest-below { - border-top: 4px solid #8b5cf6 !important; - box-shadow: 0 -4px 0 0 #8b5cf6 !important; + border-top: 2px solid #8b5cf6 !important; + box-shadow: 0 -2px 0 0 #8b5cf6 !important; } /* Apply to td cells as well for border-collapse tables */ table.spottable tbody tr.cat-nearest-below td, table.dataTable tbody tr.cat-nearest-below td, table tbody tr.cat-nearest-below td { - border-top: 4px solid #8b5cf6 !important; + border-top: 2px solid #8b5cf6 !important; } /* Spot ABOVE current frequency (higher number) appears at TOP of table → BOTTOM border points DOWN toward you */ table.spottable tbody tr.cat-nearest-above, table.dataTable tbody tr.cat-nearest-above, table tbody tr.cat-nearest-above { - border-bottom: 4px solid #8b5cf6 !important; - box-shadow: 0 4px 0 0 #8b5cf6 !important; + border-bottom: 2px solid #8b5cf6 !important; + box-shadow: 0 2px 0 0 #8b5cf6 !important; } /* Apply to td cells as well for border-collapse tables */ table.spottable tbody tr.cat-nearest-above td, table.dataTable tbody tr.cat-nearest-above td, table tbody tr.cat-nearest-above td { - border-bottom: 4px solid #8b5cf6 !important; + border-bottom: 2px solid #8b5cf6 !important; } /* Status bar styling */ @@ -274,12 +274,16 @@ table tbody tr.cat-nearest-above td { display: flex; align-items: center; gap: 15px; + flex-wrap: nowrap; } .status-bar-left { - flex: 1; + flex: 1 1 auto; min-width: 0; font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } #statusMessage { @@ -296,6 +300,7 @@ table tbody tr.cat-nearest-above td { display: flex; align-items: center; justify-content: flex-end; + white-space: nowrap; } .dataTables_wrapper { diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 76a60ce16..c968c99a3 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -429,6 +429,19 @@ $(function() { let qrg = parseFloat(rowData[2]) * 1000000; // Frequency in MHz, convert to Hz let mode = rowData[3]; // Mode is column 4 (0-indexed = 3) + // Ctrl+click: Only tune radio, don't prepare logging form + if (e.ctrlKey || e.metaKey) { + if (isCatTrackingEnabled) { + tuneRadio(qrg, mode); + } else { + console.log('CAT Control is not enabled - cannot tune radio'); + if (typeof showToast === 'function') { + showToast('CAT Control Required', 'Enable CAT Control to tune the radio', 'bg-warning text-dark', 3000); + } + } + return; + } + console.log('=== SEARCHING FOR SPOT DATA ==='); console.log('Looking for callsign:', call); console.log('Row frequency (MHz):', rowData[2]); @@ -1155,8 +1168,9 @@ $(function() { updateBandCountBadges(); // Update CAT frequency gradient colors/borders after rendering if CAT is enabled + // Force update to ensure borders appear even if frequency hasn't changed if (isCatTrackingEnabled && currentRadioFrequency) { - updateFrequencyGradientColors(); + updateFrequencyGradientColors(true); } // Update status bar after render completes @@ -2288,6 +2302,8 @@ $(function() { let nearestBelow = null; // Spot below current frequency let minDistanceAbove = Infinity; let minDistanceBelow = Infinity; + let nearestAboveFreq = null; // Track frequency of nearest above + let nearestBelowFreq = null; // Track frequency of nearest below // Iterate through all visible rows table.rows({ search: 'applied' }).every(function() { @@ -2307,13 +2323,24 @@ $(function() { $(row).attr('data-spot-frequency', spotFreqKhz); // Track nearest spots above and below current frequency + // For nearestAbove (gets BOTTOM border): use <= to select LAST occurrence (bottommost in group) + // For nearestBelow (gets TOP border): use < to select FIRST occurrence (topmost in group) const distance = spotFreqKhz - currentRadioFrequency; - if (distance > 0 && distance < minDistanceAbove) { - minDistanceAbove = distance; - nearestAbove = row; - } else if (distance < 0 && Math.abs(distance) < minDistanceBelow) { - minDistanceBelow = Math.abs(distance); - nearestBelow = row; + if (distance > 0) { + // Spot is above current frequency + if (distance <= minDistanceAbove) { + minDistanceAbove = distance; + nearestAbove = row; + nearestAboveFreq = spotFreqKhz; + } + } else if (distance < 0) { + // Spot is below current frequency + const absDistance = Math.abs(distance); + if (absDistance < minDistanceBelow) { + minDistanceBelow = absDistance; + nearestBelow = row; + nearestBelowFreq = spotFreqKhz; + } } if (gradientColor) { @@ -2334,8 +2361,6 @@ $(function() { row.style.removeProperty('--bs-table-bg'); row.style.removeProperty('--bs-table-accent-bg'); row.style.removeProperty('background-color'); - // Remove border markers (will be added back if needed) - $(row).removeClass('cat-nearest-above cat-nearest-below'); } }); @@ -2343,21 +2368,46 @@ $(function() { // NOTE: Table is sorted DESC, so higher frequencies appear at TOP, lower at BOTTOM // Borders point TOWARD current frequency to create visual bracket if (coloredCount === 0) { + // First, remove any existing border classes from all rows + table.rows().every(function() { + $(this.node()).removeClass('cat-nearest-above cat-nearest-below'); + }); + console.log('No spots colored - adding border indicators. Current freq:', currentRadioFrequency); console.log('Nearest below:', nearestBelow, 'Distance:', minDistanceBelow, 'kHz'); console.log('Nearest above:', nearestAbove, 'Distance:', minDistanceAbove, 'kHz'); + // DEBUG: Check how many rows match each frequency + if (nearestBelow) { + const belowFreq = $(nearestBelow).attr('data-spot-frequency'); + const belowMatches = $('tr[data-spot-frequency="' + belowFreq + '"]'); + console.log('DEBUG: Rows at nearest below freq (' + belowFreq + '):', belowMatches.length); + } + if (nearestAbove) { + const aboveFreq = $(nearestAbove).attr('data-spot-frequency'); + const aboveMatches = $('tr[data-spot-frequency="' + aboveFreq + '"]'); + console.log('DEBUG: Rows at nearest above freq (' + aboveFreq + '):', aboveMatches.length); + } + // Spot BELOW current freq (lower number) appears at BOTTOM of DESC table → TOP border points UP toward you if (nearestBelow) { $(nearestBelow).addClass('cat-nearest-below'); console.log('Added cat-nearest-below class (lower frequency, top border points up)'); + // DEBUG: Verify only one row has the class + console.log('DEBUG: Total rows with cat-nearest-below:', $('.cat-nearest-below').length); } // Spot ABOVE current freq (higher number) appears at TOP of DESC table → BOTTOM border points DOWN toward you if (nearestAbove) { $(nearestAbove).addClass('cat-nearest-above'); console.log('Added cat-nearest-above class (higher frequency, bottom border points down)'); + // DEBUG: Verify only one row has the class + console.log('DEBUG: Total rows with cat-nearest-above:', $('.cat-nearest-above').length); } } else { + // Remove border indicators when spots are in gradient range + table.rows().every(function() { + $(this.node()).removeClass('cat-nearest-above cat-nearest-below'); + }); console.log('Spots colored:', coloredCount, '- no border indicators needed'); } } // Save reference to cat.js's updateCATui if it exists From 4b562459517683d81d5a10ab3925e433d78ef800 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 5 Nov 2025 23:00:08 +0100 Subject: [PATCH 32/51] Fix for CAT control switched off --- application/views/interface_assets/footer.php | 4 +- assets/js/sections/bandmap_list.js | 232 ++++++++++++++---- 2 files changed, 184 insertions(+), 52 deletions(-) diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 27ae03ca9..e54bdcc30 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1465,8 +1465,8 @@ mymap.on('mousemove', onQsoMapMove); uri->segment(1) == "bandmap" && $this->uri->segment(2) == "list") { ?> - - + + uri->segment(1) == "logbook" && $this->uri->segment(2) == "view") { ?> diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index c968c99a3..2502d66f9 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -32,13 +32,52 @@ 'use strict'; +console.log('=== BANDMAP_LIST.JS LOADING ==='); + // ======================================== // CONFIGURATION // ======================================== const SPOT_REFRESH_INTERVAL = 60; // Auto-refresh interval in seconds +// Configure DataTables error mode BEFORE document ready +// This prevents alert dialogs from showing +console.log('Configuring DataTables error mode...'); +if (typeof jQuery !== 'undefined' && jQuery.fn && jQuery.fn.dataTable) { + console.log('DataTables found, setting errMode to console logging'); + jQuery.fn.dataTable.ext.errMode = function(settings, helpPage, message) { + console.error('=== DataTables Error (pre-init) ==='); + console.error('Message:', message); + console.error('Help page:', helpPage); + console.error('Settings:', settings); + }; +} else { + console.warn('DataTables not found at pre-init stage'); +} + $(function() { + console.log('=== BANDMAP Document Ready ==='); + + // Configure DataTables to log errors to console instead of showing alert dialogs + // MUST be set before any DataTable is initialized + if ($.fn.dataTable) { + console.log('Setting DataTables errMode inside document ready'); + $.fn.dataTable.ext.errMode = function(settings, helpPage, message) { + console.error('=== DataTables Error ==='); + console.error('Message:', message); + console.error('Help page:', helpPage); + console.error('Settings:', settings); + // Also log which row/column caused the issue + if (message.indexOf('parameter') !== -1) { + console.error('This usually means the data array has wrong number of columns'); + console.error('Expected columns: 15 (Age, Band, Freq, Mode, Spotted, Cont, CQZ, Flag, Entity, DXCC#, Spotter, de Cont, de CQZ, Special, Message)'); + } + }; + console.log('DataTables errMode configured successfully'); + } else { + console.error('$.fn.dataTable not available!'); + } + // ======================================== // FILTER UI MANAGEMENT // ======================================== @@ -859,14 +898,12 @@ $(function() { } if (!passesDeContFilter) 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) + // Apply spotted continent filter (which continent the DX station is in) + let passesContinentFilter = spottedContinents.includes('Any'); + if (!passesContinentFilter) { + passesContinentFilter = single.dxcc_spotted && 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); @@ -904,15 +941,38 @@ $(function() { break; } } - } - if (!passesFlagsFilter) return; + } + if (!passesFlagsFilter) return; - // All filters passed - build table row data - spots2render++; - var data = []; - var dxcc_wked_info, wked_info; + // All filters passed - validate essential data exists + // We need at least the spotted callsign and basic DXCC info + if (!single.dxcc_spotted) { + console.warn('Spot missing dxcc_spotted - creating placeholder:', single.spotted, single.frequency); + // Create minimal dxcc_spotted object to prevent errors + single.dxcc_spotted = { + dxcc_id: 0, + cont: '', + cqz: '', + flag: '', + entity: 'Unknown' + }; + } + if (!single.dxcc_spotter) { + console.warn('Spot missing dxcc_spotter - creating placeholder:', single.spotted, single.frequency); + // Create minimal dxcc_spotter object to prevent errors + single.dxcc_spotter = { + dxcc_id: 0, + cont: '', + cqz: '', + flag: '', + entity: 'Unknown' + }; + } - // Color code DXCC entity: green=confirmed, yellow=worked, red=new + // 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"; @@ -1022,48 +1082,50 @@ $(function() { var spotted = wked_info; data[0].push(spotted); - // 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); + // 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"; + } + let continent_value = (single.dxcc_spotted && single.dxcc_spotted.cont) ? single.dxcc_spotted.cont : ''; + continent_wked_info = continent_value ? ((continent_wked_info != '' ? '' : '') + continent_value + (continent_wked_info != '' ? '' : '')) : ''; + data[0].push(continent_wked_info); - // CQ Zone column: show CQ Zone (moved here, right after Cont) - data[0].push(single.dxcc_spotted.cqz || ''); + // CQ Zone column: show CQ Zone (moved here, right after Cont) + data[0].push((single.dxcc_spotted && single.dxcc_spotted.cqz) ? single.dxcc_spotted.cqz : ''); // Flag column: just the flag emoji without entity name + let flag_only = ''; + if (single.dxcc_spotted && single.dxcc_spotted.flag) { + flag_only = '' + single.dxcc_spotted.flag + ''; + } + data[0].push(flag_only); - // Flag column: just the flag emoji without entity name - let flag_only = ''; - if (single.dxcc_spotted.flag) { - flag_only = '' + single.dxcc_spotted.flag + ''; - } - data[0].push(flag_only); - - // Entity column: entity name with color coding (no flag) - let dxcc_entity_full = single.dxcc_spotted.entity; - let entity_colored = (dxcc_wked_info != '' ? '' : '') + single.dxcc_spotted.entity + (dxcc_wked_info != '' ? '' : ''); + // Entity column: entity name with color coding (no flag) + let dxcc_entity_full = single.dxcc_spotted ? (single.dxcc_spotted.entity || '') : ''; + let entity_colored = dxcc_entity_full ? ((dxcc_wked_info != '' ? '' : '') + dxcc_entity_full + (dxcc_wked_info != '' ? '' : '')) : ''; + if (single.dxcc_spotted && single.dxcc_spotted.dxcc_id && dxcc_entity_full) { data[0].push('' + entity_colored + ''); + } else { + data[0].push(entity_colored); + } - // DXCC Number column: show ADIF DXCC entity number with color coding - let dxcc_number = ((dxcc_wked_info != '' ? '' : '') + single.dxcc_spotted.dxcc_id + (dxcc_wked_info != '' ? '' : '')); - data[0].push(dxcc_number); + // DXCC Number column: show ADIF DXCC entity number with color coding + let dxcc_id_value = (single.dxcc_spotted && single.dxcc_spotted.dxcc_id) ? single.dxcc_spotted.dxcc_id : ''; + let dxcc_number = dxcc_id_value ? ((dxcc_wked_info != '' ? '' : '') + dxcc_id_value + (dxcc_wked_info != '' ? '' : '')) : ''; + data[0].push(dxcc_number); - // de Callsign column (Spotter) - clickable QRZ link - let spotterQrzLink = '' + single.spotter + ''; - data[0].push(spotterQrzLink); + // de Callsign column (Spotter) - clickable QRZ link + let spotterQrzLink = '' + single.spotter + ''; + data[0].push(spotterQrzLink); - // de Cont column: spotter's continent - data[0].push(single.dxcc_spotter.cont || ''); + // de Cont column: spotter's continent + data[0].push((single.dxcc_spotter && single.dxcc_spotter.cont) ? single.dxcc_spotter.cont : ''); - // de CQZ column: spotter's CQ Zone - data[0].push(single.dxcc_spotter.cqz || ''); - - // Build medal badge - show only highest priority: continent > country > callsign +// de CQZ column: spotter's CQ Zone +data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spotter.cqz : ''); // Build medal badge - show only highest priority: continent > country > callsign let medals = ''; if (single.worked_continent === false) { // New Continent (not worked before) - Gold medal @@ -1081,6 +1143,18 @@ $(function() { data[0].push(flags_column); // Message column data[0].push(single.message || ''); + // Debug: Validate data array has exactly 15 columns + if (data[0].length !== 15) { + console.error('INVALID DATA ARRAY LENGTH:', data[0].length, 'Expected: 15'); + console.error('Spot:', single.spotted, 'Frequency:', single.frequency); + console.error('Data array:', data[0]); + console.error('Missing columns:', 15 - data[0].length); + // Pad array with empty strings to prevent DataTables error + while (data[0].length < 15) { + data[0].push(''); + } + } + // Add row to table with appropriate styling based on TTL and age // Priority: TTL=0 (expiring) > age < 1 min (very new) > fresh let rowClass = ''; @@ -1577,6 +1651,27 @@ $(function() { currentAjaxRequest = null; table.page.len(50); + // Debug: Log response details + console.log('Backend response received:', { + url: dxurl, + spotCount: Array.isArray(dxspots) ? dxspots.length : 0, + responseType: typeof dxspots, + hasError: dxspots && dxspots.error ? dxspots.error : 'none' + }); + + // Check if response is an error object + if (dxspots && dxspots.error) { + console.warn('Backend returned error:', dxspots.error); + cachedSpotData = []; + table.clear(); + table.settings()[0].oLanguage.sEmptyTable = "No spots found for selected filters"; + table.draw(); + updateStatusBar(0, 0, getServerFilterText(), getClientFilterText(), false, false); + isFetchInProgress = false; + startRefreshTimer(); + return; + } + if (dxspots.length > 0) { dxspots.sort(SortByQrg); // Sort by frequency @@ -1969,6 +2064,43 @@ $(function() { }); $("#radio").on("change", function() { + let selectedRadio = $(this).val(); + + // If "None" (value "0") is selected, automatically disable CAT Control + if (selectedRadio === "0") { + console.log('Radio set to None - automatically disabling CAT Control'); + + // If CAT Control is currently enabled, turn it off + if (isCatTrackingEnabled) { + let btn = $('#toggleCatTracking'); + btn.removeClass('btn-success').addClass('btn-secondary'); + isCatTrackingEnabled = false; + window.isCatTrackingEnabled = false; + + // Hide radio status + $('#radio_cat_state').remove(); + + // Re-enable band filter controls + enableBandFilterControls(); + + // Unlock table sorting + unlockTableSorting(); + + // Reset band filter to 'All' and fetch all bands + const currentBands = $("#band").val() || []; + if (currentBands.length !== 1 || currentBands[0] !== 'All') { + console.log('Resetting to all bands after disabling CAT Control'); + $("#band").val(['All']); + updateSelectCheckboxes('band'); + syncQuickFilterButtons(); + applyFilters(true); // Force reload to fetch all bands + } + + if (typeof showToast === 'function') { + showToast('Radio', 'Radio set to None - CAT Control disabled', 'bg-info text-white', 3000); + } + } + } }); $("#spottertoggle").on("click", function() { From cb560e47a5fb57d9a97cd47821943860e6033037 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 5 Nov 2025 23:12:53 +0100 Subject: [PATCH 33/51] Fullscreen layout fix --- application/models/Dxcluster_model.php | 31 ++++++++++++---- assets/css/bandmap_list.css | 44 ++++++++++++++++++++++- assets/js/sections/bandmap_list.js | 50 ++++++++++++++++++-------- 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/application/models/Dxcluster_model.php b/application/models/Dxcluster_model.php index d3bea2b1e..3033af399 100644 --- a/application/models/Dxcluster_model.php +++ b/application/models/Dxcluster_model.php @@ -563,18 +563,35 @@ class Dxcluster_model extends CI_Model { // Contest detection - use class property instead of creating array each time if (!$spot->dxcc_spotted->isContest) { - // Check for contest keywords using optimized strpbrk-like approach + // More strict contest detection - require clear indicators + + // Method 1: Explicit contest keywords with word boundaries foreach ($this->contestIndicators as $indicator) { - if (strpos($upperMessage, $indicator) !== false) { + // Use word boundary to avoid matching "CQ DX" in "CQ DX Americas" (which is just a CQ call) + if (preg_match('/\b' . preg_quote($indicator, '/') . '\b/', $upperMessage)) { + // Additional check: avoid false positives from generic "CQ" messages + if ($indicator === 'DX CONTEST' && preg_match('/^CQ\s+DX\s+[A-Z]+$/i', trim($message))) { + continue; // Skip "CQ DX " patterns + } $spot->dxcc_spotted->isContest = true; - return $spot; // Early exit once contest detected + $spot->dxcc_spotted->contestName = $indicator; + return $spot; } } - // Additional heuristic: Check for typical contest exchange patterns - // Match RST + serial number patterns OR zone/state exchanges in single regex - if (preg_match('/\b(?:(?:599|59|5NN)\s+[0-9A-Z]{2,4}|CQ\s+[0-9A-Z]{1,3})\b/', $upperMessage)) { - $spot->dxcc_spotted->isContest = true; + // Method 2: Contest exchange pattern - must have RST AND serial AND no conversational words + // Exclude spots with conversational indicators (TU, TNX, 73, GL, etc.) + $conversational = '/\b(TU|TNX|THANKS|73|GL|HI|FB|CUL|HPE|PSE|DE)\b/'; + + if (!preg_match($conversational, $upperMessage)) { + // Look for typical contest exchange: RST + number (but not just any 599) + // Must be followed by more structured exchange (not just "ur 599") + if (preg_match('/\b(?:599|5NN)\s+(?:TU\s+)?[0-9]{2,4}\b/', $upperMessage) && + !preg_match('/\bUR\s+599\b/', $upperMessage)) { + $spot->dxcc_spotted->isContest = true; + $spot->dxcc_spotted->contestName = 'CONTEST'; + return $spot; + } } } diff --git a/assets/css/bandmap_list.css b/assets/css/bandmap_list.css index 7b540bb33..ca6a14641 100644 --- a/assets/css/bandmap_list.css +++ b/assets/css/bandmap_list.css @@ -537,9 +537,51 @@ table tbody tr.cat-nearest-above td { margin: 0 0 0.5rem 0 !important; } +/* Override status bar wrapper constraints in fullscreen - keep it sharing row with search */ +.bandmap-fullscreen .menu-bar > div > div[style*="max-width: 70%"] { + max-width: none !important; + flex: 1 1 auto !important; + min-width: 400px !important; + display: flex !important; + align-items: center !important; +} + .bandmap-fullscreen .status-bar { flex: 0 0 auto; - margin: 0 0 0.5rem 0 !important; + margin: 0 !important; + width: 100% !important; + padding: 0.25rem 0.5rem !important; + display: flex; + align-items: center; + min-height: calc(1.5em + 0.5rem + 2px); + line-height: 1.5; +} + +/* Override parent container max-width in fullscreen */ +.bandmap-fullscreen .status-bar { + max-width: 100% !important; +} + +.bandmap-fullscreen .status-bar-inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 15px; + flex-wrap: nowrap; +} + +.bandmap-fullscreen .status-bar-left { + flex: 1 1 auto; + min-width: 0; + white-space: normal !important; + overflow: visible !important; + text-overflow: clip !important; +} + +.bandmap-fullscreen .status-bar-right { + flex: 0 0 auto; + min-width: 150px; + white-space: nowrap; } .bandmap-fullscreen .table-responsive { diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 2502d66f9..e950006c1 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1976,10 +1976,13 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot }); $("#clearFiltersButton").on("click", function() { + // Preserve current band selection if CAT Control is enabled + let currentBand = isCatTrackingEnabled ? $('#band').val() : null; + $('#cwnSelect').val(['All']); $('#decontSelect').val(['Any']); $('#continentSelect').val(['Any']); - $('#band').val(['All']); + $('#band').val(currentBand || ['All']); // Preserve band if CAT is enabled $('#mode').val(['All']); $('#additionalFlags').val(['All']); $('#requiredFlags').val([]); @@ -2001,17 +2004,23 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot updateFilterIcon(); applyFilters(true); $('#filterDropdown').dropdown('hide'); + + if (isCatTrackingEnabled && typeof showToast === 'function') { + showToast('Clear Filters', 'Band filter preserved (CAT Control is active)', 'bg-info text-white', 2000); + } }); // Clear Filters Quick Button (preserves De Continent) $("#clearFiltersButtonQuick").on("click", function() { // Preserve current De Continent selection let currentDecont = $('#decontSelect').val(); + // Preserve current band selection if CAT Control is enabled + let currentBand = isCatTrackingEnabled ? $('#band').val() : null; // Reset all other filters $('#cwnSelect').val(['All']).trigger('change'); $('#continentSelect').val(['Any']).trigger('change'); - $('#band').val(['All']).trigger('change'); + $('#band').val(currentBand || ['All']).trigger('change'); // Preserve band if CAT is enabled $('#mode').val(['All']).trigger('change'); $('#additionalFlags').val(['All']).trigger('change'); $('#requiredFlags').val([]).trigger('change'); @@ -2026,6 +2035,10 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot syncQuickFilterButtons(); updateFilterIcon(); applyFilters(false); // Don't refetch from server since De Continent is preserved + + if (isCatTrackingEnabled && typeof showToast === 'function') { + showToast('Clear Filters', 'Band filter preserved (CAT Control is active)', 'bg-info text-white', 2000); + } }); // Sync button states when dropdown is shown @@ -2065,27 +2078,27 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot $("#radio").on("change", function() { let selectedRadio = $(this).val(); - + // If "None" (value "0") is selected, automatically disable CAT Control if (selectedRadio === "0") { console.log('Radio set to None - automatically disabling CAT Control'); - + // If CAT Control is currently enabled, turn it off if (isCatTrackingEnabled) { let btn = $('#toggleCatTracking'); btn.removeClass('btn-success').addClass('btn-secondary'); isCatTrackingEnabled = false; window.isCatTrackingEnabled = false; - + // Hide radio status $('#radio_cat_state').remove(); - + // Re-enable band filter controls enableBandFilterControls(); - + // Unlock table sorting unlockTableSorting(); - + // Reset band filter to 'All' and fetch all bands const currentBands = $("#band").val() || []; if (currentBands.length !== 1 || currentBands[0] !== 'All') { @@ -2095,7 +2108,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot syncQuickFilterButtons(); applyFilters(true); // Force reload to fetch all bands } - + if (typeof showToast === 'function') { showToast('Radio', 'Radio set to None - CAT Control disabled', 'bg-info text-white', 3000); } @@ -3462,12 +3475,21 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot * Apply user favorites to band and mode filters */ function applyUserFavorites(favorites) { - // Apply bands - if (favorites.bands && favorites.bands.length > 0) { - $('#band').val(favorites.bands).trigger('change'); + // Apply bands - but preserve current band if CAT Control is enabled + if (isCatTrackingEnabled) { + // CAT Control is active - don't change band filter + console.log('CAT Control is active - skipping band filter change from favorites'); + if (typeof showToast === 'function') { + showToast('My Favorites', 'Modes applied. Band filter preserved (CAT Control is active)', 'bg-info text-white', 3000); + } } else { - // No active bands, set to All - $('#band').val(['All']).trigger('change'); + // CAT Control is off - apply favorite bands + if (favorites.bands && favorites.bands.length > 0) { + $('#band').val(favorites.bands).trigger('change'); + } else { + // No active bands, set to All + $('#band').val(['All']).trigger('change'); + } } // Apply modes From 603a1868921cc5f98a86c2d71642155088a57e77 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 5 Nov 2025 23:14:41 +0100 Subject: [PATCH 34/51] Contest name --- assets/js/sections/bandmap_list.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index e950006c1..487bd689a 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1037,7 +1037,12 @@ $(function() { } if (single.dxcc_spotted && single.dxcc_spotted.isContest) { - activity_flags += buildBadge('warning', 'fa-trophy', 'Contest'); + // Build contest badge with contest name in tooltip if available + let contestTitle = 'Contest'; + if (single.dxcc_spotted.contest_name && single.dxcc_spotted.contest_name !== '') { + contestTitle = 'Contest: ' + single.dxcc_spotted.contest_name; + } + activity_flags += buildBadge('warning', 'fa-trophy', contestTitle); } // Add "Fresh" badge for spots less than 5 minutes old From 997c0eabb57300b67729233b93948768e1d1ca57 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 5 Nov 2025 23:32:32 +0100 Subject: [PATCH 35/51] Fixes --- application/models/Dxcluster_model.php | 22 ++++++++++++++++++---- application/views/bandmap/list.php | 19 ++++++++++--------- assets/js/sections/bandmap_list.js | 6 ++++++ 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/application/models/Dxcluster_model.php b/application/models/Dxcluster_model.php index 3033af399..03fbcce0b 100644 --- a/application/models/Dxcluster_model.php +++ b/application/models/Dxcluster_model.php @@ -249,11 +249,25 @@ class Dxcluster_model extends CI_Model { $spot->cnfmd_continent = $status['cnfmd_continent']; $spot->worked_continent = $status['worked_continent']; - // Use batch last_worked data - if ($spot->worked_call && isset($last_worked_batch[$callsign])) { - $spot->last_wked = $last_worked_batch[$callsign]; - $spot->last_wked->LAST_QSO = date($custom_date_format, strtotime($spot->last_wked->LAST_QSO)); + // Use batch last_worked data + if ($spot->worked_call && isset($last_worked_batch[$callsign])) { + $spot->last_wked = $last_worked_batch[$callsign]; + + // Validate and convert date safely to prevent epoch date (1970) issues + if (!empty($spot->last_wked->LAST_QSO)) { + $timestamp = strtotime($spot->last_wked->LAST_QSO); + // Check if strtotime succeeded and timestamp is valid (> 0) + if ($timestamp !== false && $timestamp > 0) { + $spot->last_wked->LAST_QSO = date($custom_date_format, $timestamp); + } else { + // Invalid date - remove last_wked to prevent displaying incorrect date + unset($spot->last_wked); + } + } else { + // Empty date - remove last_wked + unset($spot->last_wked); } + } } else { // Fallback for spots without status $spot->worked_dxcc = false; diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 60ad3967f..6ba3dd76f 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -117,15 +117,16 @@
- - + +
@@ -93,7 +125,7 @@ document.addEventListener('DOMContentLoaded', function() { var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; var modKey = isMac ? 'Cmd' : 'Ctrl'; - document.getElementById('filterTipText').textContent = 'Hold ' + modKey + ' and click to select multiple options'; + document.getElementById('filterTipText').textContent = ' ' + modKey + ' '; });
@@ -235,7 +267,7 @@
- + @@ -300,25 +332,25 @@
@@ -355,11 +387,11 @@
- - - - - + + + + + diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index cde8f2f03..4a568a6ad 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1,17 +1,24 @@ /** - * @fileoverview DX CLUSTER BANDMAP for WaveLog - * @version 1.0.0 - * @author Wavelog Team + * @fileoverview DX Cluster Bandmap for Wavelog + * @version 2.0.0 + * @author Wavelog Development Team + * @date 2024-2025 * * @description - * Real-time DX spot filtering and display with intelligent client/server-side - * filter architecture, smart caching, and multi-criteria spot filtering. + * Advanced real-time DX spot filtering and display system with intelligent + * client/server architecture, smart caching, CAT control integration, and + * comprehensive multi-criteria filtering capabilities. * - * @requires jQuery - * @requires DataTables + * @requires jQuery 3.x+ + * @requires DataTables 1.13+ + * @requires Bootstrap 5.x * @requires base_url (global from Wavelog) * @requires dxcluster_provider (global from Wavelog) * @requires dxcluster_maxage (global from Wavelog) + * @requires custom_date_format (global from Wavelog) + * @requires popup_warning (global from Wavelog) + * @requires cat_timeout_interval (global from Wavelog) + * @requires lang_* translation variables (global from Wavelog) * * @browserSupport * - Chrome 90+ @@ -20,20 +27,22 @@ * - Edge 90+ * * @features - * - Smart filter architecture (server-side: continent only; client-side: band, mode, flags) - * - Real-time spot caching and client-side filtering + * - Hybrid filter architecture (server-side: continent, band; client-side: mode, flags, DXCC status) + * - Real-time spot caching with smart TTL management * - Multi-select filters with AND/OR logic - * - Required flags (LoTW, Not Worked) with AND logic - * - Activity flags (POTA, SOTA, WWFF, IOTA, Contest) - * - Auto-refresh with 60-second countdown timer + * - Required flags (LoTW, New Country, New Continent, Worked Callsign) with AND logic + * - Activity reference filters (POTA, SOTA, WWFF, IOTA, Contest) + * - CAT Control integration with frequency gradient visualization + * - Auto-refresh with countdown timer * - DXCC status color coding (Confirmed/Worked/New) - * - TTL-based spot lifecycle (expiring spots shown in red) + * - TTL-based spot lifecycle visualization + * - Fullscreen mode support + * - Responsive design with mobile optimization + * - BroadcastChannel API for QSO window integration */ 'use strict'; -console.log('=== BANDMAP_LIST.JS LOADING ==='); - // ======================================== // CONFIGURATION // ======================================== @@ -41,9 +50,7 @@ const SPOT_REFRESH_INTERVAL = 60; // Auto-refresh interval in seconds // Configure DataTables error mode BEFORE document ready // This prevents alert dialogs from showing -console.log('Configuring DataTables error mode...'); if (typeof jQuery !== 'undefined' && jQuery.fn && jQuery.fn.dataTable) { - console.log('DataTables found, setting errMode to console logging'); jQuery.fn.dataTable.ext.errMode = function(settings, helpPage, message) { console.error('=== DataTables Error (pre-init) ==='); console.error('Message:', message); @@ -56,12 +63,9 @@ if (typeof jQuery !== 'undefined' && jQuery.fn && jQuery.fn.dataTable) { $(function() { - console.log('=== BANDMAP Document Ready ==='); - // Configure DataTables to log errors to console instead of showing alert dialogs // MUST be set before any DataTable is initialized if ($.fn.dataTable) { - console.log('Setting DataTables errMode inside document ready'); $.fn.dataTable.ext.errMode = function(settings, helpPage, message) { console.error('=== DataTables Error ==='); console.error('Message:', message); @@ -73,7 +77,6 @@ $(function() { console.error('Expected columns: 15 (Age, Band, Freq, Mode, Spotted, Cont, CQZ, Flag, Entity, DXCC#, Spotter, de Cont, de CQZ, Special, Message)'); } }; - console.log('DataTables errMode configured successfully'); } else { console.error('$.fn.dataTable not available!'); } @@ -402,7 +405,7 @@ $(function() { language: { url: getDataTablesLanguageUrl(), "emptyTable": " Loading spots...", - "zeroRecords": "No spots found" + "zeroRecords": lang_bandmap_no_spots_found }, 'columnDefs': [ { @@ -473,50 +476,22 @@ $(function() { if (isCatTrackingEnabled) { tuneRadio(qrg, mode); } else { - console.log('CAT Control is not enabled - cannot tune radio'); if (typeof showToast === 'function') { - showToast('CAT Control Required', 'Enable CAT Control to tune the radio', 'bg-warning text-dark', 3000); + showToast(lang_bandmap_cat_required, lang_bandmap_enable_cat, 'bg-warning text-dark', 3000); } } return; } - console.log('=== SEARCHING FOR SPOT DATA ==='); - console.log('Looking for callsign:', call); - console.log('Row frequency (MHz):', rowData[2]); - console.log('Converted to Hz:', qrg); - console.log('Total cached spots:', cachedSpotData ? cachedSpotData.length : 0); - // Find the original spot data to get reference information let spotData = null; if (cachedSpotData) { - // First try exact callsign match to see what frequencies are available - let callsignMatches = cachedSpotData.filter(spot => spot.spotted === call); - console.log('Spots matching callsign', call, ':', callsignMatches.length); - if (callsignMatches.length > 0) { - console.log('Available frequencies for', call, ':', callsignMatches.map(s => ({ - freq_khz: s.frequency, - freq_hz: s.frequency * 1000, // frequency is in kHz, not MHz! - diff_hz: Math.abs(s.frequency * 1000 - qrg) - }))); - } - // Note: spot.frequency is in kHz, so multiply by 1000 to get Hz spotData = cachedSpotData.find(spot => spot.spotted === call && Math.abs(spot.frequency * 1000 - qrg) < 100 // Match within 100 Hz tolerance ); - console.log('Spot data found for', call, ':', spotData); - if (spotData && spotData.dxcc_spotted) { - console.log('References:', { - pota: spotData.dxcc_spotted.pota_ref, - sota: spotData.dxcc_spotted.sota_ref, - wwff: spotData.dxcc_spotted.wwff_ref, - iota: spotData.dxcc_spotted.iota_ref - }); - } } - console.log('================================'); prepareLogging(call, qrg, mode, spotData); }); return table; @@ -697,7 +672,6 @@ $(function() { refreshTimerInterval = setInterval(function() { refreshCountdown--; if (refreshCountdown <= 0) { - console.log('Timer countdown: reloading spot data with current filters'); let table = get_dtable(); table.clear(); @@ -813,7 +787,7 @@ $(function() { if (!cachedSpotData || cachedSpotData.length === 0) { table.clear(); - table.settings()[0].oLanguage.sEmptyTable = "No data available"; + table.settings()[0].oLanguage.sEmptyTable = lang_bandmap_no_data; table.draw(); return; } @@ -840,11 +814,6 @@ $(function() { return; } - // Debug: Log TTL for first few spots - if (spots2render < 3) { - console.log('Spot:', single.spotted, 'Freq:', single.frequency, 'TTL:', ttl); - } - // Extract time from spot data - use 'when' field let timeOnly = single.when; @@ -1171,7 +1140,6 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot if (ttl === 0) { // Expiring spot (gone from cluster but visible for one more cycle) rowClass = 'spot-expiring'; - console.log('EXPIRING SPOT:', single.spotted, 'Freq:', single.frequency, 'TTL:', ttl); } else if (ageMinutesForStyling < 1) { // Very new spot (less than 1 minute old) rowClass = 'spot-very-new'; @@ -1188,16 +1156,12 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } } - // Add row with appropriate class - let addedRow = table.rows.add(data).draw().nodes().to$(); - - if (rowClass) { - addedRow.addClass(rowClass); - if (ttl === 0) { - console.log('Added expiring class to row:', addedRow.hasClass('spot-expiring')); - } - } + // Add row with appropriate class + let addedRow = table.rows.add(data).draw().nodes().to$(); + if (rowClass) { + addedRow.addClass(rowClass); + } // Apply CAT frequency gradient AFTER adding lifecycle classes to ensure it overrides if (isCatTrackingEnabled && currentRadioFrequency) { const spotFreqKhz = single.frequency * 1000; // Convert MHz to kHz @@ -1225,7 +1189,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot if (spots2render == 0) { table.clear(); - table.settings()[0].oLanguage.sEmptyTable = "No data available"; + table.settings()[0].oLanguage.sEmptyTable = lang_bandmap_no_data; table.draw(); } @@ -1625,7 +1589,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // bandForAPI is now passed as a parameter from applyFilters() // Log if CAT Control influenced the band selection if (bandForAPI !== 'All') { - console.log('Fetching specific band from server:', bandForAPI); + } // Update backend filter state @@ -1641,11 +1605,11 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Build API URL: /spots/{band}/{maxAge}/{continent}/{mode} // Mode is always 'All' - filtering happens client-side let dxurl = dxcluster_provider + "/spots/" + bandForAPI + "/" + 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; } @@ -1662,20 +1626,12 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot currentAjaxRequest = null; table.page.len(50); - // Debug: Log response details - console.log('Backend response received:', { - url: dxurl, - spotCount: Array.isArray(dxspots) ? dxspots.length : 0, - responseType: typeof dxspots, - hasError: dxspots && dxspots.error ? dxspots.error : 'none' - }); - // Check if response is an error object if (dxspots && dxspots.error) { console.warn('Backend returned error:', dxspots.error); cachedSpotData = []; table.clear(); - table.settings()[0].oLanguage.sEmptyTable = "No spots found for selected filters"; + table.settings()[0].oLanguage.sEmptyTable = lang_bandmap_no_spots_filters; table.draw(); updateStatusBar(0, 0, getServerFilterText(), getClientFilterText(), false, false); isFetchInProgress = false; @@ -1755,9 +1711,9 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } }); - console.log('TTL Update:', ttlStats, 'Total tracked spots:', spotTTLMap.size); + if (expiringSpots.length > 0) { - console.log('Adding', expiringSpots.length, 'expiring spots back to display'); + } // Merge new spots with expiring spots (TTL=0) for display @@ -1778,14 +1734,14 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // 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.settings()[0].oLanguage.sEmptyTable = lang_bandmap_error_loading; table.draw(); updateStatusBar(0, 0, getServerFilterText(), getClientFilterText(), false, false); startRefreshTimer(); @@ -1936,8 +1892,8 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } } - console.log('applyFilters - Current backend filters:', loadedBackendFilters); - console.log('applyFilters - Requested backend params:', {continent: continentForAPI, band: bandForAPI, singleBandMode: isSingleBandMode}); + + // Check if backend parameters changed (requires new data fetch) // In single-band mode, band selection changes also require server fetch @@ -1945,7 +1901,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot loadedBackendFilters.continent !== continentForAPI || (isSingleBandMode && loadedBackendFilters.band !== bandForAPI); - console.log('applyFilters - backendParamsChanged:', backendParamsChanged); + // Always update current filters for client-side filtering currentFilters = { @@ -1959,11 +1915,11 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot }; if (backendParamsChanged) { - console.log('Reloading from backend: continent=' + continentForAPI + ' band=' + bandForAPI); + table.clear(); fill_list(de, dxcluster_maxage, bandForAPI); } else { - console.log('Client-side filtering changed - using cached data'); + renderFilteredSpots(); updateBandCountBadges(); } @@ -2017,7 +1973,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot $('#filterDropdown').dropdown('hide'); if (isCatTrackingEnabled && typeof showToast === 'function') { - showToast('Clear Filters', 'Band filter preserved (CAT Control is active)', 'bg-info text-white', 2000); + showToast(lang_bandmap_clear_filters, lang_bandmap_band_preserved, 'bg-info text-white', 2000); } }); @@ -2048,7 +2004,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot applyFilters(false); // Don't refetch from server since De Continent is preserved if (isCatTrackingEnabled && typeof showToast === 'function') { - showToast('Clear Filters', 'Band filter preserved (CAT Control is active)', 'bg-info text-white', 2000); + showToast(lang_bandmap_clear_filters, lang_bandmap_band_preserved, 'bg-info text-white', 2000); } }); @@ -2092,7 +2048,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // If "None" (value "0") is selected, automatically disable CAT Control if (selectedRadio === "0") { - console.log('Radio set to None - automatically disabling CAT Control'); + // If CAT Control is currently enabled, turn it off if (isCatTrackingEnabled) { @@ -2113,7 +2069,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Reset band filter to 'All' and fetch all bands const currentBands = $("#band").val() || []; if (currentBands.length !== 1 || currentBands[0] !== 'All') { - console.log('Resetting to all bands after disabling CAT Control'); + $("#band").val(['All']); updateSelectCheckboxes('band'); syncQuickFilterButtons(); @@ -2121,7 +2077,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } if (typeof showToast === 'function') { - showToast('Radio', 'Radio set to None - CAT Control disabled', 'bg-info text-white', 3000); + showToast(lang_bandmap_radio, lang_bandmap_radio_none, 'bg-info text-white', 3000); } } } @@ -2215,7 +2171,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot const selectedRadio = $('.radios option:selected').val(); if (!selectedRadio || selectedRadio === '0') { - console.log('No radio selected - cannot tune'); + return; } @@ -2229,16 +2185,16 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot radioMode, // Use determined radio mode function() { // Success callback - console.log('Radio tuned to:', freqHz, 'Hz', 'Mode:', radioMode); + if (typeof showToast === 'function') { - showToast('Radio Tuned', `Tuned to ${(freqHz / 1000000).toFixed(3)} MHz (${radioMode})`, 'bg-success text-white', 2000); + showToast(lang_bandmap_radio_tuned, `${lang_bandmap_tuned_to} ${(freqHz / 1000000).toFixed(3)} MHz (${radioMode})`, 'bg-success text-white', 2000); } }, function(jqXHR, textStatus, errorThrown) { // Error callback console.error('Failed to tune radio:', errorThrown); if (typeof showToast === 'function') { - showToast('Tuning Failed', 'Failed to tune radio to frequency', 'bg-danger text-white', 3000); + showToast(lang_bandmap_tuning_failed, lang_bandmap_tune_failed_msg, 'bg-danger text-white', 3000); } } ); @@ -2266,50 +2222,48 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot if (mode) { // Determine appropriate radio mode based on spot mode and frequency message.mode = determineRadioMode(mode, qrg); - console.log('Added mode to message:', message.mode, '(from spot mode:', mode + ')'); } else { // Fallback to SSB based on frequency message.mode = qrg < 10000000 ? 'LSB' : 'USB'; - console.log('No spot mode - using fallback:', message.mode); } // If radio is in split mode, include the RX frequency if (window.lastCATData && window.lastCATData.frequency_rx) { message.frequency_rx = window.lastCATData.frequency_rx; - console.log('Split mode detected, RX frequency:', message.frequency_rx); + } // Add reference fields if available (backward compatible - only if spotData exists) if (spotData && spotData.dxcc_spotted) { - console.log('Building message with spot data:', spotData.dxcc_spotted); + if (spotData.dxcc_spotted.pota_ref) { message.pota_ref = spotData.dxcc_spotted.pota_ref; - console.log('Added POTA ref:', message.pota_ref); + } if (spotData.dxcc_spotted.sota_ref) { message.sota_ref = spotData.dxcc_spotted.sota_ref; - console.log('Added SOTA ref:', message.sota_ref); + } if (spotData.dxcc_spotted.wwff_ref) { message.wwff_ref = spotData.dxcc_spotted.wwff_ref; - console.log('Added WWFF ref:', message.wwff_ref); + } if (spotData.dxcc_spotted.iota_ref) { message.iota_ref = spotData.dxcc_spotted.iota_ref; - console.log('Added IOTA ref:', message.iota_ref); + } } else { - console.log('No spot data or dxcc_spotted available'); + } - console.log('Final message to send:', message); + let check_pong = setInterval(function() { if (pong_rcvd || ((Date.now() - qso_window_last_seen) < wait4pong)) { clearInterval(check_pong); bc2qso.postMessage(message); // Show toast notification when callsign is sent to existing QSO window - showToast('QSO Prepared', `Callsign ${call} sent to logging form`, 'bg-success text-white', 3000); + showToast(lang_bandmap_qso_prepared, `${lang_bandmap_callsign_sent} ${call} ${lang_bandmap_sent_to_form}`, 'bg-success text-white', 3000); } else { clearInterval(check_pong); let cl = message; // Use the message object with all fields @@ -2324,7 +2278,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } else { newWindow.focus(); // Show toast notification when opening new QSO window - showToast('QSO Prepared', `Callsign ${call} sent to logging form`, 'bg-success text-white', 3000); + showToast(lang_bandmap_qso_prepared, `${lang_bandmap_callsign_sent} ${call} ${lang_bandmap_sent_to_form}`, 'bg-success text-white', 3000); } bc2qso.onmessage = function(ev) { @@ -2529,53 +2483,35 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot $(this.node()).removeClass('cat-nearest-above cat-nearest-below'); }); - console.log('No spots colored - adding border indicators. Current freq:', currentRadioFrequency); - console.log('Nearest below:', nearestBelow, 'Distance:', minDistanceBelow, 'kHz'); - console.log('Nearest above:', nearestAbove, 'Distance:', minDistanceAbove, 'kHz'); - // DEBUG: Check how many rows match each frequency - if (nearestBelow) { - const belowFreq = $(nearestBelow).attr('data-spot-frequency'); - const belowMatches = $('tr[data-spot-frequency="' + belowFreq + '"]'); - console.log('DEBUG: Rows at nearest below freq (' + belowFreq + '):', belowMatches.length); - } - if (nearestAbove) { - const aboveFreq = $(nearestAbove).attr('data-spot-frequency'); - const aboveMatches = $('tr[data-spot-frequency="' + aboveFreq + '"]'); - console.log('DEBUG: Rows at nearest above freq (' + aboveFreq + '):', aboveMatches.length); - } + + // Spot BELOW current freq (lower number) appears at BOTTOM of DESC table → TOP border points UP toward you if (nearestBelow) { $(nearestBelow).addClass('cat-nearest-below'); - console.log('Added cat-nearest-below class (lower frequency, top border points up)'); - // DEBUG: Verify only one row has the class - console.log('DEBUG: Total rows with cat-nearest-below:', $('.cat-nearest-below').length); } // Spot ABOVE current freq (higher number) appears at TOP of DESC table → BOTTOM border points DOWN toward you if (nearestAbove) { $(nearestAbove).addClass('cat-nearest-above'); - console.log('Added cat-nearest-above class (higher frequency, bottom border points down)'); - // DEBUG: Verify only one row has the class - console.log('DEBUG: Total rows with cat-nearest-above:', $('.cat-nearest-above').length); } } else { // Remove border indicators when spots are in gradient range table.rows().every(function() { $(this.node()).removeClass('cat-nearest-above cat-nearest-below'); }); - console.log('Spots colored:', coloredCount, '- no border indicators needed'); + } } // Save reference to cat.js's updateCATui if it exists var catJsUpdateCATui = window.updateCATui; // Override updateCATui to add bandmap-specific behavior window.updateCATui = function(data) { - console.log('Bandmap: updateCATui called with data:', data); + const band = frequencyToBand(data.frequency); - console.log('Bandmap CAT Update - Frequency:', data.frequency, 'Band:', band, 'Control enabled:', isCatTrackingEnabled); + // Store current radio frequency (convert Hz to kHz) currentRadioFrequency = data.frequency / 1000; @@ -2588,28 +2524,28 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Valid band found - set filter to this specific band // Check if current selection is not just this band if (currentBands.length !== 1 || currentBands[0] !== band) { - console.log('Updating band filter to:', band); + $("#band").val([band]); updateSelectCheckboxes('band'); syncQuickFilterButtons(); applyFilters(false); // Show toast notification when band filter is changed by CAT if (typeof showToast === 'function') { - showToast('CAT Control', `Frequency filter changed to ${band} by transceiver`, 'bg-info text-white', 3000); + showToast(lang_bandmap_cat_control, `${lang_bandmap_freq_changed} ${band} ${lang_bandmap_by_transceiver}`, 'bg-info text-white', 3000); } } } else { // No band match - clear band filter to show all bands // Only update if not already showing all bands if (currentBands.length !== 1 || currentBands[0] !== 'All') { - console.log('Frequency outside known bands - clearing band filter to show all'); + $("#band").val(['All']); updateSelectCheckboxes('band'); syncQuickFilterButtons(); applyFilters(false); // Show toast notification if (typeof showToast === 'function') { - showToast('CAT Control', 'Frequency outside known bands - showing all bands', 'bg-warning text-dark', 3000); + showToast(lang_bandmap_cat_control, lang_bandmap_freq_outside, 'bg-warning text-dark', 3000); } } } @@ -2620,7 +2556,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Call cat.js's original updateCATui for standard CAT UI updates if (typeof catJsUpdateCATui === 'function') { - console.log('Bandmap: Calling cat.js updateCATui'); + // Store current band selection before calling cat.js updateCATui const bandBeforeUpdate = $("#band").val(); @@ -2636,7 +2572,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot } else { console.warn('Bandmap: cat.js updateCATui not available'); } -}; console.log('Bandmap: CAT integration complete, updateCATui override installed'); +}; $.fn.dataTable.moment(custom_date_format + ' HH:mm'); @@ -2664,7 +2600,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot const elem = document.documentElement; if (elem.requestFullscreen) { elem.requestFullscreen().catch(err => { - console.log('Fullscreen request failed:', err); + }); } else if (elem.webkitRequestFullscreen) { // Safari elem.webkitRequestFullscreen(); @@ -2688,7 +2624,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Exit browser fullscreen if (document.exitFullscreen) { document.exitFullscreen().catch(err => { - console.log('Exit fullscreen failed:', err); + }); } else if (document.webkitExitFullscreen) { // Safari document.webkitExitFullscreen(); @@ -3476,7 +3412,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot applyUserFavorites(favorites); }, error: function() { - showToast('My Favorites', 'Failed to load favorites', 'bg-danger text-white', 3000); + showToast(lang_bandmap_my_favorites, lang_bandmap_favorites_failed, 'bg-danger text-white', 3000); } }); } @@ -3489,9 +3425,9 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Apply bands - but preserve current band if CAT Control is enabled if (isCatTrackingEnabled) { // CAT Control is active - don't change band filter - console.log('CAT Control is active - skipping band filter change from favorites'); + if (typeof showToast === 'function') { - showToast('My Favorites', 'Modes applied. Band filter preserved (CAT Control is active)', 'bg-info text-white', 3000); + showToast(lang_bandmap_my_favorites, lang_bandmap_modes_applied, 'bg-info text-white', 3000); } } else { // CAT Control is off - apply favorite bands @@ -3521,7 +3457,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot updateBandCountBadges(); applyFilters(false); - showToast('My Favorites', 'Applied your favorite bands and modes', 'bg-success text-white', 3000); + showToast(lang_bandmap_my_favorites, lang_bandmap_favorites_applied, 'bg-success text-white', 3000); } // ======================================== @@ -3581,15 +3517,12 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot col.bSortable = false; }); - // Disable click events on all column headers - $('.spottable thead th').off('click.DT'); - - // Redraw column headers to update sort icons - table.columns.adjust(); - - console.log('Table sorting locked to Frequency (DESC) only'); - } + // Disable click events on all column headers + $('.spottable thead th').off('click.DT'); + // Redraw column headers to update sort icons + table.columns.adjust(); +} /** * Unlock table sorting when CAT Control is disabled */ @@ -3621,7 +3554,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Clear frequency gradient colors clearFrequencyGradientColors(); - console.log('Table sorting unlocked'); + } /** @@ -3650,7 +3583,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot btn.removeClass('btn-success').addClass('btn-secondary'); isCatTrackingEnabled = false; window.isCatTrackingEnabled = false; // Update window variable for cat.js - console.log('CAT Control disabled'); + // Hide radio status when CAT Control is disabled $('#radio_cat_state').remove(); @@ -3664,7 +3597,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Reset band filter to 'All' and fetch all bands const currentBands = $("#band").val() || []; if (currentBands.length !== 1 || currentBands[0] !== 'All') { - console.log('CAT Control disabled - resetting to all bands'); + $("#band").val(['All']); updateSelectCheckboxes('band'); syncQuickFilterButtons(); @@ -3675,7 +3608,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot btn.removeClass('btn-secondary').addClass('btn-success'); isCatTrackingEnabled = true; window.isCatTrackingEnabled = true; // Update window variable for cat.js - console.log('CAT Control enabled'); + // Trigger radio status display if we have data if (window.lastCATData) { @@ -3692,34 +3625,34 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Immediately apply current radio frequency if available if (window.lastCATData && window.lastCATData.frequency) { - console.log('Applying current radio frequency:', window.lastCATData.frequency); + const band = frequencyToBand(window.lastCATData.frequency); if (band && band !== '') { // Valid band found - set filter to this specific band - console.log('Setting band filter to:', band); + $("#band").val([band]); updateSelectCheckboxes('band'); syncQuickFilterButtons(); applyFilters(false); if (typeof showToast === 'function') { - showToast('CAT Control', `Frequency filter set to ${band} by transceiver`, 'bg-info text-white', 3000); + showToast(lang_bandmap_cat_control, `${lang_bandmap_freq_filter_set} ${band} ${lang_bandmap_by_transceiver}`, 'bg-info text-white', 3000); } } else { // No band match - clear band filter to show all bands - console.log('Frequency outside known bands - showing all'); + $("#band").val(['All']); updateSelectCheckboxes('band'); syncQuickFilterButtons(); applyFilters(false); if (typeof showToast === 'function') { - showToast('CAT Control', 'Frequency outside known bands - showing all bands', 'bg-warning text-dark', 3000); + showToast(lang_bandmap_cat_control, lang_bandmap_freq_outside, 'bg-warning text-dark', 3000); } } } else { - console.log('No radio data available yet - waiting for next CAT update'); + if (typeof showToast === 'function') { - showToast('CAT Control', 'Waiting for radio data...', 'bg-info text-white', 2000); + showToast(lang_bandmap_cat_control, lang_bandmap_waiting_radio, 'bg-info text-white', 2000); } } } @@ -3855,7 +3788,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot dataType: 'json', success: function(favorites) { cachedUserFavorites = favorites; - console.log('User favorites cached:', favorites); + }, error: function() { console.warn('Failed to cache user favorites'); @@ -3890,3 +3823,5 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot setInterval(updateSpotAges, 60000); }); + + From ad9816cfa35ef7c1236b4c638ae0cf8c45977fa2 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 6 Nov 2025 00:53:54 +0100 Subject: [PATCH 38/51] Refactoring --- application/views/bandmap/list.php | 44 +- assets/js/radiohelpers.js | 225 ++++-- assets/js/sections/bandmap_list.js | 1192 ++++++++-------------------- 3 files changed, 560 insertions(+), 901 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index affae57cf..9c1c8c62d 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -4,7 +4,6 @@ var cat_timeout_interval = "optionslib->get_option('cat_timeout_interval'); ?>"; var dxcluster_maxage = optionslib->get_option('dxcluster_maxage') ?? 60; ?>; var custom_date_format = ""; - var popup_warning = ""; // Detect OS for proper keyboard shortcuts var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -12,6 +11,8 @@ var lang_click_to_prepare_logging = " (" + modKey + "+Click )"; // Bandmap toast messages + var lang_bandmap_popup_blocked = ""; + var lang_bandmap_popup_warning = ""; var lang_bandmap_cat_required = ""; var lang_bandmap_enable_cat = ""; var lang_bandmap_clear_filters = ""; @@ -36,6 +37,45 @@ var lang_bandmap_modes_applied = ""; var lang_bandmap_favorites_applied = ""; + // Bandmap filter status messages + var lang_bandmap_loading_data = ""; + var lang_bandmap_last_fetched = ""; + var lang_bandmap_max_age = ""; + var lang_bandmap_fetched_at = ""; + var lang_bandmap_next_update = ""; + var lang_bandmap_minutes = ""; + var lang_bandmap_seconds = ""; + + // Bandmap filter labels + var lang_bandmap_not_worked = ""; + var lang_bandmap_lotw_user = ""; + var lang_bandmap_new_callsign = ""; + var lang_bandmap_new_continent = ""; + var lang_bandmap_new_country = ""; + var lang_bandmap_worked_before = ""; + var lang_bandmap_confirmed = ""; + + // Bandmap tooltip messages + var lang_bandmap_fresh_spot = ""; + var lang_bandmap_click_view_qrz = ""; + var lang_bandmap_on_qrz = ""; + var lang_bandmap_see_details = ""; + var lang_bandmap_worked_on = ""; + var lang_bandmap_not_worked_band = ""; + + // Bandmap UI messages + var lang_bandmap_exit_fullscreen = ""; + var lang_bandmap_toggle_fullscreen = ""; + var lang_bandmap_cat_band_control = ""; + var lang_bandmap_click_to_qso = ""; + var lang_bandmap_ctrl_click_tune = ""; + var lang_bandmap_requires_cat = ""; + var lang_bandmap_spotter = ""; + var lang_bandmap_comment = ""; + var lang_bandmap_age = ""; + var lang_bandmap_time = ""; + + // DataTables messages var lang_bandmap_loading_spots = ""; var lang_bandmap_no_spots_found = ""; @@ -50,8 +90,6 @@
- -
diff --git a/assets/js/radiohelpers.js b/assets/js/radiohelpers.js index f98857e2a..4854554fd 100644 --- a/assets/js/radiohelpers.js +++ b/assets/js/radiohelpers.js @@ -1,69 +1,184 @@ -function frequencyToBand(frequency) { - result = parseInt(frequency); +/** + * Convert frequency to ham radio band name + * @param {number} frequency - Frequency value + * @param {string} unit - Unit of frequency: 'Hz' (default) or 'kHz' + * @returns {string} Band name (e.g., '20m', '2m', '70cm') or 'All' if not in a known band + */ +function frequencyToBand(frequency, unit = 'Hz') { + // Convert to Hz if input is in kHz + const freqHz = (unit.toLowerCase() === 'khz') ? frequency * 1000 : parseInt(frequency); - if(result >= 14000000 && result <= 14400000) { - return '20m'; + // MF/HF Bands + if (freqHz >= 1800000 && freqHz <= 2000000) return '160m'; + if (freqHz >= 3500000 && freqHz <= 4000000) return '80m'; + if (freqHz >= 5250000 && freqHz <= 5450000) return '60m'; + if (freqHz >= 7000000 && freqHz <= 7300000) return '40m'; + if (freqHz >= 10100000 && freqHz <= 10150000) return '30m'; + if (freqHz >= 14000000 && freqHz <= 14350000) return '20m'; + if (freqHz >= 18068000 && freqHz <= 18168000) return '17m'; + if (freqHz >= 21000000 && freqHz <= 21450000) return '15m'; + if (freqHz >= 24890000 && freqHz <= 24990000) return '12m'; + if (freqHz >= 28000000 && freqHz <= 29700000) return '10m'; + + // VHF Bands + if (freqHz >= 50000000 && freqHz <= 54000000) return '6m'; + if (freqHz >= 70000000 && freqHz <= 71000000) return '4m'; + if (freqHz >= 144000000 && freqHz <= 148000000) return '2m'; + if (freqHz >= 222000000 && freqHz <= 225000000) return '1.25m'; + + // UHF Bands + if (freqHz >= 420000000 && freqHz <= 450000000) return '70cm'; + if (freqHz >= 902000000 && freqHz <= 928000000) return '33cm'; + if (freqHz >= 1240000000 && freqHz <= 1300000000) return '23cm'; + + // SHF Bands + if (freqHz >= 2300000000 && freqHz <= 2450000000) return '13cm'; + if (freqHz >= 3300000000 && freqHz <= 3500000000) return '9cm'; + if (freqHz >= 5650000000 && freqHz <= 5925000000) return '6cm'; + if (freqHz >= 10000000000 && freqHz <= 10500000000) return '3cm'; + if (freqHz >= 24000000000 && freqHz <= 24250000000) return '1.25cm'; + if (freqHz >= 47000000000 && freqHz <= 47200000000) return '6mm'; + if (freqHz >= 75500000000 && freqHz <= 81000000000) return '4mm'; + if (freqHz >= 119980000000 && freqHz <= 120020000000) return '2.5mm'; + if (freqHz >= 142000000000 && freqHz <= 149000000000) return '2mm'; + if (freqHz >= 241000000000 && freqHz <= 250000000000) return '1mm'; + + return 'All'; +} + +/** + * Alias for backward compatibility - converts frequency in kHz to band name + * @deprecated Use frequencyToBand(frequency, 'kHz') instead + * @param {number} freq_khz - Frequency in kilohertz + * @returns {string} Band name or 'All' + */ +function frequencyToBandKhz(freq_khz) { + return frequencyToBand(freq_khz, 'kHz'); +} + +/** + * Determine appropriate radio mode based on spot mode and frequency + * @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) + */ +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 } - else if(result >= 18000000 && result <= 19000000) { - return '17m'; + + const modeUpper = spotMode.toUpperCase(); + + // CW modes + if (modeUpper === 'CW' || modeUpper === 'A1A') { + return 'CW'; } - else if(result >= 1810000 && result <= 2000000) { - return '160m'; + + // 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'; + } } - else if(result >= 3000000 && result <= 4000000) { - return '80m'; + + // 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'; } - else if(result >= 5250000 && result <= 5450000) { - return '60m'; + + // Default: use frequency to determine USB/LSB + return freqHz < 10000000 ? '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 + */ +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'] +}; + +/** + * Map individual band to its band group (MF, HF, VHF, UHF, SHF) + * @param {string} band - Band identifier (e.g., '20m', '2m', '70cm', '13cm') + * @returns {string|null} Band group name or null if band not found + */ +function getBandGroup(band) { + for (const [group, bands] of Object.entries(BAND_GROUPS)) { + if (bands.includes(band)) return group; } - else if(result >= 7000000 && result <= 7500000) { - return '40m'; + return null; +} + +/** + * Get all bands in a band group + * @param {string} group - Band group name (MF, HF, VHF, UHF, or SHF) + * @returns {Array} Array of band identifiers or empty array if group not found + */ +function getBandsInGroup(group) { + return BAND_GROUPS[group] || []; +} + +/** + * Categorize amateur radio mode into phone/cw/digi for filtering + * @param {string} mode - Mode name (e.g., 'USB', 'CW', 'FT8', 'phone') + * @returns {string|null} Mode category: 'phone', 'cw', 'digi', or null if unknown + */ +function getModeCategory(mode) { + if (!mode) return null; + + const modeLower = mode.toLowerCase(); + + // Check if already a category + if (['phone', 'cw', 'digi'].includes(modeLower)) { + return modeLower; } - else if(result >= 10000000 && result <= 11000000) { - return '30m'; + + const modeUpper = mode.toUpperCase(); + + // CW modes + if (['CW', 'CWR', 'A1A'].includes(modeUpper) || modeLower.includes('cw')) { + return 'cw'; } - else if(result >= 21000000 && result <= 21600000) { - return '15m'; + + // Phone modes (voice) + if (['SSB', 'LSB', 'USB', 'FM', 'AM', 'DV', 'PHONE', 'C3E', 'J3E'].includes(modeUpper)) { + return 'phone'; } - else if(result >= 24000000 && result <= 25000000) { - return '12m'; + + // 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)) { + return 'digi'; } - else if(result >= 28000000 && result <= 30000000) { - return '10m'; - } - else if(result >= 50000000 && result <= 56000000) { - return '6m'; - } - else if(result >= 70000000 && result <= 75000000) { - return '4m'; - } - else if(result >= 144000000 && result <= 148000000) { - return '2m'; - } - else if(result >= 219000000 && result <= 225000000) { - return '1.25m'; - } - else if(result >= 420000000 && result <= 450000000) { - return '70cm'; - } - else if(result >= 902000000 && result <= 928000000) { - return '33cm'; - } - else if(result >= 1200000000 && result <= 1600000000) { - return '23cm'; - } - else if(result >= 2300000000 && result <= 2890800000) { - return '13cm'; - } - else if(result >= 3300000000 && result <= 3500000000) { - return '9cm'; - } - else if(result >= 5650000000 && result <= 5925000000) { - return '6cm'; - } - else if(result >= 10000000000 && result <= 10525000000) { - return '3cm'; + + // Check for digital mode substrings + if (modeLower.includes('ft') || modeLower.includes('psk') || modeLower.includes('rtty') || + modeLower.includes('jt') || modeLower === 'digi' || modeLower === 'data') { + return 'digi'; } + + return null; } function catmode(mode) { diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 4a568a6ad..2e698ca57 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -2,21 +2,16 @@ * @fileoverview DX Cluster Bandmap for Wavelog * @version 2.0.0 * @author Wavelog Development Team - * @date 2024-2025 * * @description * Advanced real-time DX spot filtering and display system with intelligent * client/server architecture, smart caching, CAT control integration, and * comprehensive multi-criteria filtering capabilities. - * - * @requires jQuery 3.x+ - * @requires DataTables 1.13+ - * @requires Bootstrap 5.x + * @requires base_url (global from Wavelog) * @requires dxcluster_provider (global from Wavelog) * @requires dxcluster_maxage (global from Wavelog) * @requires custom_date_format (global from Wavelog) - * @requires popup_warning (global from Wavelog) * @requires cat_timeout_interval (global from Wavelog) * @requires lang_* translation variables (global from Wavelog) * @@ -44,25 +39,59 @@ 'use strict'; // ======================================== -// CONFIGURATION +// CONFIGURATION & CONSTANTS // ======================================== + const SPOT_REFRESH_INTERVAL = 60; // Auto-refresh interval in seconds -// Configure DataTables error mode BEFORE document ready -// This prevents alert dialogs from showing -if (typeof jQuery !== 'undefined' && jQuery.fn && jQuery.fn.dataTable) { - jQuery.fn.dataTable.ext.errMode = function(settings, helpPage, message) { - console.error('=== DataTables Error (pre-init) ==='); - console.error('Message:', message); - console.error('Help page:', helpPage); - console.error('Settings:', settings); - }; -} else { - console.warn('DataTables not found at pre-init stage'); -} +// Filter button configurations +const BAND_BUTTONS = [ + { id: '#toggle160mFilter', band: '160m' }, + { id: '#toggle80mFilter', band: '80m' }, + { id: '#toggle60mFilter', band: '60m' }, + { id: '#toggle40mFilter', band: '40m' }, + { id: '#toggle30mFilter', band: '30m' }, + { id: '#toggle20mFilter', band: '20m' }, + { id: '#toggle17mFilter', band: '17m' }, + { id: '#toggle15mFilter', band: '15m' }, + { id: '#toggle12mFilter', band: '12m' }, + { id: '#toggle10mFilter', band: '10m' } +]; + +const BAND_GROUP_BUTTONS = [ + { id: '#toggleVHFFilter', group: 'VHF' }, + { id: '#toggleUHFFilter', group: 'UHF' }, + { id: '#toggleSHFFilter', group: 'SHF' } +]; + +const MODE_BUTTONS = [ + { id: '#toggleCwFilter', mode: 'cw', icon: 'fa-wave-square' }, + { id: '#toggleDigiFilter', mode: 'digi', icon: 'fa-keyboard' }, + { id: '#togglePhoneFilter', mode: 'phone', icon: 'fa-microphone' } +]; + +const CONTINENT_BUTTONS = [ + { id: '#toggleAfricaFilter', continent: 'AF' }, + { id: '#toggleAntarcticaFilter', continent: 'AN' }, + { id: '#toggleAsiaFilter', continent: 'AS' }, + { id: '#toggleEuropeFilter', continent: 'EU' }, + { id: '#toggleNorthAmericaFilter', continent: 'NA' }, + { id: '#toggleOceaniaFilter', continent: 'OC' }, + { id: '#toggleSouthAmericaFilter', continent: 'SA' } +]; + +const GEO_FLAGS = ['POTA', 'SOTA', 'IOTA', 'WWFF']; + +// ======================================== +// MAIN APPLICATION +// ======================================== $(function() { + // ======================================== + // DATATABLES ERROR HANDLING + // ======================================== + // Configure DataTables to log errors to console instead of showing alert dialogs // MUST be set before any DataTable is initialized if ($.fn.dataTable) { @@ -81,33 +110,72 @@ $(function() { console.error('$.fn.dataTable not available!'); } + // ======================================== + // UTILITY FUNCTIONS + // ======================================== + + /** + * Get current values from all filter selects + * @returns {Object} Object containing all filter values + */ + function getAllFilterValues() { + return { + cwn: $('#cwnSelect').val() || [], + deCont: $('#decontSelect').val() || [], + continent: $('#continentSelect').val() || [], + band: $('#band').val() || [], + mode: $('#mode').val() || [], + additionalFlags: $('#additionalFlags').val() || [], + requiredFlags: ($('#requiredFlags').val() || []).filter(v => v !== 'None') + }; + } + + /** + * Check if a filter array contains default "All" or "Any" value + * @param {Array} values - Filter values array + * @param {string} defaultValue - Default value to check ('All' or 'Any') + * @returns {boolean} True if array contains only the default value + */ + function isDefaultFilterValue(values, defaultValue = 'All') { + return values.length === 1 && values.includes(defaultValue); + } + + /** + * Update button visual state (active/inactive) + * @param {string} buttonId - jQuery selector for button + * @param {boolean} isActive - Whether button should appear active + */ + function updateButtonState(buttonId, isActive) { + const $btn = $(buttonId); + $btn.removeClass('btn-secondary btn-success'); + $btn.addClass(isActive ? 'btn-success' : 'btn-secondary'); + } + // ======================================== // FILTER UI MANAGEMENT // ======================================== - // Check if any filters are active (not default "All"/"Any" values) + /** + * Check if any filters are active (not default "All"/"Any" values) + * @returns {boolean} True if any non-default filters are applied + */ function areFiltersApplied() { - let cwnVal = $('#cwnSelect').val() || []; - let decontVal = $('#decontSelect').val() || []; - let continentVal = $('#continentSelect').val() || []; - let bandVal = $('#band').val() || []; - let modeVal = $('#mode').val() || []; - let flagsVal = $('#additionalFlags').val() || []; - let requiredVal = $('#requiredFlags').val() || []; + const filters = getAllFilterValues(); - // Check if anything is selected besides "All"/"Any"/"None" - let isDefaultCwn = cwnVal.length === 1 && cwnVal.includes('All'); - let isDefaultDecont = decontVal.length === 1 && decontVal.includes('Any'); - let isDefaultContinent = continentVal.length === 1 && continentVal.includes('Any'); - let isDefaultBand = bandVal.length === 1 && bandVal.includes('All'); - let isDefaultMode = modeVal.length === 1 && modeVal.includes('All'); - let isDefaultFlags = flagsVal.length === 1 && flagsVal.includes('All'); - let isDefaultRequired = requiredVal.length === 0 || (requiredVal.length === 1 && requiredVal.includes('None')); + const isDefaultCwn = isDefaultFilterValue(filters.cwn); + const isDefaultDecont = isDefaultFilterValue(filters.deCont, 'Any'); + const isDefaultContinent = isDefaultFilterValue(filters.continent, 'Any'); + const isDefaultBand = isDefaultFilterValue(filters.band); + const isDefaultMode = isDefaultFilterValue(filters.mode); + const isDefaultFlags = isDefaultFilterValue(filters.additionalFlags); + const isDefaultRequired = filters.requiredFlags.length === 0; return !(isDefaultCwn && isDefaultDecont && isDefaultContinent && isDefaultBand && isDefaultMode && isDefaultFlags && isDefaultRequired); } - // Update filter icon based on whether filters are active + /** + * Update filter icon based on whether filters are active + */ function updateFilterIcon() { if (areFiltersApplied()) { $('#filterIcon').removeClass('fa-filter').addClass('fa-filter-circle-xmark text-success'); @@ -116,275 +184,207 @@ $(function() { } } - // Sync quick filter button states with their corresponding dropdown values - function syncQuickFilterButtons() { - let requiredFlags = ($('#requiredFlags').val() || []).filter(v => v !== 'None'); // Remove "None" - let additionalFlags = $('#additionalFlags').val() || []; - let cwnValues = $('#cwnSelect').val() || []; - let modeValues = $('#mode').val() || []; - let bandValues = $('#band').val() || []; - let decontValues = $('#decontSelect').val() || []; + /** + * Toggle a value in a multi-select filter + * @param {string} selectId - jQuery selector for the select element + * @param {string} value - Value to toggle in the selection + * @param {string} defaultValue - Default value to restore if selection becomes empty (default: 'All') + * @param {boolean} applyFiltersAfter - Whether to trigger filter application (default: true) + * @param {number} debounceMs - Debounce delay in milliseconds (default: 0 for no debounce) + * @param {boolean} updateBadges - Whether to call updateBandCountBadges() (default: false) + */ + function toggleFilterValue(selectId, value, defaultValue = 'All', applyFiltersAfter = true, debounceMs = 0, updateBadges = false) { + let currentValues = $(selectId).val() || []; - // LoTW button - if (requiredFlags.includes('lotw')) { - $('#toggleLotwFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleLotwFilter').removeClass('btn-success').addClass('btn-secondary'); + // Remove default value if present + if (currentValues.includes(defaultValue)) { + currentValues = currentValues.filter(v => v !== defaultValue); } - // New Continent button - if (requiredFlags.includes('newcontinent')) { - $('#toggleNewContinentFilter').removeClass('btn-secondary').addClass('btn-success'); + // Toggle the target value + if (currentValues.includes(value)) { + currentValues = currentValues.filter(v => v !== value); + // Restore default if empty + if (currentValues.length === 0) { + currentValues = [defaultValue]; + } } else { - $('#toggleNewContinentFilter').removeClass('btn-success').addClass('btn-secondary'); + currentValues.push(value); } - // New Country button (previously DXCC Needed) - if (requiredFlags.includes('newcountry')) { - $('#toggleDxccNeededFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleDxccNeededFilter').removeClass('btn-success').addClass('btn-secondary'); + // Update selectize + $(selectId).val(currentValues).trigger('change'); + syncQuickFilterButtons(); + + // Update badge counts if requested + if (updateBadges && typeof updateBandCountBadges === 'function') { + updateBandCountBadges(); } - // New Callsign button (previously Not Worked) - if (requiredFlags.includes('newcallsign')) { - $('#toggleNewCallsignFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleNewCallsignFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Contest button (now in Required Flags) - if (requiredFlags.includes('Contest')) { - $('#toggleContestFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleContestFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Geo Hunter button (stays in Additional Flags) - let geoFlags = ['POTA', 'SOTA', 'IOTA', 'WWFF']; - let hasGeoFlag = geoFlags.some(flag => additionalFlags.includes(flag)); - if (hasGeoFlag) { - $('#toggleGeoHunterFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleGeoHunterFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Fresh filter button - if (additionalFlags.includes('Fresh')) { - $('#toggleFreshFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleFreshFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // CW mode button - if (modeValues.includes('cw')) { - $('#toggleCwFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleCwFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Digi mode button - if (modeValues.includes('digi')) { - $('#toggleDigiFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#toggleDigiFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Phone mode button - if (modeValues.includes('phone')) { - $('#togglePhoneFilter').removeClass('btn-secondary').addClass('btn-success'); - } else { - $('#togglePhoneFilter').removeClass('btn-success').addClass('btn-secondary'); - } - - // Check if "All" is selected for bands, modes, and continents - let allBandsSelected = bandValues.length === 1 && bandValues.includes('All'); - - // For modes: check if "All" is selected OR if all individual modes are selected - let allModesSelected = (modeValues.length === 1 && modeValues.includes('All')) || - (modeValues.includes('cw') && modeValues.includes('digi') && modeValues.includes('phone')); - - // For continents: check if "Any" is selected OR if all continents are selected - // All continents: AF, AN, AS, EU, NA, OC, SA (7 continents) - let allContinentsSelected = (decontValues.length === 1 && decontValues.includes('Any')) || - (decontValues.includes('AF') && decontValues.includes('AN') && - decontValues.includes('AS') && decontValues.includes('EU') && - decontValues.includes('NA') && decontValues.includes('OC') && - decontValues.includes('SA')); - - // Band filter buttons - green if All, orange if specific band, gray if not selected - // Always update colors, even when CAT Control is enabled (so users can see which band is active) - let bandButtons = ['#toggle160mFilter', '#toggle80mFilter', '#toggle60mFilter', '#toggle40mFilter', - '#toggle30mFilter', '#toggle20mFilter', '#toggle17mFilter', '#toggle15mFilter', - '#toggle12mFilter', '#toggle10mFilter']; - let bandIds = ['160m', '80m', '60m', '40m', '30m', '20m', '17m', '15m', '12m', '10m']; - - bandButtons.forEach((btnId, index) => { - let $btn = $(btnId); - $btn.removeClass('btn-secondary btn-success'); - if (allBandsSelected) { - $btn.addClass('btn-success'); - } else if (bandValues.includes(bandIds[index])) { - $btn.addClass('btn-success'); - } else { - $btn.addClass('btn-secondary'); - } - }); - - // Band group buttons (VHF, UHF, SHF) - let groupButtons = [ - { id: '#toggleVHFFilter', group: 'VHF' }, - { id: '#toggleUHFFilter', group: 'UHF' }, - { id: '#toggleSHFFilter', group: 'SHF' } - ]; - - groupButtons.forEach(btn => { - let $btn = $(btn.id); - $btn.removeClass('btn-secondary btn-success'); - - if (allBandsSelected) { - $btn.addClass('btn-success'); - } else { - // Check if ALL bands in the group are selected (not just some) - const groupBands = getBandsInGroup(btn.group); - const allGroupBandsSelected = groupBands.every(b => bandValues.includes(b)); - - if (allGroupBandsSelected) { - $btn.addClass('btn-success'); + // Apply filters with optional debounce + if (applyFiltersAfter) { + if (debounceMs > 0) { + clearTimeout(window.filterDebounceTimer); + window.filterDebounceTimer = setTimeout(() => { + applyFilters(false); + }, debounceMs); } else { - $btn.addClass('btn-secondary'); + applyFilters(false); } } - }); // Mode buttons - green if All, orange if selected, blue if not - let modeButtons = [ - { id: '#toggleCwFilter', mode: 'cw', icon: 'fa-wave-square' }, - { id: '#toggleDigiFilter', mode: 'digi', icon: 'fa-keyboard' }, - { id: '#togglePhoneFilter', mode: 'phone', icon: 'fa-microphone' } + } + + /** + * Sync quick filter button states with their corresponding dropdown values + */ + function syncQuickFilterButtons() { + const filters = getAllFilterValues(); + + // Required flags buttons + const requiredFlagButtons = [ + { id: '#toggleLotwFilter', flag: 'lotw' }, + { id: '#toggleNewContinentFilter', flag: 'newcontinent' }, + { id: '#toggleDxccNeededFilter', flag: 'newcountry' }, + { id: '#toggleNewCallsignFilter', flag: 'newcallsign' }, + { id: '#toggleContestFilter', flag: 'Contest' } ]; - modeButtons.forEach(btn => { - let $btn = $(btn.id); - $btn.removeClass('btn-secondary btn-success'); - - if (allModesSelected) { - $btn.addClass('btn-success'); - } else if (modeValues.includes(btn.mode)) { - $btn.addClass('btn-success'); - } else { - $btn.addClass('btn-secondary'); - } + requiredFlagButtons.forEach(btn => { + updateButtonState(btn.id, filters.requiredFlags.includes(btn.flag)); }); - // Continent filter buttons - green if Any or selected, gray if not - // "All" button - green when all continents are selected - let $allContinentsBtn = $('#toggleAllContinentsFilter'); - $allContinentsBtn.removeClass('btn-secondary btn-success'); - if (allContinentsSelected) { - $allContinentsBtn.addClass('btn-success'); - } else { - $allContinentsBtn.addClass('btn-secondary'); - } + // Geo Hunter button (stays in Additional Flags) + const hasGeoFlag = GEO_FLAGS.some(flag => filters.additionalFlags.includes(flag)); + updateButtonState('#toggleGeoHunterFilter', hasGeoFlag); - let continentButtons = [ - { id: '#toggleAfricaFilter', continent: 'AF' }, - { id: '#toggleAntarcticaFilter', continent: 'AN' }, - { id: '#toggleAsiaFilter', continent: 'AS' }, - { id: '#toggleEuropeFilter', continent: 'EU' }, - { id: '#toggleNorthAmericaFilter', continent: 'NA' }, - { id: '#toggleOceaniaFilter', continent: 'OC' }, - { id: '#toggleSouthAmericaFilter', continent: 'SA' } - ]; + // Fresh filter button + updateButtonState('#toggleFreshFilter', filters.additionalFlags.includes('Fresh')); - continentButtons.forEach(btn => { - let $btn = $(btn.id); - $btn.removeClass('btn-secondary btn-success'); - if (allContinentsSelected) { - $btn.addClass('btn-success'); - } else if (decontValues.includes(btn.continent)) { - $btn.addClass('btn-success'); - } else { - $btn.addClass('btn-secondary'); - } + // Mode buttons + MODE_BUTTONS.forEach(btn => { + updateButtonState(btn.id, filters.mode.includes(btn.mode)); + }); + + // Check if "All" is selected for bands, modes, and continents + const allBandsSelected = isDefaultFilterValue(filters.band); + const allModesSelected = isDefaultFilterValue(filters.mode) || + (filters.mode.includes('cw') && filters.mode.includes('digi') && filters.mode.includes('phone')); + const allContinentsSelected = isDefaultFilterValue(filters.deCont, 'Any') || + (filters.deCont.includes('AF') && filters.deCont.includes('AN') && + filters.deCont.includes('AS') && filters.deCont.includes('EU') && + filters.deCont.includes('NA') && filters.deCont.includes('OC') && + filters.deCont.includes('SA')); + + // Band filter buttons - always update colors (for CAT Control visibility) + BAND_BUTTONS.forEach(btn => { + const isActive = allBandsSelected || filters.band.includes(btn.band); + updateButtonState(btn.id, isActive); + }); + + // Band group buttons (VHF, UHF, SHF) + BAND_GROUP_BUTTONS.forEach(btn => { + const groupBands = getBandsInGroup(btn.group); + const allGroupBandsSelected = groupBands.every(b => filters.band.includes(b)); + const isActive = allBandsSelected || allGroupBandsSelected; + updateButtonState(btn.id, isActive); + }); + + // Mode buttons + MODE_BUTTONS.forEach(btn => { + const isActive = allModesSelected || filters.mode.includes(btn.mode); + updateButtonState(btn.id, isActive); + }); + + // "All Continents" button + updateButtonState('#toggleAllContinentsFilter', allContinentsSelected); + + // Individual continent buttons + CONTINENT_BUTTONS.forEach(btn => { + const isActive = allContinentsSelected || filters.deCont.includes(btn.continent); + updateButtonState(btn.id, isActive); }); } - // Add checkbox-style indicators (☑/☐) to multi-select dropdowns + /** + * Add checkbox-style indicators (☑/☐) to multi-select dropdowns + * @param {string} selectId - ID of the select element + */ function updateSelectCheckboxes(selectId) { let $select = $('#' + selectId); $select.find('option').each(function() { let $option = $(this); let originalText = $option.data('original-text'); - if (!originalText) { - originalText = $option.text(); - $option.data('original-text', originalText); - } - - if ($option.is(':selected')) { - $option.text('☑ ' + originalText); - } else { - $option.text('☐ ' + originalText); - } - }); - } - - // Initialize checkbox indicators for all filter selects - function initFilterCheckboxes() { - ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags', 'requiredFlags'].forEach(function(selectId) { - updateSelectCheckboxes(selectId); - $('#' + selectId).on('change', function() { - updateSelectCheckboxes(selectId); - }); - }); - } - - // Handle "All"/"Any" option behavior in multi-selects - // If "All" is selected with other options, keep only "All" - // If nothing selected, default back to "All"/"Any" - function handleAllOption(selectId) { - $('#' + selectId).on('change', function() { - let selected = $(this).val() || []; - - if (selected.includes('All') || selected.includes('Any')) { - let allValue = selected.includes('All') ? 'All' : 'Any'; - if (selected.length > 1) { - $(this).val([allValue]); + if (!originalText) { + originalText = $option.text(); + $option.data('original-text', originalText); } - } else if (selected.length === 0) { - let allValue = (selectId === 'decontSelect' || selectId === 'continentSelect') ? 'Any' : 'All'; - $(this).val([allValue]); - } - updateFilterIcon(); - - // Sync button states when band, mode, or continent filters change - if (selectId === 'band' || selectId === 'mode' || selectId === 'decontSelect' || selectId === 'continentSelect') { - syncQuickFilterButtons(); - } - }); - } - - // Apply "All" handler to all filter dropdowns - ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags'].forEach(handleAllOption); - - // Required flags filter - handle "None" option - $('#requiredFlags').on('change', function() { - let currentValues = $(this).val() || []; - - // If "None" is selected, deselect all others - if (currentValues.includes('None')) { - if (currentValues.length > 1) { - // User selected something else, remove "None" - currentValues = currentValues.filter(v => v !== 'None'); - } - } else if (currentValues.length === 0) { - // If nothing is selected, select "None" - currentValues = ['None']; + if ($option.is(':selected')) { + $option.text('☑ ' + originalText); + } else { + $option.text('☐ ' + originalText); + } + }); } - $(this).val(currentValues); - updateFilterIcon(); - }); + // Initialize checkbox indicators for all filter selects + function initFilterCheckboxes() { + ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags', 'requiredFlags'].forEach(function(selectId) { + updateSelectCheckboxes(selectId); + $('#' + selectId).on('change', function() { + updateSelectCheckboxes(selectId); + }); + }); + } + + // Handle "All"/"Any" option behavior in multi-selects + // If "All" is selected with other options, keep only "All" + // If nothing selected, default back to "All"/"Any" + function handleAllOption(selectId) { + $('#' + selectId).on('change', function() { + let selected = $(this).val() || []; + + if (selected.includes('All') || selected.includes('Any')) { + let allValue = selected.includes('All') ? 'All' : 'Any'; + if (selected.length > 1) { + $(this).val([allValue]); + } + } else if (selected.length === 0) { + let allValue = (selectId === 'decontSelect' || selectId === 'continentSelect') ? 'Any' : 'All'; + $(this).val([allValue]); + } + + updateFilterIcon(); + + // Sync button states when band, mode, or continent filters change + if (selectId === 'band' || selectId === 'mode' || selectId === 'decontSelect' || selectId === 'continentSelect') { + syncQuickFilterButtons(); + } + }); + } + + // Apply "All" handler to all filter dropdowns + ['cwnSelect', 'decontSelect', 'continentSelect', 'band', 'mode', 'additionalFlags'].forEach(handleAllOption); + + // Required flags filter - handle "None" option + $('#requiredFlags').on('change', function() { + let currentValues = $(this).val() || []; + + // If "None" is selected, deselect all others + if (currentValues.includes('None')) { + if (currentValues.length > 1) { + // User selected something else, remove "None" + currentValues = currentValues.filter(v => v !== 'None'); + } + } else if (currentValues.length === 0) { + // If nothing is selected, select "None" + currentValues = ['None']; + } + + $(this).val(currentValues); + updateFilterIcon(); + }); // ======================================== // DATATABLE CONFIGURATION @@ -458,7 +458,6 @@ $(function() { } } - // Default row click: prepare QSO logging with callsign, frequency, mode let rowData = table.row(this).data(); if (!rowData) return; @@ -494,7 +493,8 @@ $(function() { } prepareLogging(call, qrg, mode, spotData); -}); return table; + }); + return table; } // ======================================== @@ -575,7 +575,7 @@ $(function() { allFilters = allFilters.concat(clientFilters); } - let loadingMessage = 'Loading data from DX Cluster'; + let loadingMessage = lang_bandmap_loading_data; if (allFilters.length > 0) { loadingMessage += '...'; } else { @@ -625,17 +625,17 @@ $(function() { } // Build tooltip for status message (fetch information) - let fetchTooltipLines = ['Last fetched for:']; + let fetchTooltipLines = [lang_bandmap_last_fetched + ':']; fetchTooltipLines.push('Band: ' + (lastFetchParams.band || 'All')); fetchTooltipLines.push('Continent: ' + (lastFetchParams.continent || 'All')); fetchTooltipLines.push('Mode: ' + (lastFetchParams.mode || 'All')); - fetchTooltipLines.push('Max Age: ' + (lastFetchParams.maxAge || '120') + ' min'); + fetchTooltipLines.push(lang_bandmap_max_age + ': ' + (lastFetchParams.maxAge || '120') + ' 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'); - fetchTooltipLines.push('Fetched at: ' + fetchTimeStr); + fetchTooltipLines.push(lang_bandmap_fetched_at + ': ' + fetchTimeStr); } $('#statusMessage').text(statusMessage).attr('title', fetchTooltipLines.join('\n')); @@ -1101,8 +1101,8 @@ $(function() { // de Cont column: spotter's continent data[0].push((single.dxcc_spotter && single.dxcc_spotter.cont) ? single.dxcc_spotter.cont : ''); -// de CQZ column: spotter's CQ Zone -data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spotter.cqz : ''); // Build medal badge - show only highest priority: continent > country > callsign + // de CQZ column: spotter's CQ Zone + data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spotter.cqz : ''); // Build medal badge - show only highest priority: continent > country > callsign let medals = ''; if (single.worked_continent === false) { // New Continent (not worked before) - Gold medal @@ -1771,81 +1771,19 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot return '' + content + ''; } - // Map frequency (in kHz) to ham band name + /** + * Map frequency (in kHz) to ham band name + * Uses frequencyToBand() from radiohelpers.js with 'kHz' parameter + * @param {number} freq_khz - Frequency in kilohertz + * @returns {string} Band name (e.g., '20m', '2m') or 'All' if not in a known band + */ 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'; + return frequencyToBand(freq_khz, 'kHz'); } - // Map individual bands to their band groups (VHF, UHF, SHF) - function getBandGroup(band) { - const VHF_BANDS = ['6m', '4m', '2m', '1.25m']; - const UHF_BANDS = ['70cm', '33cm', '23cm']; - const SHF_BANDS = ['13cm', '9cm', '6cm', '3cm', '1.25cm', '6mm', '4mm', '2.5mm', '2mm', '1mm']; - - if (VHF_BANDS.includes(band)) return 'VHF'; - if (UHF_BANDS.includes(band)) return 'UHF'; - if (SHF_BANDS.includes(band)) return 'SHF'; - return null; // MF/HF bands don't have groups - } - - // Get all bands in a band group - function getBandsInGroup(group) { - const BAND_GROUPS = { - 'VHF': ['6m', '4m', '2m', '1.25m'], - 'UHF': ['70cm', '33cm', '23cm'], - 'SHF': ['13cm', '9cm', '6cm', '3cm', '1.25cm', '6mm', '4mm', '2.5mm', '2mm', '1mm'] - }; - return BAND_GROUPS[group] || []; - } - - // 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; - } + // Use BAND_GROUPS from radiohelpers.js (loaded globally in footer) + // Note: These functions are now available globally, but we keep local references for consistency + // If radiohelpers not loaded, fallback to local definition (shouldn't happen in production) // Get selected values from multi-select dropdown function getSelectedValues(selectId) { @@ -2095,9 +2033,6 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot let bc_qsowin = new BroadcastChannel('qso_window'); let pong_rcvd = false; - // Debounce timer for de continent filter changes (3 second cooldown) - let decontFilterTimeout = null; - bc_qsowin.onmessage = function (ev) { if (ev.data == 'pong') { qso_window_last_seen=Date.now(); @@ -2117,53 +2052,9 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot let wait4pong = 2000; let check_intv = 100; - /** - * Determine appropriate radio mode based on spot mode and frequency - * Similar to dxwaterfall.js logic - * @param {string} spotMode - Mode from the spot (e.g., 'CW', 'SSB', 'FT8') - * @param {number} freqHz - Frequency in Hz - * @returns {string} Radio mode ('CW', 'USB', 'LSB', 'RTTY', etc.) - */ - 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 - } - - const modeUpper = spotMode.toUpperCase(); - - // CW modes - if (modeUpper === 'CW' || modeUpper === 'A1A') { - 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 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'; - } - - // Default: use frequency to determine USB/LSB - return freqHz < 10000000 ? 'LSB' : 'USB'; - } - /** * Tune radio to specified frequency when CAT Control is active + * Uses determineRadioMode() from radiohelpers.js for mode selection * @param {number} freqHz - Frequency in Hz * @param {string} mode - Mode (optional, e.g., 'USB', 'LSB', 'CW') */ @@ -2268,19 +2159,18 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot clearInterval(check_pong); let cl = message; // Use the message object with all fields - let newWindow = window.open(base_url + 'index.php/qso?manual=1', '_blank'); - - if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { - $('#errormessage').html(popup_warning).addClass('alert alert-danger').show(); - setTimeout(function() { - $('#errormessage').fadeOut(); - }, 3000); - } else { - newWindow.focus(); - // Show toast notification when opening new QSO window - showToast(lang_bandmap_qso_prepared, `${lang_bandmap_callsign_sent} ${call} ${lang_bandmap_sent_to_form}`, 'bg-success text-white', 3000); - } + let newWindow = window.open(base_url + 'index.php/qso?manual=1', '_blank'); + if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { + // Pop-up was blocked - show toast notification + if (typeof showToast === 'function') { + showToast(lang_bandmap_popup_blocked, lang_bandmap_popup_warning, 'bg-danger text-white', 5000); + } + } else { + newWindow.focus(); + // Show toast notification when opening new QSO window + showToast(lang_bandmap_qso_prepared, `${lang_bandmap_callsign_sent} ${call} ${lang_bandmap_sent_to_form}`, 'bg-success text-white', 3000); + } bc2qso.onmessage = function(ev) { if (ready_listener == true) { if (ev.data === 'ready') { @@ -2507,12 +2397,8 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Override updateCATui to add bandmap-specific behavior window.updateCATui = function(data) { - - + // Determine band from frequency const band = frequencyToBand(data.frequency); - - - // Store current radio frequency (convert Hz to kHz) currentRadioFrequency = data.frequency / 1000; @@ -2554,25 +2440,24 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot updateFrequencyGradientColors(); } - // Call cat.js's original updateCATui for standard CAT UI updates - if (typeof catJsUpdateCATui === 'function') { + // Call cat.js's original updateCATui for standard CAT UI updates + if (typeof catJsUpdateCATui === 'function') { + // Store current band selection before calling cat.js updateCATui + const bandBeforeUpdate = $("#band").val(); - // Store current band selection before calling cat.js updateCATui - const bandBeforeUpdate = $("#band").val(); + catJsUpdateCATui(data); - catJsUpdateCATui(data); - - // If CAT Control is OFF, restore the band selection - // (cat.js updateCATui automatically sets band based on frequency, but we don't want that on bandmap unless CAT Control is ON) - if (!isCatTrackingEnabled && bandBeforeUpdate) { - $("#band").val(bandBeforeUpdate); - updateSelectCheckboxes('band'); + // If CAT Control is OFF, restore the band selection + // (cat.js updateCATui automatically sets band based on frequency, but we don't want that on bandmap unless CAT Control is ON) + if (!isCatTrackingEnabled && bandBeforeUpdate) { + $("#band").val(bandBeforeUpdate); + updateSelectCheckboxes('band'); + } + } else { + console.warn('Bandmap: cat.js updateCATui not available'); } - } else { - console.warn('Bandmap: cat.js updateCATui not available'); - } -}; + }; $.fn.dataTable.moment(custom_date_format + ' HH:mm'); @@ -2592,7 +2477,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot container.addClass('bandmap-fullscreen'); $('body').addClass('fullscreen-active'); icon.removeClass('fa-expand').addClass('fa-compress'); - $('#fullscreenToggle').attr('title', 'Exit Fullscreen'); + $('#fullscreenToggle').attr('title', lang_bandmap_exit_fullscreen); isFullscreen = true; @@ -2617,7 +2502,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot container.removeClass('bandmap-fullscreen'); $('body').removeClass('fullscreen-active'); icon.removeClass('fa-compress').addClass('fa-expand'); - $(this).attr('title', 'Toggle Fullscreen'); + $(this).attr('title', lang_bandmap_toggle_fullscreen); isFullscreen = false; @@ -2734,216 +2619,22 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot 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); - }); + // Band filter buttons - Individual bands + $('#toggle160mFilter').on('click', () => toggleFilterValue('#band', '160m')); + $('#toggle80mFilter').on('click', () => toggleFilterValue('#band', '80m')); + $('#toggle60mFilter').on('click', () => toggleFilterValue('#band', '60m')); + $('#toggle40mFilter').on('click', () => toggleFilterValue('#band', '40m')); + $('#toggle30mFilter').on('click', () => toggleFilterValue('#band', '30m')); + $('#toggle20mFilter').on('click', () => toggleFilterValue('#band', '20m')); + $('#toggle17mFilter').on('click', () => toggleFilterValue('#band', '17m')); + $('#toggle15mFilter').on('click', () => toggleFilterValue('#band', '15m')); + $('#toggle12mFilter').on('click', () => toggleFilterValue('#band', '12m')); + $('#toggle10mFilter').on('click', () => toggleFilterValue('#band', '10m')); + $('#toggle6mFilter').on('click', () => toggleFilterValue('#band', '6m')); + $('#toggle4mFilter').on('click', () => toggleFilterValue('#band', '4m')); + $('#toggle2mFilter').on('click', () => toggleFilterValue('#band', '2m')); + $('#toggle70cmFilter').on('click', () => toggleFilterValue('#band', '70cm')); + $('#toggle23cmFilter').on('click', () => toggleFilterValue('#band', '23cm')); // Band group filter buttons (VHF, UHF, SHF, SAT) $('#toggleVHFFilter').on('click', function() { @@ -3024,205 +2715,20 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot updateBandCountBadges(); // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { + clearTimeout(window.filterDebounceTimer); + window.filterDebounceTimer = setTimeout(function() { applyFilters(false); }, 3000); }); - // 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'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#toggleAntarcticaFilter').on('click', function() { - let currentValues = $('#decontSelect').val() || []; - if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any'); - if (currentValues.includes('AN')) { - currentValues = currentValues.filter(v => v !== 'AN'); - if (currentValues.length === 0) currentValues = ['Any']; - } else { - currentValues.push('AN'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#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'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#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'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#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'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#toggleOceaniaFilter').on('click', function() { - let currentValues = $('#decontSelect').val() || []; - if (currentValues.includes('Any')) currentValues = currentValues.filter(v => v !== 'Any'); - if (currentValues.includes('OC')) { - currentValues = currentValues.filter(v => v !== 'OC'); - if (currentValues.length === 0) currentValues = ['Any']; - } else { - currentValues.push('OC'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Update badge counts immediately (before debounced filter application) - updateBandCountBadges(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); - - $('#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'); - // Check if all continents are now selected - if (currentValues.includes('AF') && currentValues.includes('AN') && currentValues.includes('AS') && - currentValues.includes('EU') && currentValues.includes('NA') && currentValues.includes('OC') && - currentValues.includes('SA')) { - currentValues = ['Any']; - } - } - $('#decontSelect').val(currentValues).trigger('change'); - syncQuickFilterButtons(); - - // Debounce the filter application (3 second cooldown) - clearTimeout(decontFilterTimeout); - decontFilterTimeout = setTimeout(function() { - applyFilters(false); - }, 3000); - }); + // Continent filter buttons (spotter's continent - de continent) - with 3s debounce + $('#toggleAfricaFilter').on('click', () => toggleFilterValue('#decontSelect', 'AF', 'Any', true, 3000, true)); + $('#toggleAntarcticaFilter').on('click', () => toggleFilterValue('#decontSelect', 'AN', 'Any', true, 3000, true)); + $('#toggleAsiaFilter').on('click', () => toggleFilterValue('#decontSelect', 'AS', 'Any', true, 3000, true)); + $('#toggleEuropeFilter').on('click', () => toggleFilterValue('#decontSelect', 'EU', 'Any', true, 3000, true)); + $('#toggleNorthAmericaFilter').on('click', () => toggleFilterValue('#decontSelect', 'NA', 'Any', true, 3000, true)); + $('#toggleOceaniaFilter').on('click', () => toggleFilterValue('#decontSelect', 'OC', 'Any', true, 3000, true)); + $('#toggleSouthAmericaFilter').on('click', () => toggleFilterValue('#decontSelect', 'SA', 'Any', true, 3000, true)); // Toggle LoTW User filter $('#toggleLotwFilter').on('click', function() { @@ -3478,7 +2984,7 @@ data[0].push((single.dxcc_spotter && single.dxcc_spotter.cqz) ? single.dxcc_spot // Add info icon and message to band filter label in popup const bandLabel = $('#band').closest('.mb-3').find('label'); if (!bandLabel.find('.cat-control-info').length) { - bandLabel.append(' '); + bandLabel.append(' '); } } From 41fdb5f3ef301917e66045cfc2d439c1a7043c0b Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 6 Nov 2025 02:40:56 +0100 Subject: [PATCH 39/51] DX Map --- application/views/bandmap/list.php | 45 +- application/views/interface_assets/footer.php | 1 + application/views/interface_assets/header.php | 4 +- assets/js/leaflet.polylineDecorator.js | 478 ++++++++++ assets/js/sections/bandmap_list.js | 883 ++++++++++++++++++ 5 files changed, 1406 insertions(+), 5 deletions(-) create mode 100644 assets/js/leaflet.polylineDecorator.js diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 9c1c8c62d..1566dc42c 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -83,8 +83,32 @@ var lang_bandmap_no_spots_filters = ""; var lang_bandmap_error_loading = ""; + // DX Map translation strings + var lang_bandmap_draw_spotters = ""; + var lang_bandmap_your_qth = ""; + var lang_bandmap_callsign = ""; + var lang_bandmap_frequency = ""; + var lang_bandmap_mode = ""; + var lang_bandmap_band = ""; + // Enable compact radio status display for bandmap page window.CAT_COMPACT_MODE = true; + + // Map configuration (matches QSO map settings) + var map_tile_server = 'optionslib->get_option('option_map_tile_server');?>'; + var map_tile_server_copyright = 'optionslib->get_option('option_map_tile_server_copyright');?>'; + var icon_dot_url = "assets/images/dot.png"; + + // User gridsquare for home position marker + var user_gridsquare = 'optionslib->get_option("station_gridsquare") ?? "") != "") { + echo $this->optionslib->get_option("station_gridsquare"); + } else if (null !== $this->config->item("locator")) { + echo $this->config->item("locator"); + } else { + echo "IO91WM"; + } + ?>'; @@ -100,7 +124,7 @@ "> -
+
@@ -391,12 +415,19 @@
+ + +
+ +
-
+
@@ -411,13 +442,21 @@
-
+
" aria-label="">
+ + +
"> "> [MHz]">">">">">">">">">">"> "> "> ">
diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index e54bdcc30..a794c3636 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1466,6 +1466,7 @@ mymap.on('mousemove', onQsoMapMove); uri->segment(1) == "bandmap" && $this->uri->segment(2) == "list") { ?> + diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index 9635cbc82..f8e29a4d8 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -282,14 +282,14 @@
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + spots.forEach(spot => { + const freqMHz = (spot.frequency / 1000).toFixed(3); + + // Color code callsign based on worked/confirmed status (matching bandmap table) + let callClass = ''; + if (spot.cnfmd_call) { + callClass = 'text-success'; // Green = confirmed + } else if (spot.worked_call) { + callClass = 'text-warning'; // Yellow = worked + } + + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
' + lang_bandmap_callsign + '' + lang_bandmap_frequency + '' + lang_bandmap_mode + '' + lang_bandmap_band + 'Spotter
' + spot.spotted + '' + freqMHz + '' + (spot.mode || '') + '' + (spot.band || '') + '' + (spot.spotter || '') + '
'; + + if (needsScroll) { + html += '
'; + } + + return html; + } + + /** + * Get color based on mode (using DX waterfall colors) + */ + function getSpotModeColor(mode) { + const modeUpper = (mode || '').toUpperCase(); + if (modeUpper === 'CW') return '#FFA500'; // Orange + if (['SSB', 'LSB', 'USB', 'AM', 'FM', 'PHONE'].includes(modeUpper)) return '#00FF00'; // Green + if (['FT8', 'FT4', 'RTTY', 'PSK', 'DIGITAL', 'DIGI'].some(m => modeUpper.includes(m))) return '#0096FF'; // Blue + return '#A020F0'; // Purple for other + } + + /** + * Get border color based on continent status (matching bandmap table colors) + */ + function getContinentStatusColor(cnfmdContinent, workedContinent) { + // Green = confirmed, Yellow = worked (not confirmed), Red = new (not worked) + if (cnfmdContinent) { + return '#28a745'; // Bootstrap success green (confirmed) + } else if (workedContinent) { + return '#ffc107'; // Bootstrap warning yellow (worked but not confirmed) + } + return '#dc3545'; // Bootstrap danger red (new/not worked) + } + + /** + * Get fill color based on DXCC status (matching bandmap table colors) + */ + function getDxccStatusColor(cnfmdDxcc, workedDxcc) { + // Green = confirmed, Yellow = worked (not confirmed), Red = new (not worked) + if (cnfmdDxcc) { + return '#28a745'; // Bootstrap success green (confirmed) + } else if (workedDxcc) { + return '#ffc107'; // Bootstrap warning yellow (worked but not confirmed) + } + return '#dc3545'; // Bootstrap danger red (new/not worked) + } + + /** + * Scroll to spot in the main DataTable + */ + function scrollToSpotInTable(callsign) { + const table = get_dtable(); + if (!table) return; + + // Find row with matching callsign + const row = table.rows().nodes().toArray().find(node => { + const callsignCell = $(node).find('td:eq(4)').text(); + return callsignCell.includes(callsign); + }); + + if (row) { + // Scroll to row + $('html, body').animate({ + scrollTop: $(row).offset().top - 100 + }, 500); + + // Briefly highlight the row + $(row).addClass('table-active'); + setTimeout(() => { + $(row).removeClass('table-active'); + }, 2000); + } + } + + /** + * Update DX Map with DXCC grouping + */ + function updateDxMap() { + if (!dxMap) { + console.log('DX Map: map not initialized'); + return; + } + + // Clear existing markers + dxccMarkers.forEach(marker => dxMap.removeLayer(marker)); + spotterMarkers.forEach(marker => dxMap.removeLayer(marker)); + connectionLines.forEach(line => dxMap.removeLayer(line)); + dxccMarkers = []; + spotterMarkers = []; + connectionLines = []; + + // Get filtered spots from DataTable + const table = get_dtable(); + if (!table) { + return; + } + + const filteredData = table.rows({ search: 'applied' }).data(); + if (filteredData.length === 0) { + return; + } + + // Build list of spots from filtered data + const spots = []; + filteredData.each(function(row) { + const freqMHzStr = row[2]; + const freqKHz = parseFloat(freqMHzStr) * 1000; + const callsignHtml = row[4]; + + let callsign = null; + let match = callsignHtml.match(/db\/([^"]+)"/); + if (match) { + callsign = match[1]; + } else { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = callsignHtml; + callsign = tempDiv.textContent.trim(); + } + + if (!callsign) return; + + const spot = cachedSpotData.find(s => + s.spotted === callsign && + Math.abs(s.frequency - freqKHz) < 5 + ); + + if (spot && spot.dxcc_spotted?.lat && spot.dxcc_spotted?.lng) { + spots.push(spot); + } + }); + + // Group by DXCC + const dxccGroups = groupSpotsByDXCC(spots); + + // Clear hover data for new update + hoverSpottersData.clear(); + + // Create one marker per DXCC + const bounds = []; + let markersCreated = 0; + + dxccGroups.forEach(dxccInfo => { + const lat = parseFloat(dxccInfo.lat); + const lng = parseFloat(dxccInfo.lng); + if (isNaN(lat) || isNaN(lng)) { + return; + } + + const count = dxccInfo.spots.length; + const countText = count > 1 ? ` x${count}` : ''; + + // Derive a short prefix from the first callsign + const firstCall = dxccInfo.spots[0]?.spotted || ''; + const prefix = firstCall.match(/^[A-Z0-9]{1,3}/)?.[0] || dxccInfo.entity.substring(0, 3).toUpperCase(); + + // Find the best (most optimistic) status in the group + // Priority: confirmed > worked > new + let bestContinentConfirmed = false; + let bestContinentWorked = false; + let bestDxccConfirmed = false; + let bestDxccWorked = false; + + dxccInfo.spots.forEach(spot => { + // Check continent status + if (spot.cnfmd_continent) { + bestContinentConfirmed = true; + } + if (spot.worked_continent) { + bestContinentWorked = true; + } + + // Check DXCC status + if (spot.cnfmd_dxcc) { + bestDxccConfirmed = true; + } + if (spot.worked_dxcc) { + bestDxccWorked = true; + } + }); + + const borderColor = getContinentStatusColor(bestContinentConfirmed, bestContinentWorked); + const fillColor = getDxccStatusColor(bestDxccConfirmed, bestDxccWorked); + + const marker = L.marker([lat, lng], { + icon: L.divIcon({ + className: 'dx-dxcc-marker', + html: `
+ ${prefix}${countText} +
`, + iconSize: [45, 18], + iconAnchor: [22, 9] + }) + }); + + // Store spotter data for this DXCC for hover functionality (incoming spots) + const spottersForThisDxcc = []; + dxccInfo.spots.forEach(spot => { + if (spot.dxcc_spotter?.dxcc_id && spot.dxcc_spotter.lat && spot.dxcc_spotter.lng) { + spottersForThisDxcc.push({ + dxccId: spot.dxcc_spotter.dxcc_id, + lat: spot.dxcc_spotter.lat, + lng: spot.dxcc_spotter.lng, + entity: spot.dxcc_spotter.entity, + flag: spot.dxcc_spotter.flag, + continent: spot.dxcc_spotter.cont, + spotter: spot.spotter + }); + } + }); + + // Store outgoing spots data (where this DXCC is the spotter) + const outgoingSpots = []; + spots.forEach(spot => { + if (spot.dxcc_spotter?.dxcc_id === dxccInfo.dxccId && + spot.dxcc_spotted?.dxcc_id && + spot.dxcc_spotted.lat && + spot.dxcc_spotted.lng) { + outgoingSpots.push({ + dxccId: spot.dxcc_spotted.dxcc_id, + lat: spot.dxcc_spotted.lat, + lng: spot.dxcc_spotted.lng, + entity: spot.dxcc_spotted.entity, + flag: spot.dxcc_spotted.flag, + continent: spot.dxcc_spotted.cont, + callsign: spot.callsign + }); + } + }); + + hoverSpottersData.set(String(dxccInfo.dxccId), { + spotters: spottersForThisDxcc, // incoming (red) + outgoing: outgoingSpots, // outgoing (green) + targetLat: lat, + targetLng: lng, + targetContinent: dxccInfo.continent + }); + + marker.bindPopup(createSpotTable(dxccInfo.spots, dxccInfo.entity, dxccInfo.flag), { + maxWidth: 500, + minWidth: 350 + }); + marker.on('popupopen', function() { + // Add click handlers to callsign links after popup opens + setTimeout(() => { + document.querySelectorAll('.spot-link').forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + const callsign = this.getAttribute('data-callsign'); + scrollToSpotInTable(callsign); + }); + }); + }, 10); + }); + marker.addTo(dxMap); + dxccMarkers.push(marker); + bounds.push([lat, lng]); + markersCreated++; + }); + + // Draw spotters if enabled + if (showSpotters) { + const spotterGroups = new Map(); + const drawnConnections = new Set(); // Track drawn connections + + spots.forEach(spot => { + const spotterId = spot.dxcc_spotter?.dxcc_id; + if (!spotterId) return; + + if (!spotterGroups.has(spotterId)) { + spotterGroups.set(spotterId, { + lat: spot.dxcc_spotter.lat, + lng: spot.dxcc_spotter.lng, + entity: spot.dxcc_spotter.entity, + flag: spot.dxcc_spotter.flag, + continent: spot.dxcc_spotter.cont, + spotIds: new Set(), + callsigns: [] + }); + } + + spotterGroups.get(spotterId).spotIds.add(spot.dxcc_spotted?.dxcc_id); + spotterGroups.get(spotterId).callsigns.push(spot.spotter); + }); + + // Detect bi-directional connections + const biDirectionalPairs = new Set(); + spotterGroups.forEach((spotterInfo, spotterId) => { + spotterInfo.spotIds.forEach(spotId => { + const spottedGroup = spotterGroups.get(spotId); + if (spottedGroup && spottedGroup.spotIds.has(spotterId)) { + // Create consistent pair key (sorted to avoid duplicates) + const pairKey = [spotterId, spotId].sort().join('-'); + biDirectionalPairs.add(pairKey); + } + }); + }); + + if (biDirectionalPairs.size > 0) { + console.log(`Found ${biDirectionalPairs.size} bi-directional connections:`, Array.from(biDirectionalPairs)); + } + + // Draw blue dots for spotters (permanent connections shown in orange) + spotterGroups.forEach((spotterInfo, spotterId) => { + const lat = parseFloat(spotterInfo.lat); + const lng = parseFloat(spotterInfo.lng); + if (isNaN(lat) || isNaN(lng)) return; + + const marker = L.circleMarker([lat, lng], { + radius: 5, + fillColor: '#ff9900', + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }); + + // Add tooltip showing spotter entity and count + const uniqueCallsigns = [...new Set(spotterInfo.callsigns)]; + const spotterCount = uniqueCallsigns.length; + const tooltipText = `${spotterInfo.flag || ''} ${spotterInfo.entity}
${spotterCount} spotter${spotterCount !== 1 ? 's' : ''}`; + marker.bindTooltip(tooltipText, { permanent: false, direction: 'top' }); + + marker.addTo(dxMap); + spotterMarkers.push(marker); + + // Draw lines to spotted DXCC entities (skip if same continent) + spotterInfo.spotIds.forEach(spotId => { + const dxccInfo = dxccGroups.get(spotId); + if (dxccInfo) { + // Skip line if both are in same continent + if (spotterInfo.continent && dxccInfo.continent && + spotterInfo.continent === dxccInfo.continent) { + return; + } + + const spotLat = parseFloat(dxccInfo.lat); + const spotLng = parseFloat(dxccInfo.lng); + if (!isNaN(spotLat) && !isNaN(spotLng)) { + // Check if this is a bi-directional connection + const pairKey = [spotterId, spotId].sort().join('-'); + const isBiDirectional = biDirectionalPairs.has(pairKey); + + // Only draw once for bi-directional pairs (using sorted key) + if (isBiDirectional && drawnConnections.has(pairKey)) { + return; + } + drawnConnections.add(pairKey); + + // Create line with proper pane (orange for permanent spotters) + const line = L.polyline([[lat, lng], [spotLat, spotLng]], { + color: '#ff9900', + weight: 1, + opacity: 0.5, + dashArray: '5, 5', + pane: 'connectionLines' + }); + + line.addTo(dxMap); + connectionLines.push(line); + + // Add arrow decorator(s) to show direction (spotter → spotted) + if (typeof L.polylineDecorator !== 'undefined') { + if (isBiDirectional) { + // Bi-directional: add two filled arrows pointing in opposite directions + const decorator = L.polylineDecorator(line, { + patterns: [ + { + offset: '30%', + repeat: 0, + symbol: L.Symbol.arrowHead({ + pixelSize: 10, + polygon: true, + pathOptions: { + fillColor: '#ff9900', + fillOpacity: 0.9, + color: '#cc6600', + weight: 1, + opacity: 1 + } + }) + }, + { + offset: '70%', + repeat: 0, + symbol: L.Symbol.arrowHead({ + pixelSize: 10, + polygon: true, + pathOptions: { + fillColor: '#ff9900', + fillOpacity: 0.9, + color: '#cc6600', + weight: 1, + opacity: 1 + } + }) + } + ] + }); + decorator.addTo(dxMap); + connectionLines.push(decorator); + } else { + // Uni-directional: single filled arrow + const decorator = L.polylineDecorator(line, { + patterns: [{ + offset: '50%', + repeat: 0, + symbol: L.Symbol.arrowHead({ + pixelSize: 10, + polygon: true, + pathOptions: { + fillColor: '#ff9900', + fillOpacity: 0.9, + color: '#cc6600', + weight: 1, + opacity: 1 + } + }) + }] + }); + decorator.addTo(dxMap); + connectionLines.push(decorator); + } + } + } + } + }); + }); + } + + // Set up hover event handlers only once + if (!hoverEventsInitialized) { + hoverEventsInitialized = true; + + $(document).on('mouseenter', '.dx-marker-label', function() { + if (!dxMap) { + console.log('Hover: Map not initialized'); + return; + } + + // Clear any existing hover elements + hoverSpotterMarkers.forEach(marker => { + try { dxMap.removeLayer(marker); } catch(e) {} + }); + hoverConnectionLines.forEach(line => { + try { dxMap.removeLayer(line); } catch(e) {} + }); + hoverSpotterMarkers = []; + hoverConnectionLines = []; + + const dxccId = String($(this).data('dxcc-id')); + if (!dxccId || dxccId === 'undefined') { + console.log('Hover: No dxccId found'); + return; + } + + const hoverData = hoverSpottersData.get(dxccId); + if (!hoverData) { + console.log('Hover: No hover data for', dxccId); + return; + } + + // Group incoming spotters by their DXCC to avoid duplicate lines + const spotterMap = new Map(); + if (hoverData.spotters && hoverData.spotters.length > 0) { + hoverData.spotters.forEach(spotter => { + if (!spotterMap.has(spotter.dxccId)) { + spotterMap.set(spotter.dxccId, { + lat: spotter.lat, + lng: spotter.lng, + entity: spotter.entity, + flag: spotter.flag, + continent: spotter.continent, + callsigns: [] + }); + } + spotterMap.get(spotter.dxccId).callsigns.push(spotter.spotter); + }); + } + + // Group outgoing spots by their DXCC + const outgoingMap = new Map(); + if (hoverData.outgoing && hoverData.outgoing.length > 0) { + hoverData.outgoing.forEach(spotted => { + if (!outgoingMap.has(spotted.dxccId)) { + outgoingMap.set(spotted.dxccId, { + lat: spotted.lat, + lng: spotted.lng, + entity: spotted.entity, + flag: spotted.flag, + continent: spotted.continent, + callsigns: [] + }); + } + outgoingMap.get(spotted.dxccId).callsigns.push(spotted.callsign); + }); + } + + // Use requestAnimationFrame for smooth rendering + requestAnimationFrame(() => { + // Draw incoming spotter markers and lines (RED) + spotterMap.forEach((spotterInfo) => { + const lat = parseFloat(spotterInfo.lat); + const lng = parseFloat(spotterInfo.lng); + if (isNaN(lat) || isNaN(lng)) return; + + try { + const marker = L.circleMarker([lat, lng], { + radius: 5, + fillColor: '#ff0000', + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }); + + const uniqueCallsigns = [...new Set(spotterInfo.callsigns)]; + const spotterCount = uniqueCallsigns.length; + const tooltipText = `${spotterInfo.flag || ''} ${spotterInfo.entity}
${spotterCount} spotter${spotterCount !== 1 ? 's' : ''}
→ Incoming`; + marker.bindTooltip(tooltipText, { permanent: false, direction: 'top' }); + + marker.addTo(dxMap); + hoverSpotterMarkers.push(marker); + + // Draw RED line (incoming: spotter → target) + const line = L.polyline([[lat, lng], [hoverData.targetLat, hoverData.targetLng]], { + color: '#ff0000', + weight: 2, + opacity: 0.7, + dashArray: '5, 5', + pane: 'connectionLines' + }); + + line.addTo(dxMap); + hoverConnectionLines.push(line); + + // Add arrow decorator to show direction (spotter → spotted) + if (L.polylineDecorator) { + const decorator = L.polylineDecorator(line, { + patterns: [{ + offset: '50%', + repeat: 0, + symbol: L.Symbol.arrowHead({ + pixelSize: 10, + polygon: true, + pathOptions: { + fillColor: '#ff0000', + fillOpacity: 0.9, + color: '#990000', + weight: 1, + opacity: 1 + } + }) + }] + }); + decorator.addTo(dxMap); + hoverConnectionLines.push(decorator); + } + } catch(e) { + console.error('Error drawing incoming hover spotter:', e); + } + }); + + // Draw outgoing spot markers and lines (GREEN) + outgoingMap.forEach((spottedInfo) => { + const lat = parseFloat(spottedInfo.lat); + const lng = parseFloat(spottedInfo.lng); + if (isNaN(lat) || isNaN(lng)) return; + + try { + const marker = L.circleMarker([lat, lng], { + radius: 5, + fillColor: '#00ff00', + color: '#fff', + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }); + + const uniqueCallsigns = [...new Set(spottedInfo.callsigns)]; + const spotCount = uniqueCallsigns.length; + const tooltipText = `${spottedInfo.flag || ''} ${spottedInfo.entity}
${spotCount} spot${spotCount !== 1 ? 's' : ''}
← Outgoing`; + marker.bindTooltip(tooltipText, { permanent: false, direction: 'top' }); + + marker.addTo(dxMap); + hoverSpotterMarkers.push(marker); + + // Draw GREEN line (outgoing: target → spotted) + const line = L.polyline([[hoverData.targetLat, hoverData.targetLng], [lat, lng]], { + color: '#00ff00', + weight: 2, + opacity: 0.7, + dashArray: '5, 5', + pane: 'connectionLines' + }); + + line.addTo(dxMap); + hoverConnectionLines.push(line); + + // Add arrow decorator to show direction (target → spotted) + if (L.polylineDecorator) { + const decorator = L.polylineDecorator(line, { + patterns: [{ + offset: '50%', + repeat: 0, + symbol: L.Symbol.arrowHead({ + pixelSize: 10, + polygon: true, + pathOptions: { + fillColor: '#00ff00', + fillOpacity: 0.9, + color: '#009900', + weight: 1, + opacity: 1 + } + }) + }] + }); + decorator.addTo(dxMap); + hoverConnectionLines.push(decorator); + } + } catch(e) { + console.error('Error drawing outgoing hover spot:', e); + } + }); + }); + }); + + $(document).on('mouseleave', '.dx-marker-label', function() { + if (!dxMap) return; + + // Use requestAnimationFrame for smooth cleanup + requestAnimationFrame(() => { + // Remove hover spotters and lines + hoverSpotterMarkers.forEach(marker => { + try { dxMap.removeLayer(marker); } catch(e) {} + }); + hoverConnectionLines.forEach(line => { + try { dxMap.removeLayer(line); } catch(e) {} + }); + hoverSpotterMarkers = []; + hoverConnectionLines = []; + }); + }); + } + + // Fit bounds + if (bounds.length > 0) { + dxMap.fitBounds(bounds, { padding: [50, 50], maxZoom: 8 }); + } + + setTimeout(() => { + if (dxMap) dxMap.invalidateSize(); + }, 100); + } + + /** + * Toggle DX Map visibility + */ + $('#dxMapButton').on('click', function() { + const container = $('#dxMapContainer'); + + if (dxMapVisible) { + // Hide map + container.slideUp(300); + dxMapVisible = false; + $(this).removeClass('btn-success').addClass('btn-primary'); + } else { + // Show map + if (!dxMap) { + initDxMap(); + } + container.slideDown(300, function() { + updateDxMap(); + // After first show, wait 1 second and reset zoom/viewport + setTimeout(() => { + if (dxMap) { + const table = get_dtable(); + if (table) { + const filteredData = table.rows({ search: 'applied' }).data(); + if (filteredData.length > 0) { + // Collect bounds from all visible markers + const mapBounds = []; + dxccMarkers.forEach(marker => { + const latLng = marker.getLatLng(); + if (latLng) mapBounds.push([latLng.lat, latLng.lng]); + }); + if (mapBounds.length > 0) { + dxMap.fitBounds(mapBounds, { padding: [50, 50], maxZoom: 8 }); + } + } + } + } + }, 1000); + }); + dxMapVisible = true; + $(this).removeClass('btn-primary').addClass('btn-success'); + } + }); + + // Update map when filters change (if map is visible) + const originalApplyFilters = applyFilters; + applyFilters = function(forceReload = false) { + originalApplyFilters(forceReload); + // Only update map if it's visible - don't waste resources + if (dxMapVisible && dxMap) { + setTimeout(updateDxMap, 500); + } + }; + }); From d70203947976681e28a86071cf9c672f5503864d Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 6 Nov 2025 09:45:04 +0100 Subject: [PATCH 40/51] Clear search button --- application/views/bandmap/list.php | 5 ++++- assets/js/sections/bandmap_list.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 1566dc42c..d37489220 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -442,8 +442,11 @@ -
+
" aria-label=""> +
diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index b45f9a189..f19d9551a 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -1909,6 +1909,7 @@ $(function() { // Clear text search $('#spotSearchInput').val(''); table.search('').draw(); + $('#clearSearchBtn').hide(); syncQuickFilterButtons(); updateFilterIcon(); @@ -1941,6 +1942,7 @@ $(function() { // Clear text search $('#spotSearchInput').val(''); table.search('').draw(); + $('#clearSearchBtn').hide(); syncQuickFilterButtons(); updateFilterIcon(); @@ -1963,6 +1965,12 @@ $(function() { $("#spotSearchInput").on("keyup", function() { table.search(this.value).draw(); + // Show/hide clear button based on input value + if (this.value.length > 0) { + $('#clearSearchBtn').show(); + } else { + $('#clearSearchBtn').hide(); + } }); $("#spotSearchInput").on("input", function() { @@ -1977,6 +1985,21 @@ $(function() { // Trigger search with new value table.search(newValue).draw(); } + + // Show/hide clear button based on input value + if (this.value.length > 0) { + $('#clearSearchBtn').show(); + } else { + $('#clearSearchBtn').hide(); + } + }); + + // Clear search button handler + $("#clearSearchBtn").on("click", function() { + $('#spotSearchInput').val(''); + table.search('').draw(); + $('#clearSearchBtn').hide(); + $('#spotSearchInput').focus(); }); $("#searchIcon").on("click", function() { From d67e7b6f8151cd9b02bb9b62f99f2d01fa0bce20 Mon Sep 17 00:00:00 2001 From: Fabian Berg Date: Thu, 6 Nov 2025 12:06:44 +0100 Subject: [PATCH 41/51] check for dom elements before loading the resizeobserver --- assets/js/sections/bandmap_list.js | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index f19d9551a..21c6a8333 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -3277,24 +3277,36 @@ $(function() { } } - // Initialize ResizeObserver to watch for container size changes - if (typeof ResizeObserver !== 'undefined') { + // Wait for table container to be ready, then initialize ResizeObserver + function initResizeObserver() { const tableContainer = document.querySelector('.table-responsive'); - if (tableContainer) { - const resizeObserver = new ResizeObserver(function(entries) { - handleResponsiveColumns(); - }); - resizeObserver.observe(tableContainer); - } - } else { - // Fallback for browsers without ResizeObserver support - $(window).on('resize', function() { + const dataTable = document.querySelector('#DataTables_Table_0_wrapper'); + + if (tableContainer && dataTable) { + // now we found the datatable and the table container is also available handleResponsiveColumns(); - }); + + if (typeof ResizeObserver !== 'undefined') { + const resizeObserver = new ResizeObserver(function(entries) { + handleResponsiveColumns(); + }); + resizeObserver.observe(tableContainer); + } else { + // Fallback for browsers without ResizeObserver support + $(window).on('resize', function() { + handleResponsiveColumns(); + }); + } + + } else { + // elements not ready yet, try again + setTimeout(initResizeObserver, 50); + return; + } } - // Initial call to set up column visibility - handleResponsiveColumns(); + // Start init process of the ResizeObserver + initResizeObserver(); // ======================================== // INITIALIZE CAT CONTROL STATE From 8f3c604e6aa04ce54a430bfb8e13312eb33eae76 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Fri, 7 Nov 2025 15:32:25 +0100 Subject: [PATCH 42/51] Fixes --- application/views/bandmap/list.php | 4 +++ assets/css/bandmap_list.css | 3 +- assets/js/sections/bandmap_list.js | 57 +++++++++++++++++++++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index d37489220..0d9161e13 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -220,6 +220,7 @@ + ' + lang_bandmap_draw_spotters + '
'; + div.innerHTML += ' ' + lang_bandmap_extend_map + '
'; return div; }; legend.addTo(dxMap); @@ -3517,6 +3518,21 @@ $(function() { showSpotters = this.checked; updateDxMap(); }); + + $('#extendMapCheckbox').on('change', function() { + const mapContainer = $('#dxMap'); + if (this.checked) { + // Double the height (345px -> 690px) + mapContainer.css('height', '690px'); + } else { + // Restore original height + mapContainer.css('height', '345px'); + } + // Invalidate map size to ensure it redraws properly + if (dxMap) { + dxMap.invalidateSize(); + } + }); }, 100); } From ff32621180fe1d611bd70d3fe8bcd0c870aab121 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Fri, 7 Nov 2025 17:00:01 +0100 Subject: [PATCH 47/51] Day/Night mode at map --- application/views/bandmap/list.php | 1 + application/views/interface_assets/footer.php | 1 + assets/js/sections/bandmap_list.js | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 938c4f0cd..9ce18b5ce 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -91,6 +91,7 @@ // DX Map translation strings var lang_bandmap_draw_spotters = ""; var lang_bandmap_extend_map = ""; + var lang_bandmap_show_daynight = ""; var lang_bandmap_your_qth = ""; var lang_bandmap_callsign = ""; var lang_bandmap_frequency = ""; diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 472bbf6d5..d0fb2da6e 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -1468,6 +1468,7 @@ mymap.on('mousemove', onQsoMapMove); + diff --git a/assets/js/sections/bandmap_list.js b/assets/js/sections/bandmap_list.js index 5fd7a6d39..499de2c28 100644 --- a/assets/js/sections/bandmap_list.js +++ b/assets/js/sections/bandmap_list.js @@ -3434,6 +3434,8 @@ $(function() { let connectionLines = []; let userHomeMarker = null; let showSpotters = false; + let showDayNight = true; // Day/Night terminator enabled by default + let terminatorLayer = null; // Store terminator layer reference let hoverSpottersData = new Map(); // Store spotter data for hover let hoverSpotterMarkers = []; // Temporary markers shown on hover let hoverConnectionLines = []; // Temporary lines shown on hover @@ -3471,6 +3473,9 @@ $(function() { addUserHomeMarker(); addSpottersControl(); + + // Initialize terminator (enabled by default) + updateTerminator(); } /** @@ -3509,6 +3514,7 @@ $(function() { const div = L.DomUtil.create("div", "legend"); div.innerHTML = ' ' + lang_bandmap_draw_spotters + '
'; div.innerHTML += ' ' + lang_bandmap_extend_map + '
'; + div.innerHTML += ' ' + lang_bandmap_show_daynight + '
'; return div; }; legend.addTo(dxMap); @@ -3533,9 +3539,37 @@ $(function() { dxMap.invalidateSize(); } }); + + $('#showDayNightCheckbox').on('change', function() { + showDayNight = this.checked; + updateTerminator(); + }); }, 100); } + /** + * Update day/night terminator layer + */ + function updateTerminator() { + if (!dxMap) return; + + // Remove existing terminator layer if it exists + if (terminatorLayer) { + dxMap.removeLayer(terminatorLayer); + terminatorLayer = null; + } + + // Add new terminator layer if enabled + if (showDayNight) { + terminatorLayer = L.terminator({ + fillOpacity: 0.3, + color: '#000', + weight: 1 + }); + terminatorLayer.addTo(dxMap); + } + } + /** * Group spots by DXCC entity */ @@ -4263,6 +4297,9 @@ $(function() { dxMap.fitBounds(bounds, { padding: [50, 50], maxZoom: 8 }); } + // Update day/night terminator + updateTerminator(); + setTimeout(() => { if (dxMap) dxMap.invalidateSize(); }, 100); From e0be81fa26e0b57befd0f8a3b205c23bf59c42cb Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Fri, 7 Nov 2025 18:35:13 +0100 Subject: [PATCH 48/51] Layout update --- application/views/bandmap/list.php | 162 +++++++------- application/views/interface_assets/footer.php | 7 +- assets/css/bandmap_list.css | 49 ++++ assets/js/cat.js | 209 +++++++++++++++++- assets/js/sections/bandmap_list.js | 72 +++++- 5 files changed, 399 insertions(+), 100 deletions(-) diff --git a/application/views/bandmap/list.php b/application/views/bandmap/list.php index 9ce18b5ce..e1c129f88 100644 --- a/application/views/bandmap/list.php +++ b/application/views/bandmap/list.php @@ -13,12 +13,12 @@ // Bandmap toast messages var lang_bandmap_popup_blocked = ""; var lang_bandmap_popup_warning = ""; - var lang_bandmap_cat_required = ""; - var lang_bandmap_enable_cat = ""; + var lang_bandmap_cat_required = ""; + var lang_bandmap_enable_cat = ""; var lang_bandmap_clear_filters = ""; - var lang_bandmap_band_preserved = ""; + var lang_bandmap_band_preserved = ""; var lang_bandmap_radio = ""; - var lang_bandmap_radio_none = ""; + var lang_bandmap_radio_none = ""; var lang_bandmap_radio_tuned = ""; var lang_bandmap_tuned_to = ""; var lang_bandmap_tuning_failed = ""; @@ -26,7 +26,7 @@ var lang_bandmap_qso_prepared = ""; var lang_bandmap_callsign_sent = ""; var lang_bandmap_sent_to_form = ""; - var lang_bandmap_cat_control = ""; + var lang_bandmap_cat_control = ""; var lang_bandmap_freq_changed = ""; var lang_bandmap_by_transceiver = ""; var lang_bandmap_freq_filter_set = ""; @@ -34,7 +34,7 @@ var lang_bandmap_waiting_radio = ""; var lang_bandmap_my_favorites = ""; var lang_bandmap_favorites_failed = ""; - var lang_bandmap_modes_applied = ""; + var lang_bandmap_modes_applied = ""; var lang_bandmap_favorites_applied = ""; // Bandmap filter status messages @@ -66,10 +66,10 @@ // Bandmap UI messages var lang_bandmap_exit_fullscreen = ""; var lang_bandmap_toggle_fullscreen = ""; - var lang_bandmap_cat_band_control = ""; + var lang_bandmap_cat_band_control = ""; var lang_bandmap_click_to_qso = ""; var lang_bandmap_ctrl_click_tune = ""; - var lang_bandmap_requires_cat = ""; + var lang_bandmap_requires_cat = ""; var lang_bandmap_spotter = ""; var lang_bandmap_comment = ""; var lang_bandmap_age = ""; @@ -88,6 +88,12 @@ var lang_bandmap_no_spots_filters = ""; var lang_bandmap_error_loading = ""; + // Offline radio status messages + var lang_bandmap_show_all_modes = ""; + var lang_bandmap_show_all_spots = ""; + + // DX Map Visualization + // DX Map translation strings var lang_bandmap_draw_spotters = ""; var lang_bandmap_extend_map = ""; @@ -98,8 +104,8 @@ var lang_bandmap_mode = ""; var lang_bandmap_band = ""; - // Enable compact radio status display for bandmap page - window.CAT_COMPACT_MODE = true; + // Enable ultra-compact radio status display for bandmap page (tooltip only) + window.CAT_COMPACT_MODE = 'ultra-compact'; // Map configuration (matches QSO map settings) var map_tile_server = 'optionslib->get_option('option_map_tile_server');?>'; @@ -149,41 +155,54 @@