Merge pull request #2479 from szporwolik/dev_dx_cluster_ui

[DX Cluster] Bandplan List Update
This commit is contained in:
Andreas Kristiansen
2025-11-10 12:45:27 +01:00
committed by GitHub
11 changed files with 6920 additions and 568 deletions

View File

@@ -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
@@ -71,4 +71,53 @@ class Bandmap extends CI_Controller {
$this->load->view('bandmap/list',$pageData);
$this->load->view('interface_assets/footer', $footerData);
}
// Get user's favorite bands and modes (active ones)
function get_user_favorites() {
session_write_close();
$this->load->model('bands');
$this->load->model('usermodes');
// Get active bands
$activeBands = $this->bands->get_user_bands_for_qso_entry(false); // false = only active
$bandList = [];
if (is_array($activeBands)) {
foreach ($activeBands as $group => $bands) {
if (is_array($bands)) {
foreach ($bands as $band) {
$bandList[] = $band;
}
}
}
}
// Get active modes (user-specific) and categorize them
$activeModes = $this->usermodes->active();
$modeCategories = [
'cw' => false,
'phone' => false,
'digi' => false
];
if ($activeModes) {
foreach ($activeModes as $mode) {
$qrgmode = strtoupper($mode->qrgmode ?? '');
if ($qrgmode === 'CW') {
$modeCategories['cw'] = true;
} elseif ($qrgmode === 'SSB') {
$modeCategories['phone'] = true;
} elseif ($qrgmode === 'DATA') {
$modeCategories['digi'] = true;
}
}
}
header('Content-Type: application/json');
echo json_encode([
'bands' => $bandList,
'modes' => $modeCategories
]);
}
}

View File

@@ -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;
@@ -563,18 +577,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 <region>" 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;
}
}
}

View File

@@ -1,135 +1,518 @@
<script>
// Global variables that don't require jQuery
var dxcluster_provider = "<?php echo base_url(); ?>index.php/dxcluster";
var cat_timeout_interval = "<?php echo $this->optionslib->get_option('cat_timeout_interval'); ?>";
var dxcluster_maxage = <?php echo $this->optionslib->get_option('dxcluster_maxage') ?? 60; ?>;
var custom_date_format = "<?php echo $custom_date_format ?>";
var popup_warning = "<?= __("Pop-up was blocked! Please allow pop-ups for this site permanently."); ?>";
var lang_click_to_prepare_logging = "<?= __("Click to prepare logging."); ?>";
// Detect OS for proper keyboard shortcuts
var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
var modKey = isMac ? 'Cmd' : 'Ctrl';
var lang_click_to_prepare_logging = "<?= __("Click to prepare logging."); ?> (" + modKey + "+Click <?= __("to tune frequency"); ?>)";
// Bandmap toast messages
var lang_bandmap_popup_blocked = "<?= __("Pop-up Blocked"); ?>";
var lang_bandmap_popup_warning = "<?= __("Pop-up was blocked! Please allow pop-ups for this site permanently."); ?>";
var lang_bandmap_cat_required = "<?= __("CAT Connection Required"); ?>";
var lang_bandmap_enable_cat = "<?= __("Enable CAT connection to tune the radio"); ?>";
var lang_bandmap_clear_filters = "<?= __("Clear Filters"); ?>";
var lang_bandmap_band_preserved = "<?= __("Band filter preserved (CAT connection is active)"); ?>";
var lang_bandmap_radio = "<?= __("Radio"); ?>";
var lang_bandmap_radio_none = "<?= __("Radio set to None - CAT connection disabled"); ?>";
var lang_bandmap_radio_tuned = "<?= __("Radio Tuned"); ?>";
var lang_bandmap_tuned_to = "<?= __("Tuned to"); ?>";
var lang_bandmap_tuning_failed = "<?= __("Tuning Failed"); ?>";
var lang_bandmap_tune_failed_msg = "<?= __("Failed to tune radio to frequency"); ?>";
var lang_bandmap_qso_prepared = "<?= __("QSO Prepared"); ?>";
var lang_bandmap_callsign_sent = "<?= __("Callsign"); ?>";
var lang_bandmap_sent_to_form = "<?= __("sent to logging form"); ?>";
var lang_bandmap_cat_control = "<?= __("CAT Connection"); ?>";
var lang_bandmap_freq_changed = "<?= __("Frequency filter changed to"); ?>";
var lang_bandmap_by_transceiver = "<?= __("by transceiver"); ?>";
var lang_bandmap_freq_filter_set = "<?= __("Frequency filter set to"); ?>";
var lang_bandmap_freq_outside = "<?= __("Frequency outside known bands - showing all bands"); ?>";
var lang_bandmap_waiting_radio = "<?= __("Waiting for radio data..."); ?>";
var lang_bandmap_my_favorites = "<?= __("My Favorites"); ?>";
var lang_bandmap_favorites_failed = "<?= __("Failed to load favorites"); ?>";
var lang_bandmap_modes_applied = "<?= __("Modes applied. Band filter preserved (CAT connection is active)"); ?>";
var lang_bandmap_favorites_applied = "<?= __("Applied your favorite bands and modes"); ?>";
// Bandmap filter status messages
var lang_bandmap_loading_data = "<?= __("Loading data from DX Cluster"); ?>";
var lang_bandmap_last_fetched = "<?= __("Last fetched for"); ?>";
var lang_bandmap_max_age = "<?= __("Max Age"); ?>";
var lang_bandmap_fetched_at = "<?= __("Fetched at"); ?>";
var lang_bandmap_next_update = "<?= __("Next update in"); ?>";
var lang_bandmap_minutes = "<?= __("minutes"); ?>";
var lang_bandmap_seconds = "<?= __("seconds"); ?>";
// Bandmap filter labels
var lang_bandmap_not_worked = "<?= __("Not worked"); ?>";
var lang_bandmap_worked = "<?= __("Worked"); ?>";
var lang_bandmap_confirmed = "<?= __("Confirmed"); ?>";
var lang_bandmap_worked_not_confirmed = "<?= __("Worked, not Confirmed"); ?>";
var lang_bandmap_lotw_user = "<?= __("LoTW User"); ?>";
var lang_bandmap_new_callsign = "<?= __("New Callsign"); ?>";
var lang_bandmap_new_continent = "<?= __("New Continent"); ?>";
var lang_bandmap_new_country = "<?= __("New Country"); ?>";
var lang_bandmap_worked_before = "<?= __("Worked Before"); ?>";
// Bandmap filter prefixes
var lang_bandmap_dxcc = "<?= __("DXCC"); ?>";
var lang_bandmap_band = "<?= __("Band"); ?>";
var lang_bandmap_mode = "<?= __("Mode"); ?>";
var lang_bandmap_continent = "<?= __("Continent"); ?>";
var lang_bandmap_all = "<?= __("All"); ?>";
var lang_bandmap_de = "<?= __("de"); ?>";
var lang_bandmap_spotted = "<?= __("spotted"); ?>";
// Bandmap tooltip messages
var lang_bandmap_fresh_spot = "<?= __("Fresh spot (< 5 minutes old)"); ?>";
var lang_bandmap_contest = "<?= __("Contest"); ?>";
var lang_bandmap_contest_name = "<?= __("Contest"); ?>"; // Same as above, for "Contest: NAME" format
var lang_bandmap_click_view_qrz = "<?= __("Click to view"); ?>";
var lang_bandmap_on_qrz = "<?= __("on QRZ.com"); ?>";
var lang_bandmap_see_details = "<?= __("See details for"); ?>";
var lang_bandmap_worked_on = "<?= __("Worked on"); ?>";
var lang_bandmap_not_worked_band = "<?= __("Not worked on this band"); ?>";
// Bandmap UI messages
var lang_bandmap_exit_fullscreen = "<?= __("Exit Fullscreen"); ?>";
var lang_bandmap_toggle_fullscreen = "<?= __("Toggle Fullscreen"); ?>";
var lang_bandmap_cat_band_control = "<?= __("Band filtering is controlled by your radio when CAT connection is enabled"); ?>";
var lang_bandmap_click_to_qso = "<?= __("Click to prepare logging"); ?>";
var lang_bandmap_ctrl_click_tune = "<?= __("to tune frequency"); ?>";
var lang_bandmap_requires_cat = "<?= __("(requires CAT connection)"); ?>";
var lang_bandmap_spotter = "<?= __("Spotter"); ?>";
var lang_bandmap_comment = "<?= __("Comment"); ?>";
var lang_bandmap_age = "<?= __("Age"); ?>";
var lang_bandmap_time = "<?= __("Time"); ?>";
var lang_bandmap_incoming = "<?= __("Incoming"); ?>";
var lang_bandmap_outgoing = "<?= __("Outgoing"); ?>";
var lang_bandmap_spots = "<?= __("spots"); ?>";
var lang_bandmap_spot = "<?= __("spot"); ?>";
var lang_bandmap_spotters = "<?= __("spotters"); ?>";
// DataTables messages
var lang_bandmap_loading_spots = "<?= __("Loading spots..."); ?>";
var lang_bandmap_no_spots_found = "<?= __("No spots found"); ?>";
var lang_bandmap_no_data = "<?= __("No data available"); ?>";
var lang_bandmap_no_spots_filters = "<?= __("No spots found for selected filters"); ?>";
var lang_bandmap_error_loading = "<?= __("Error loading spots. Please try again."); ?>";
// Offline radio status messages
var lang_bandmap_show_all_modes = "<?= __("Show all modes"); ?>";
var lang_bandmap_show_all_spots = "<?= __("Show all spots"); ?>";
// DX Map Visualization
// DX Map translation strings
var lang_bandmap_draw_spotters = "<?= __("Draw Spotters"); ?>";
var lang_bandmap_extend_map = "<?= __("Extend Map"); ?>";
var lang_bandmap_show_daynight = "<?= __("Show Day/Night"); ?>";
var lang_bandmap_your_qth = "<?= __("Your QTH"); ?>";
var lang_bandmap_callsign = "<?= __("Callsign"); ?>";
var lang_bandmap_frequency = "<?= __("Frequency"); ?>";
var lang_bandmap_mode = "<?= __("Mode"); ?>";
var lang_bandmap_band = "<?= __("Band"); ?>";
// 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 = '<?php echo $this->optionslib->get_option('option_map_tile_server');?>';
var map_tile_server_copyright = '<?php echo $this->optionslib->get_option('option_map_tile_server_copyright');?>';
var icon_dot_url = "<?php echo base_url();?>assets/images/dot.png";
// User gridsquare for home position marker
var user_gridsquare = '<?php
if (($this->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";
}
?>';
</script>
<style>
.spotted_call {
cursor: alias;
}
<link rel="stylesheet" type="text/css" href="<?php echo base_url(); ?>assets/css/bandmap_list.css" />
.kHz::after {
content: " kHz";
}
<div class="container" id="bandmapContainer">
<!-- Messages -->
<div class="messages my-1 mx-3"></div>
.bandlist {
-webkit-transition: all 15s ease;
-moz-transition: all 15s ease;
-o-transition: all 15s ease;
transition: 15s;
}
.fresh {
/* -webkit-transition: all 15s ease;
-moz-transition: all 15s ease;
-o-transition: all 15s ease; */
transition: all 500ms ease;
--bs-table-bg: #3981b2;
--bs-table-accent-bg: #3981b2;
}
tbody a {
color: inherit;
text-decoration: none;
}
.dataTables_wrapper {
margin: 10px;
}
</style>
<div class="container">
<br>
<center><button type="button" class="btn" id="menutoggle"><i class="fa fa-arrow-up" id="menutoggle_i"></i></button></center>
<div id="errormessage" style="display: none;"></div>
<h2 id="dxtitle"><?php echo $page_title; ?></h2>
<div class="tab-content" id="myTabContent">
<div class="messages my-1 me-2"></div>
<div class="d-flex align-items-center">
<label class="my-1 me-2" for="radio"><?= __("Radio"); ?></label>
<select class="form-select form-select-sm radios my-1 me-sm-2 w-auto" id="radio" name="radio">
<option value="0" selected="selected"><?= __("None"); ?></option>
<option value="ws"<?php if ($this->session->userdata('radio') == 'ws') { echo ' selected="selected"'; } ?>><?= __("WebSocket (Requires WLGate>1.1.10)"); ?></option>
<?php foreach ($radios->result() as $row) { ?>
<option value="<?php echo $row->id; ?>" <?php if ($this->session->userdata('radio') == $row->id) {
echo "selected=\"selected\"";
} ?>><?php echo $row->radio; ?></option>
<?php } ?>
</select>
<label class="my-1 me-2" for="cwnSelect"><?= __("DXCC-Status"); ?></label>
<select class="form-select form-select-sm my-1 me-sm-2 w-auto" id="cwnSelect" name="dxcluster_cwn" aria-describedby="dxcluster_cwnHelp" required>
<option value="All"><?= __("All"); ?></option>
<option value="notwkd"><?= __("Not worked"); ?></option>
<option value="wkd"><?= __("Worked"); ?></option>
<option value="cnf"><?= __("Confirmed"); ?></option>
<option value="ucnf"><?= __("Worked, not Confirmed"); ?></option>
</select>
<label class="my-1 me-2" for="decontSelect"><?= __("Spots de"); ?></label>
<select class="form-select form-select-sm my-1 me-sm-2 w-auto" id="decontSelect" name="dxcluster_decont" aria-describedby="dxcluster_decontHelp" required>
<option value="Any"><?= __("All"); ?></option>
<option value="AF" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AF') {echo " selected";} ?>><?= __("Africa"); ?></option>
<option value="AN" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AN') {echo " selected";} ?>><?= __("Antarctica"); ?></option>
<option value="AS" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AS') {echo " selected";} ?>><?= __("Asia"); ?></option>
<option value="EU" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'EU') {echo " selected";} ?>><?= __("Europe"); ?></option>
<option value="NA" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'NA') {echo " selected";} ?>><?= __("North America"); ?></option>
<option value="OC" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'OC') {echo " selected";} ?>><?= __("Oceania"); ?></option>
<option value="SA" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'SA') {echo " selected";} ?>><?= __("South America"); ?></option>
</select>
<label class="my-1 me-2" for="band"><?= __("Band"); ?></label>
<select id="band" class="form-select form-select-sm my-1 me-sm-2 w-auto" name="band">
<option value="All"><?= __("All"); ?></option>
<?php foreach ($bands as $key => $bandgroup) {
echo '<optgroup label="' . strtoupper($key) . '">';
foreach ($bandgroup as $band) {
echo '<option value="' . $band . '"';
if ($band == "20m") echo ' selected';
echo '>' . $band . '</option>' . "\n";
}
echo '</optgroup>';
}
?>
</select>
<label class="my-1 me-2" for="mode"><?= __("Mode"); ?></label>
<select id="mode" class="form-select form-select-sm my-1 me-sm-2 w-auto" name="mode">
<option value="All"><?= __("All"); ?></option>
<option value="phone"><?= __("Phone"); ?></option>
<option value="cw"><?= __("CW"); ?></option>
<option value="digi"><?= __("Digi"); ?></option>
</select>
<!-- DX Cluster Panel -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<a href="<?php echo base_url(); ?>" title="<?= __("Return to Home"); ?>">
<img class="headerLogo me-2 bandmap-logo-fullscreen" src="<?php echo base_url(); ?>assets/logo/<?php echo $this->optionslib->get_logo('header_logo'); ?>.png" alt="Logo" style="height: 32px; width: auto; cursor: pointer;" />
</a>
<h5 class="mb-0"><?= __("DX Cluster"); ?></h5>
</div>
<div class="d-flex align-items-center gap-3">
<a href="https://www.wavelog.org" target="_blank" class="fullscreen-wavelog-text" style="display: none; font-weight: 500; color: var(--bs-body-color); text-decoration: none;">www.wavelog.org</a>
<a href="https://github.com/wavelog/wavelog/wiki/DXCluster" target="_blank" title="<?= __("DX Cluster Help"); ?>" style="cursor: pointer; padding: 0.5rem; margin: -0.5rem; color: var(--bs-body-color); text-decoration: none; display: inline-flex; align-items: center;">
<i class="fas fa-question-circle" style="font-size: 1.2rem;"></i>
</a>
<div id="fullscreenToggleWrapper" style="cursor: pointer; padding: 0.5rem; margin: -0.5rem;">
<button type="button" class="btn btn-sm" id="fullscreenToggle" title="<?= __("Toggle Fullscreen"); ?>" style="background: none; border: none; padding: 0.5rem;">
<i class="fas fa-expand" id="fullscreenIcon" style="font-size: 1.2rem;"></i>
</button>
</div>
</div>
</div>
<div class="card-body pt-1">
</div>
<!-- Filters Section with darker background and rounded corners -->
<div class="menu-bar">
<!-- Row 1: CAT Connection, Radio Selector, Radio Status (left) | de Continents (right) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- Left: CAT Connection Button -->
<button class="btn btn-sm btn-secondary flex-shrink-0" type="button" id="toggleCatTracking" title="<?= __("When selected the filters will be set basing on your current radio status"); ?>">
<i class="fas fa-radio"></i> <span class="d-none d-sm-inline"><?= __("CAT Connection"); ?></span>
</button>
<p>
<div class='m-2'>
<table style="width:100%;" class="table-sm table spottable table-bordered table-hover table-striped table-condensed">
<thead>
<tr class="log_title titles">
<th style="width:200px;"><?= __("Date"); ?>/<?= __("Time"); ?></th>
<th style="width:150px;"><?= __("Frequency"); ?></th>
<th><?= __("Call"); ?></th>
<th><?= __("DXCC"); ?></th>
<th style="width:30px;"><?= __("WAC"); ?></th>
<th style="width:150px;"><?= __("Spotter"); ?></th>
<th><?= __("Message"); ?></th>
<th><?= __("Last Worked"); ?></th>
<th><?= __("Mode"); ?></th>
</tr>
</thead>
<!-- Radio Selector Dropdown -->
<small class="text-muted me-1 flex-shrink-0 d-none d-md-inline"><?= __("Radio:"); ?></small>
<select class="form-select form-select-sm radios flex-shrink-0" id="radio" name="radio" style="width: auto; min-width: 150px;">
<option value="0" selected="selected"><?= __("None"); ?></option>
<option value="ws"<?php if ($this->session->userdata('radio') == 'ws') { echo ' selected="selected"'; } ?>><?= __("Live - ") . __("WebSocket (Requires WLGate>=1.1.10)"); ?></option>
<?php foreach ($radios->result() as $row) { ?>
<option value="<?php echo $row->id; ?>" <?php if($this->session->userdata('radio') == $row->id) { echo "selected=\"selected\""; } ?>><?= __("Polling - ") . $row->radio; ?><?php if ($radio_last_updated->id == $row->id) { echo "(".__("last updated").")"; } else { echo ''; } ?></option>
<?php } ?>
</select>
<tbody class="spots_table_contents">
</tbody>
</table>
</div>
<!-- Radio Status Panel (ultra-compact, dynamically populated by JavaScript) -->
<div id="radio_status" class="d-flex align-items-center" style="flex: 1 1 auto; min-width: 0;"></div>
<!-- Right: de Continent Filter Buttons -->
<div class="d-flex flex-wrap gap-2 align-items-center">
<small class="text-muted me-1 flex-shrink-0"><?= __("de:"); ?></small>
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleAllContinentsFilter" title="<?= __("Select all continents"); ?>"><?= __("All"); ?></button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAfricaFilter" title="<?= __("Toggle Africa continent filter"); ?>">AF</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAntarcticaFilter" title="<?= __("Toggle Antarctica continent filter"); ?>">AN</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleAsiaFilter" title="<?= __("Toggle Asia continent filter"); ?>">AS</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleEuropeFilter" title="<?= __("Toggle Europe continent filter"); ?>">EU</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleNorthAmericaFilter" title="<?= __("Toggle North America continent filter"); ?>">NA</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleOceaniaFilter" title="<?= __("Toggle Oceania continent filter"); ?>">OC</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleSouthAmericaFilter" title="<?= __("Toggle South America continent filter"); ?>">SA</button>
</div>
</div>
</div>
<!-- Row 2: Advanced Filters, Favorites, Clear Filters | Band Filters (left) and Mode Filters (right) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- Left: Advanced Filters, Favorites, Clear Filters, and Band Filter Buttons -->
<div class="d-flex flex-wrap gap-2 align-items-center">
<!-- Button Group: Advanced Filters + Favorites + Clear Filters -->
<div class="btn-group flex-shrink-0" role="group">
<div class="dropdown">
<!-- Filter Dropdown Button -->
<button class="btn btn-sm btn-secondary" type="button" id="filterDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" title="<?= __("Advanced Filters"); ?>">
<i class="fas fa-filter" id="filterIcon"></i>
</button>
<div class="dropdown-menu dropdown-menu-start p-3 mt-2" aria-labelledby="filterDropdown" style="min-width: 1264px; max-width: 95vw; max-height: 98vh; overflow-y: auto;">
<!-- Filter tip -->
<div class="filter-tip">
<i class="fas fa-info-circle"></i>
<span id="filterTipText"></span>
</div>
<script>
// Set filter tip text based on OS
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"); ?>';
});
</script>
<div class="row">
<!-- Column 1: DXCC-Status and Mode -->
<div class="mb-3 col-12 col-sm-6 col-md-4 col-lg">
<label class="form-label d-block filter-label-small" for="cwnSelect"><?= __("DXCC-Status"); ?></label>
<select class="form-select form-select-sm filter-short" id="cwnSelect" name="dxcluster_cwn" multiple="multiple" aria-describedby="dxcluster_cwnHelp">
<option value="All" selected><?= __("All"); ?></option>
<option value="notwkd"><?= __("Not worked"); ?></option>
<option value="wkd"><?= __("Worked"); ?></option>
<option value="cnf"><?= __("Confirmed"); ?></option>
<option value="ucnf"><?= __("Worked, not Confirmed"); ?></option>
</select>
<label class="form-label d-block filter-label-small mt-3" for="mode"><?= __("Mode"); ?></label>
<select id="mode" class="form-select form-select-sm filter-short" name="mode" multiple="multiple">
<option value="All" selected><?= __("All"); ?></option>
<option value="phone"><?= __("Phone"); ?></option>
<option value="cw"><?= __("CW"); ?></option>
<option value="digi"><?= __("Digi"); ?></option>
</select>
</div>
<!-- Column 2: Required Flags and Additional Flags -->
<div class="mb-3 col-12 col-sm-6 col-md-4 col-lg">
<label class="form-label d-block filter-label-small" for="requiredFlags"><?= __("Required Flags"); ?></label>
<select id="requiredFlags" class="form-select form-select-sm filter-short" name="required_flags" multiple="multiple">
<option value="None" selected><?= __("None"); ?></option>
<option value="lotw"><?= __("LoTW User"); ?></option>
<option value="newcontinent"><?= __("New Continent"); ?></option>
<option value="newcountry"><?= __("New Country"); ?></option>
<option value="newcallsign"><?= __("New Callsign"); ?></option>
<option value="workedcallsign"><?= __("Worked Callsign"); ?></option>
<option value="Contest"><?= __("Contest"); ?></option>
<option value="dxspot"><?= __("DX Spot"); ?></option>
</select>
<label class="form-label d-block filter-label-small mt-3" for="additionalFlags"><?= __("Additional Flags"); ?></label>
<select id="additionalFlags" class="form-select form-select-sm filter-short" name="additional_flags" multiple="multiple">
<option value="All" selected><?= __("All"); ?></option>
<option value="SOTA"><?= __("SOTA"); ?></option>
<option value="POTA"><?= __("POTA"); ?></option>
<option value="WWFF"><?= __("WWFF"); ?></option>
<option value="IOTA"><?= __("IOTA"); ?></option>
<option value="Fresh"><?= __("Fresh (< 5 min)"); ?></option>
</select>
</div>
<!-- Column 3: Spots de Continent -->
<div class="mb-3 col-12 col-sm-6 col-md-4 col-lg">
<label class="form-label d-block filter-label-small" for="decontSelect"><?= __("Spots de Continent"); ?></label>
<select class="form-select form-select-sm" id="decontSelect" name="dxcluster_decont" multiple="multiple" aria-describedby="dxcluster_decontHelp">
<option value="Any"<?php if ($this->optionslib->get_option('dxcluster_decont') == '' || $this->optionslib->get_option('dxcluster_decont') == 'Any') {echo " selected";} ?>><?= __("All"); ?></option>
<option value="AF" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AF') {echo " selected";} ?>><?= __("Africa"); ?></option>
<option value="AN" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AN') {echo " selected";} ?>><?= __("Antarctica"); ?></option>
<option value="AS" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'AS') {echo " selected";} ?>><?= __("Asia"); ?></option>
<option value="EU" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'EU') {echo " selected";} ?>><?= __("Europe"); ?></option>
<option value="NA" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'NA') {echo " selected";} ?>><?= __("North America"); ?></option>
<option value="OC" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'OC') {echo " selected";} ?>><?= __("Oceania"); ?></option>
<option value="SA" <?php if ($this->optionslib->get_option('dxcluster_decont') == 'SA') {echo " selected";} ?>><?= __("South America"); ?></option>
</select>
</div>
<!-- Column 4: Spotted Station Continent -->
<div class="mb-3 col-12 col-sm-6 col-md-4 col-lg">
<label class="form-label d-block filter-label-small" for="continentSelect"><?= __("Spotted Station Continent"); ?></label>
<select id="continentSelect" class="form-select form-select-sm" name="continent" multiple="multiple">
<option value="Any" selected><?= __("All"); ?></option>
<option value="AF"><?= __("Africa"); ?></option>
<option value="AN"><?= __("Antarctica"); ?></option>
<option value="AS"><?= __("Asia"); ?></option>
<option value="EU"><?= __("Europe"); ?></option>
<option value="NA"><?= __("North America"); ?></option>
<option value="OC"><?= __("Oceania"); ?></option>
<option value="SA"><?= __("South America"); ?></option>
</select>
</div>
<!-- Column 5: Band -->
<div class="mb-3 col-12 col-sm-6 col-md-4 col-lg">
<label class="form-label d-block filter-label-small" for="band"><?= __("Band"); ?></label>
<select id="band" class="form-select form-select-sm" name="band" multiple="multiple">
<option value="All" selected><?= __("All"); ?></option>
<optgroup label="MF">
<option value="160m">160m</option>
</optgroup>
<optgroup label="HF">
<option value="80m">80m</option>
<option value="60m">60m</option>
<option value="40m">40m</option>
<option value="30m">30m</option>
<option value="20m">20m</option>
<option value="17m">17m</option>
<option value="15m">15m</option>
<option value="12m">12m</option>
<option value="10m">10m</option>
</optgroup>
<optgroup label="VHF">
<option value="6m">6m</option>
<option value="4m">4m</option>
<option value="2m">2m</option>
<option value="1.25m">1.25m</option>
</optgroup>
<optgroup label="UHF">
<option value="70cm">70cm</option>
<option value="33cm">33cm</option>
<option value="23cm">23cm</option>
</optgroup>
<optgroup label="SHF">
<option value="13cm">13cm</option>
<option value="9cm">9cm</option>
<option value="6cm">6cm</option>
<option value="3cm">3cm</option>
<option value="1.25cm">1.25cm</option>
<option value="6mm">6mm</option>
<option value="4mm">4mm</option>
<option value="2.5mm">2.5mm</option>
<option value="2mm">2mm</option>
<option value="1mm">1mm</option>
</optgroup>
</select>
</div>
</div>
<!-- Buttons in popup -->
<div class="text-center mt-3">
<button type="button" class="btn btn-sm btn-success me-2" id="applyFiltersButtonPopup">
<i class="fas fa-check"></i> <?= __("Apply Filters"); ?>
</button>
<button type="button" class="btn btn-sm btn-secondary" id="clearFiltersButton" title="<?= __("Clear Filters"); ?>">
<i class="fas fa-filter-circle-xmark text-danger"></i>
</button>
</div>
</div>
</div>
<!-- Favorites Button (part of button group) -->
<button class="btn btn-sm btn-secondary" type="button" id="toggleFavoritesFilter" title="<?= __("Apply your favorite bands and modes (configured in Band and Mode settings)"); ?>">
<i class="fas fa-star text-warning"></i>
</button>
<!-- Clear Filters Button (part of button group) -->
<button class="btn btn-sm btn-secondary" type="button" id="clearFiltersButtonQuick" title="<?= __("Clear all filters except De Continent"); ?>">
<i class="fas fa-filter-circle-xmark text-danger"></i>
</button>
</div>
<!-- MF Band -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggle160mFilter" title="<?= __("Toggle 160m band filter"); ?>">160m</button>
</div>
<!-- HF Bands -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggle80mFilter" title="<?= __("Toggle 80m band filter"); ?>">80m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle60mFilter" title="<?= __("Toggle 60m band filter"); ?>">60m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle40mFilter" title="<?= __("Toggle 40m band filter"); ?>">40m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle30mFilter" title="<?= __("Toggle 30m band filter"); ?>">30m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle20mFilter" title="<?= __("Toggle 20m band filter"); ?>">20m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle17mFilter" title="<?= __("Toggle 17m band filter"); ?>">17m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle15mFilter" title="<?= __("Toggle 15m band filter"); ?>">15m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle12mFilter" title="<?= __("Toggle 12m band filter"); ?>">12m</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggle10mFilter" title="<?= __("Toggle 10m band filter"); ?>">10m</button>
</div>
<!-- VHF/UHF/SHF Bands -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleVHFFilter" title="<?= __("Toggle VHF bands filter"); ?>">VHF</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleUHFFilter" title="<?= __("Toggle UHF bands filter"); ?>">UHF</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleSHFFilter" title="<?= __("Toggle SHF bands filter"); ?>">SHF</button>
</div>
</div>
<!-- Spacer to push modes to the right -->
<div class="flex-grow-1"></div>
<!-- Right: Mode Filter Buttons -->
<div class="d-flex flex-wrap gap-2 align-items-center">
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleCwFilter" title="<?= __("Toggle CW mode filter"); ?>">CW</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleDigiFilter" title="<?= __("Toggle Digital mode filter"); ?>">Digi</button>
<button class="btn btn-sm btn-secondary" type="button" id="togglePhoneFilter" title="<?= __("Toggle Phone mode filter"); ?>">Phone</button>
</div>
</div>
</div>
<!-- Row 3: Quick Filters -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- LoTW Users Button (separate) -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleLotwFilter" title="<?= __("Toggle LoTW User filter"); ?>">
<i class="fas fa-upload"></i> <span class="d-none d-sm-inline"><?= __("LoTW users"); ?></span>
</button>
</div>
<!-- DX Spot, Continent, Country, Callsign Group -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleDxSpotFilter" title="<?= __("Toggle DX Spot filter (spotted continent ≠ spotter continent)"); ?>">
<i class="fas fa-globe"></i> <span class="d-none d-sm-inline"><?= __("DX Spots"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleNewContinentFilter" title="<?= __("Toggle New Continent filter"); ?>">
<i class="fas fa-medal" style="color: #FFD700;"></i> <span class="d-none d-sm-inline"><?= __("New Continents"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleDxccNeededFilter" title="<?= __("Toggle New Country filter"); ?>">
<i class="fas fa-medal" style="color: #C0C0C0;"></i> <span class="d-none d-sm-inline"><?= __("New DXCCs"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleNewCallsignFilter" title="<?= __("Toggle New Callsign filter"); ?>">
<i class="fas fa-medal" style="color: #CD7F32;"></i> <span class="d-none d-sm-inline"><?= __("New Callsigns"); ?></span>
</button>
</div>
<!-- Fresh, Contest, Ref. Hunter Group -->
<div class="btn-group flex-shrink-0" role="group">
<button class="btn btn-sm btn-secondary" type="button" id="toggleFreshFilter" title="<?= __("Toggle Fresh spots filter (< 5 minutes old)"); ?>">
<i class="fas fa-bolt"></i> <span class="d-none d-sm-inline"><?= __("Fresh Spots"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleContestFilter" title="<?= __("Toggle Contest filter"); ?>">
<i class="fas fa-trophy"></i> <span class="d-none d-sm-inline"><?= __("Contest Spots"); ?></span>
</button>
<button class="btn btn-sm btn-secondary" type="button" id="toggleGeoHunterFilter" title="<?= __("Toggle Geo Hunter (POTA/SOTA/IOTA/WWFF)"); ?>">
<i class="fas fa-hiking"></i> <span class="d-none d-sm-inline"><?= __("Referenced Spots"); ?></span>
</button>
</div>
<!-- DX Map Button (right side) -->
<div class="ms-auto">
<button class="btn btn-sm btn-primary" type="button" id="dxMapButton" title="<?= __("Open DX Map view"); ?>">
<i class="fas fa-map-marked-alt"></i> <span class="d-none d-sm-inline"><?= __("DX Map"); ?></span>
</button>
</div>
</div>
<!-- Row 5: Status Bar (70%) and Search (30%) -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<!-- Status Bar - 70% -->
<div style="flex: 1 1 0; min-width: 300px;">
<div class="status-bar">
<div class="status-bar-inner">
<div class="status-bar-left">
<span id="statusMessage"></span>
</div>
<div class="status-bar-right">
<i class="fas fa-hourglass-half me-1" id="refreshIcon"></i>
<span id="refreshTimer"></span>
</div>
</div>
</div>
</div>
<!-- Search Input - 30% -->
<div class="input-group input-group-sm" style="flex: 0 0 auto; min-width: 200px; max-width: 400px; position: relative;">
<input type="text" class="form-control" id="spotSearchInput" placeholder="<?= __("Search spots..."); ?>" aria-label="<?= __("Search"); ?>">
<button class="btn btn-sm" id="clearSearchBtn" style="position: absolute; right: 40px; top: 50%; transform: translateY(-50%); z-index: 10; background: transparent; border: none; padding: 0 5px; display: none; cursor: pointer;">
<i class="fas fa-times" style="color: #6c757d;"></i>
</button>
<span class="input-group-text search-icon-clickable" id="searchIcon"><i class="fas fa-search"></i></span>
</div>
</div>
</div>
<!-- DX Map Container (initially hidden) -->
<div id="dxMapContainer" style="display: none; margin-bottom: 15px;">
<div id="dxMap" style="height: 345px; width: 100%; border: 1px solid #dee2e6; border-radius: 4px;"></div>
<div style="font-size: 11px; color: #6c757d; text-align: center; margin-top: 5px; font-style: italic;">
<i class="fas fa-info-circle"></i> <?= __("Note: Map shows DXCC entity locations, not actual spot locations"); ?>
</div>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover spottable">
<thead>
<tr class="log_title titles">
<th title="<?= __("Age in minutes"); ?>"><i class="fas fa-clock"></i></th>
<th title="<?= __("Band"); ?>"><i class="fas fa-wave-square"></i></th>
<th title="<?= __("Frequency"); ?> [MHz]"><?= __("Freq"); ?></th>
<th title="<?= __("Mode"); ?>"><i class="fas fa-broadcast-tower"></i></th>
<th title="<?= __("Spotted Callsign"); ?>"><?= __("Spotted"); ?></th>
<th title="<?= __("Continent"); ?>"><i class="fas fa-globe-americas"></i></th>
<th title="<?= __("CQ Zone"); ?>"><i class="fas fa-map-marked"></i></th>
<th title="<?= __("Flag"); ?>"><i class="fas fa-flag"></i></th>
<th title="<?= __("DXCC Entity"); ?>"><?= __("Entity"); ?></th>
<th title="<?= __("DXCC Number"); ?>"><i class="fas fa-hashtag"></i></th>
<th title="<?= __("Spotter Callsign"); ?>"><?= __("Spotter"); ?></th>
<th title="<?= __("Spotter Continent"); ?>"><i class="fas fa-globe-americas"></i></th>
<th title="<?= __("Spotter CQ Zone"); ?>"><i class="fas fa-map-marked"></i></th>
<th title="<?= __("Special Flags"); ?>"><?= __("Special"); ?></th>
<th title="<?= __("Message"); ?>"><?= __("Message"); ?></th>
</tr>
</thead>
<tbody class="spots_table_contents">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>

View File

@@ -58,6 +58,7 @@
var lang_general_word_please_wait = "<?= __("Please Wait ..."); ?>";
var lang_general_states_deprecated = "<?= _pgettext("Word for country states that are deprecated but kept for legacy reasons.", "deprecated"); ?>";
var lang_gen_hamradio_sat_info = "<?= __("Satellite Information"); ?>";
var lang_notes_error_loading = "<?= __("Error loading notes"); ?>";
var lang_notes_sort = "<?= __("Sorting"); ?>";
var lang_notes_duplication_disabled = "<?= __("Duplication is disabled for Contacts notes"); ?>";
@@ -113,7 +114,7 @@
var lang_qso_gridsquare_help = "<?= __("Enter multiple (4-digit) grids separated with commas. For example: IO77,IO78"); ?>";
var lang_cat_live = "<?= __("live"); ?>";
var lang_cat_polling = "<?= __("polling"); ?>";
var lang_cat_polling_tooltip = "<?= __("Periodic polling is slow. When operating locally, WebSockets are a more convenient way to control your radio in real-time."); ?>";
var lang_cat_polling_tooltip = "<?= __("Note: Periodic polling is slow. When operating locally, WebSockets are a more convenient way to control your radio in real-time."); ?>";
var lang_cat_tx = "<?= __("TX"); ?>";
var lang_cat_rx = "<?= __("RX"); ?>";
var lang_cat_tx_rx = "<?= __("TX/RX"); ?>";
@@ -132,6 +133,11 @@
var lang_qso_location_is_fetched_from_provided_gridsquare = "<?= __("Location is fetched from provided gridsquare"); ?>";
var lang_qso_location_is_fetched_from_dxcc_coordinates = "<?= __("Location is fetched from DXCC coordinates (no gridsquare provided)"); ?>";
// CAT Offline Status Messages
var lang_cat_working_offline = "<?= __("Working without CAT connection"); ?>";
var lang_cat_offline_cat_disabled = "<?= __("CAT connection is currently disabled. Enable CAT connection to work in online mode with your radio."); ?>";
var lang_cat_offline_no_radio = "<?= __("To connect your radio to Wavelog, visit the Wavelog Wiki for setup instructions."); ?>";
// CAT Configuration
var cat_timeout_minutes = Math.floor(<?php echo $this->optionslib->get_option('cat_timeout_interval'); ?> / 60);
</script>
@@ -1463,6 +1469,15 @@ mymap.on('mousemove', onQsoMapMove);
<?php } ?>
<!--- Bandmap CAT Integration --->
<?php if ($this->uri->segment(1) == "bandmap" && $this->uri->segment(2) == "list") { ?>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/cat.js?v=<?php echo time(); ?>"></script>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet/leaflet.geodesic.js?v=<?php echo time(); ?>"></script>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet.polylineDecorator.js?v=<?php echo time(); ?>"></script>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet/L.Terminator.js?v=<?php echo time(); ?>"></script>
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/sections/bandmap_list.js?v=<?php echo time(); ?>"></script>
<?php } ?>
<?php if ($this->uri->segment(1) == "logbook" && $this->uri->segment(2) == "view") { ?>
<script>
@@ -3342,6 +3357,6 @@ if (isset($scripts) && is_array($scripts)){
}
?>
<!-- Toast Notification - used by showToast() from common.js -->
<div id="toast-container" class="position-fixed top-0 end-0 p-3" style="z-index: 1100;"></div>
<div id="toast-container" class="position-fixed top-0 end-0 p-3" style="z-index: 10100;"></div>
</body>
</html>

View File

@@ -282,14 +282,14 @@
<li class="nav-item dropdown"> <!-- TOOLS -->
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#"><?= __("Tools"); ?></a>
<ul class="dropdown-menu header-dropdown">
<li><a class="dropdown-item" href="<?php echo site_url('bandmap/list'); ?>" title="DX Cluster"><i class="fa fa-tower-broadcast"></i> <?= __("DX Cluster"); ?></a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('dxcalendar'); ?>" title="DX Calendar"><i class="fas fa-calendar"></i> <?= __("DX Calendar"); ?></a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('contestcalendar'); ?>" title="Contest Calendar"><i class="fas fa-calendar"></i> <?= __("Contest Calendar"); ?></a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('hamsat'); ?>" title="Hams.at"><i class="fas fa-list"></i> Hams.at</a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('bandmap/list'); ?>" title="Bandmap"><i class="fa fa-id-card"></i> <?= __("Bandmap"); ?></a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('sattimers'); ?>" title="SAT Timers"><i class="fas fa-satellite"></i> <?= __("SAT Timers"); ?></a></li>
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="<?php echo site_url('satellite/flightpath'); ?>" title="Show Satellite Flight Path"><i class="fas fa-satellite"></i> <?= __("Satellite Flightpath"); ?></a></li>

974
assets/css/bandmap_list.css Normal file
View File

@@ -0,0 +1,974 @@
/* Always show vertical scrollbar to prevent layout shifts */
html {
overflow-y: scroll;
}
/* Blink animation for radio icon updates */
@keyframes blink-once {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.blink-once {
animation: blink-once 0.6s ease-in-out;
}
/* Reserve space for badge counts to prevent layout shifts */
.band-count-badge,
.mode-count-badge,
.quick-filter-count-badge {
display: inline-block;
min-width: 2em;
text-align: center;
padding-left: 0.4em;
padding-right: 0.4em;
}
/* Medal badge colors */
.text-bg-gold {
background-color: #FFD700 !important;
color: #fff !important;
border: 1px solid #FFA500 !important;
text-shadow: 0 0 2px rgba(0,0,0,0.3);
}
.text-bg-silver {
background-color: #A8A8A8 !important;
color: #fff !important;
border: 1px solid #E0E0E0 !important;
}
.text-bg-bronze {
background-color: #CD7F32 !important;
color: #fff !important;
border: 1px solid #8B4513 !important;
}
/* Badge borders for all badge types */
.badge.text-bg-success {
border: 1px solid #198754 !important;
}
.badge.text-bg-primary {
border: 1px solid #0a58ca !important;
}
.badge.text-bg-info {
border: 1px solid #087990 !important;
}
.badge.text-bg-warning {
border: 1px solid #cc9a06 !important;
}
.badge.text-bg-danger {
border: 1px solid #b02a37 !important;
}
.badge.text-bg-dark {
border: 1px solid #000 !important;
}
.badge.text-bg-secondary {
border: 1px solid #41464b !important;
}
/* Fix icon alignment in badges */
.spottable .badge i {
vertical-align: middle;
line-height: 1;
display: inline-block;
text-align: center;
width: 100%;
}
/* Center medal icons specifically */
.spottable .badge i.fa-medal {
margin-left: 1px;
}
.fresh {
transition: all 500ms ease;
--bs-table-bg: #3981b2;
--bs-table-accent-bg: #3981b2;
}
/* TTL-based spot styling - Expiring spots (TTL=0) */
.spot-expiring {
transition: all 300ms ease;
--bs-table-bg: rgba(220, 53, 69, 0.25) !important; /* Bootstrap danger color, subtle */
--bs-table-accent-bg: rgba(220, 53, 69, 0.25) !important;
background-color: rgba(220, 53, 69, 0.25) !important;
}
/* Very new spots (< 1 minute old) */
.spot-very-new {
transition: all 300ms ease;
--bs-table-bg: rgba(25, 135, 84, 0.2) !important; /* Bootstrap success color, light */
--bs-table-accent-bg: rgba(25, 135, 84, 0.2) !important;
background-color: rgba(25, 135, 84, 0.2) !important;
}
/* Hover effect for all spot rows */
.spottable tbody tr:hover {
--bs-table-bg: rgba(13, 110, 253, 0.15) !important; /* Bootstrap primary blue */
--bs-table-accent-bg: rgba(13, 110, 253, 0.15) !important;
background-color: rgba(13, 110, 253, 0.15) !important;
transition: all 150ms ease;
}
/* Don't apply hover to empty/loading rows */
.spottable tbody tr.dataTables_empty:hover,
.spottable tbody tr.dataTables_empty:hover td {
--bs-table-bg: transparent !important;
--bs-table-accent-bg: transparent !important;
background-color: transparent !important;
}
tbody a {
color: inherit;
text-decoration: none;
}
/* Make table rows clickable for prepare logging - use native alias cursor */
.spottable tbody tr {
cursor: alias !important;
}
.spottable tbody tr td {
cursor: alias !important;
}
/* Don't show alias cursor on loading/processing/empty rows */
.spottable tbody tr.dataTables_empty,
.spottable tbody tr.dataTables_empty td,
.spottable tbody td.dt-empty {
cursor: default !important;
}
/* Show standard pointer for clickable links (QRZ, POTA, SOTA, etc.) */
.spottable tbody tr a,
.spottable tbody tr td a {
cursor: pointer !important;
}
.spottable tbody tr td:nth-child(5) a {
text-decoration: underline !important;
cursor: pointer !important;
}
.spottable tbody tr td:nth-child(7) i:hover {
opacity: 0.7;
transform: scale(1.1);
transition: all 0.2s ease;
}
/* CAT Control - Locked Sorting */
.spottable.cat-sorting-locked thead th {
cursor: not-allowed !important;
pointer-events: none;
opacity: 0.6;
}
.spottable.cat-sorting-locked thead th:hover {
background-color: transparent !important;
}
.spottable.cat-sorting-locked thead th.sorting_asc::after,
.spottable.cat-sorting-locked thead th.sorting_desc::after {
opacity: 1 !important;
}
/* CAT Control - Frequency Gradient */
.cat-frequency-gradient {
transition: background-color 0.3s ease, var(--bs-table-bg) 0.3s ease !important;
}
/* Force gradient background to override Bootstrap striping AND spot lifecycle colors - MAXIMUM SPECIFICITY */
/* Override normal rows */
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient,
table.dataTable.table-striped tbody tr.cat-frequency-gradient,
table.table-striped tbody tr.cat-frequency-gradient,
.table-striped > tbody > tr.cat-frequency-gradient,
table tbody tr.cat-frequency-gradient {
background-color: var(--bs-table-bg) !important;
}
/* Override expiring spots (red) */
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient.spot-expiring,
table.dataTable.table-striped tbody tr.cat-frequency-gradient.spot-expiring,
table tbody tr.cat-frequency-gradient.spot-expiring {
background-color: var(--bs-table-bg) !important;
}
/* Override very new spots (green) */
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient.spot-very-new,
table.dataTable.table-striped tbody tr.cat-frequency-gradient.spot-very-new,
table tbody tr.cat-frequency-gradient.spot-very-new {
background-color: var(--bs-table-bg) !important;
}
/* Override fresh spots - force gradient color over fresh blue */
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient.fresh,
table.dataTable.table-striped tbody tr.cat-frequency-gradient.fresh,
table tbody tr.cat-frequency-gradient.fresh {
/* Override both the variable and background directly */
background-color: var(--bs-table-bg) !important;
/* Prevent .fresh class from setting its own --bs-table-bg */
transition: background-color 0.3s ease !important;
}
/* Override even and odd striping */
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient:nth-of-type(odd),
table.table.table-sm.table-bordered.table-hover.table-striped.spottable tbody tr.cat-frequency-gradient:nth-of-type(even),
table.dataTable.table-striped tbody tr.cat-frequency-gradient:nth-of-type(odd),
table.dataTable.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even),
table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(odd),
table.table-striped tbody tr.cat-frequency-gradient:nth-of-type(even),
.table-striped > tbody > tr.cat-frequency-gradient:nth-of-type(odd),
.table-striped > tbody > tr.cat-frequency-gradient:nth-of-type(even) {
--bs-table-accent-bg: transparent !important;
--bs-table-striped-bg: transparent !important;
}
.cat-frequency-gradient:hover {
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: 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: 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: 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: 2px solid #8b5cf6 !important;
}
/* Status bar styling */
.status-bar {
font-size: 0.875rem;
color: var(--bs-body-color);
font-family: 'Courier New', Courier, 'Lucida Console', Monaco, monospace;
background-color: var(--bs-body-bg);
border-radius: 8px;
margin: 0.25rem 0;
padding: 6px 12px;
}
.status-bar-inner {
display: flex;
align-items: center;
gap: 15px;
flex-wrap: nowrap;
}
.status-bar-left {
flex: 1 1 auto;
min-width: 0;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#statusMessage {
cursor: help;
}
.status-bar-right {
flex: 0 0 auto;
min-width: 100px;
margin-left: 10px;
font-weight: 500;
color: var(--bs-secondary);
text-align: right;
display: flex;
align-items: center;
justify-content: flex-end;
white-space: nowrap;
}
.dataTables_wrapper {
margin: 0;
padding: 0;
width: 100%;
}
.spottable {
width: 100%;
table-layout: fixed;
font-family: 'Courier New', Courier, 'Lucida Console', Monaco, monospace;
font-size: calc(1rem - 2px);
}
.spottable thead th {
font-weight: normal;
}
/* 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: 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: 115px; } /* Callsign (reduced by 5px) */
.spottable th:nth-child(6), .spottable td:nth-child(6) { width: 40px; } /* Continent */
.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 */
.spottable th:nth-child(11), .spottable td:nth-child(11) { width: 115px; } /* de Callsign (Spotter) (reduced by 5px) */
.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: 120px; } /* Special (LoTW, POTA, etc) (increased by 10px) */
.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;
text-overflow: ellipsis;
white-space: nowrap;
}
.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(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;
white-space: nowrap;
}
.spottable .flag-emoji {
font-family: "Twemoji Country Flags", "Helvetica", "Comic Sans", sans-serif;
font-style: normal;
font-variant: normal;
font-size: 20px;
line-height: 1;
}
.spottable img.emoji {
height: 20px;
width: 20px;
margin: 0 .05em 0 .1em;
vertical-align: -0.25em;
display: inline-block;
}
.spottable td:nth-child(14) {
overflow: visible;
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 {
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;
}
/* Fullscreen toggle button wrapper - large clickable area */
#fullscreenToggleWrapper {
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 10;
border-radius: 0.25rem;
transition: background-color 150ms ease;
}
#fullscreenToggleWrapper:hover {
background-color: rgba(0, 0, 0, 0.08) !important;
}
/* Fullscreen toggle button - ensure proper clickable area */
#fullscreenToggle {
min-width: 48px;
min-height: 48px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer !important;
position: relative;
flex-shrink: 0;
}
#fullscreenToggle:hover {
background-color: transparent !important;
}
#fullscreenIcon {
pointer-events: none; /* Icon shouldn't intercept clicks */
font-size: 1.4rem !important; /* Make icon slightly bigger too */
}
/* Ensure wavelog text doesn't overlap button */
.fullscreen-wavelog-text {
pointer-events: auto;
flex-shrink: 1;
white-space: nowrap;
cursor: pointer;
}
.fullscreen-wavelog-text:hover {
text-decoration: underline !important;
opacity: 0.8;
}
/* Fullscreen mode - CLEAN REBUILD */
.bandmap-logo-fullscreen {
display: none;
}
.bandmap-fullscreen .bandmap-logo-fullscreen {
display: inline-block;
}
.bandmap-fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
overflow: hidden;
background: var(--bs-body-bg, #fff);
max-width: none !important;
padding: 0 !important;
}
.bandmap-fullscreen .card {
height: 100vh;
margin: 0;
border-radius: 0;
border: none;
display: flex;
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;
}
.bandmap-fullscreen .card-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 0;
padding: 0.5rem;
overflow: auto;
}
/* Direct children of card-body in fullscreen */
.bandmap-fullscreen #radio_status {
flex: 0 0 auto;
margin: 0 !important;
padding: 0 !important;
min-height: 2.5rem; /* Reserve space for radio status to prevent UI shift */
}
/* Only show margin when radio_status has content */
.bandmap-fullscreen #radio_status:not(:empty) {
margin: 0 0 0.5rem 0 !important;
}
/* Reserve space for radio status even when empty */
#radio_status {
min-height: 2.5rem; /* Reserve vertical space to prevent layout shift */
}
.bandmap-fullscreen .menu-bar {
flex: 0 0 auto;
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 !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 {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
margin: 0 !important;
}
/* Radio status compact mode styling (no card wrapper) */
#radio_status #radio_cat_state:not(.card) {
border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0,0,0,.125));
border-radius: 8px;
padding: 0.5rem;
margin: 0;
background-color: var(--bs-card-cap-bg, var(--bs-secondary-bg));
}
#radio_status #radio_cat_state:not(.card) > div {
margin: 0 !important;
padding: 0 !important;
}
/* In fullscreen, ensure proper spacing */
.bandmap-fullscreen #radio_status #radio_cat_state:not(.card) {
margin: 0 !important;
}
/* Z-index management */
.bandmap-fullscreen .dataTables_processing {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10001;
}
.bandmap-fullscreen .dropdown-menu {
z-index: 10002;
position: fixed;
}
body.fullscreen-active .tooltip,
.bandmap-fullscreen .tooltip {
z-index: 10003 !important;
}
body.fullscreen-active .modal,
body.fullscreen-active .modal-backdrop {
z-index: 10050 !important;
}
body.fullscreen-active .modal-backdrop + .modal {
z-index: 10051 !important;
}
/* Hide page elements in fullscreen */
body.fullscreen-active #page-wrapper,
body.fullscreen-active nav,
body.fullscreen-active .navbar,
body.fullscreen-active header,
body.fullscreen-active #bandmapContainer > .d-flex.align-items-center.mb-3,
body.fullscreen-active #bandmapContainer > .messages {
display: none !important;
}
body.fullscreen-active .fullscreen-wavelog-text {
display: inline !important;
}
body.fullscreen-active {
overflow: hidden;
}
#fullscreenToggle {
cursor: pointer;
font-size: 1.2rem;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
height: 36px;
}
/* Menu bar and table styling */
.menu-bar {
margin-top: 5px;
margin-bottom: 5px;
overflow: visible;
}
/* Compact button padding in menu bar to save horizontal space */
.menu-bar .btn-sm {
padding-left: 0.4rem;
padding-right: 0.4rem;
}
/* Ensure radio selector dropdown matches button height */
.menu-bar .radios.form-select-sm {
height: calc(1.5em + 0.5rem + 2px); /* Match Bootstrap btn-sm height */
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
/* Ensure radio status container has no extra spacing */
.menu-bar #radio_status {
margin: 0;
padding: 0;
line-height: 1;
min-height: 0 !important; /* Override the general min-height rule */
align-self: center; /* Force vertical centering */
}
/* Override fullscreen min-height and margin when in menu-bar */
.bandmap-fullscreen .menu-bar #radio_status {
min-height: 0 !important;
margin: 0 !important;
}
/* Match radio status to button vertical rhythm */
.menu-bar #radio_status #radio_cat_state {
line-height: 1.5;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
margin: 0;
display: inline-flex !important;
align-items: center;
vertical-align: middle; /* Ensure inline alignment */
}
/* Ensure CAT Control and search box stay right-aligned when wrapping */
.menu-bar > div:last-child {
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;
}
/* Only apply custom table-responsive styling to bandmap container */
#bandmapContainer .table-responsive {
overflow: auto;
margin: 0;
padding: 0;
background-color: var(--bs-card-cap-bg, var(--bs-secondary-bg));
border: var(--bs-card-border-width, 1px) solid var(--bs-card-border-color, rgba(0,0,0,.125));
border-radius: 8px;
}
.spottable {
border-bottom: none;
margin-bottom: 0;
border-radius: 8px;
overflow: hidden;
}
.spottable thead tr {
border-top: 1px solid var(--bs-border-color);
border-bottom: 1px solid var(--bs-border-color);
}
.spottable tbody tr:last-child {
border-bottom: 1px solid var(--bs-border-color);
}
.spottable tbody tr,
.spottable tbody tr td {
border-top: none;
border-bottom: none;
}
/* Filter dropdowns */
select[multiple] {
height: auto;
min-height: 350px;
max-height: 700px;
}
select[multiple].filter-short {
min-height: 150px;
max-height: 220px;
}
select[multiple] option:checked {
background-color: var(--bs-primary);
color: white;
font-weight: normal;
}
.dropdown-menu .mb-3 {
display: flex;
flex-direction: column;
justify-content: flex-end;
min-height: 100px;
}
.dropdown-menu .filter-label-bottom {
margin-top: auto;
margin-bottom: 0.25rem;
padding-bottom: 0;
text-align: left;
}
.dropdown-menu select.form-select {
margin-bottom: 0;
}
.dropdown-menu select {
flex-shrink: 0;
}
.filter-tip {
background-color: var(--bs-card-cap-bg, var(--bs-secondary-bg));
color: var(--bs-body-color);
border: 2px solid var(--bs-dark, #212529);
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 15px;
font-size: 1rem; /* Match page default font size */
display: flex;
align-items: center;
gap: 8px;
}
.filter-label-small {
font-size: 1rem; /* Match page default font size */
margin-bottom: 0.25rem;
font-weight: 500;
}
.search-icon-clickable {
cursor: pointer;
}
.search-icon-clickable:hover {
background-color: var(--bs-secondary-bg);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.dropdown-menu {
min-width: 95vw;
max-width: 95vw;
}
select[multiple] {
min-height: 180px;
max-height: 375px;
}
select[multiple].filter-short {
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) {
select[multiple] {
min-height: 150px;
max-height: 300px;
}
/* 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;
}
}

View File

@@ -70,6 +70,10 @@ $(document).ready(function() {
LOCK_TIMEOUT_MS: 10000
};
// Global setting for radio status display mode (can be set by pages like bandmap)
// Options: false (card wrapper), 'compact' (no card), 'ultra-compact' (tooltip only)
window.CAT_COMPACT_MODE = window.CAT_COMPACT_MODE || false;
function initializeWebSocketConnection() {
try {
// Note: Browser will log WebSocket connection errors to console if server is unreachable
@@ -124,6 +128,7 @@ $(document).ready(function() {
/**
* Handle incoming WebSocket data messages
* Processes 'welcome' and 'radio_status' message types
* On bandmap, only processes radio status when CAT Control is enabled
* @param {object} data - Message data from WebSocket server
*/
function handleWebSocketData(data) {
@@ -134,6 +139,17 @@ $(document).ready(function() {
// Handle radio status updates
if (data.type === 'radio_status' && data.radio && ($(".radios option:selected").val() == 'ws')) {
// On bandmap page, check CAT Control state
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF - show offline status and skip processing
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
displayOfflineStatus('cat_disabled');
}
return;
}
}
data.updated_minutes_ago = Math.floor((Date.now() - data.timestamp) / 60000);
// Cache the radio data
updateCATui(data);
@@ -340,17 +356,98 @@ $(document).ready(function() {
}
/**
* Display radio status panel with unified styling
* Handles success (active connection), error (connection lost), timeout (stale data), and not_logged_in states
* Display radio status panel with current CAT information
* Creates or updates a Bootstrap card panel showing radio connection status and frequency data
* Includes visual feedback with color-coded icon and blink animation on updates
* Respects global CAT_COMPACT_MODE setting for rendering style
* @param {string} state - Display state: 'success', 'error', 'timeout', or 'not_logged_in'
* @param {object|string} data - Radio data object (success) or radio name string (error/timeout/not_logged_in)
* CAT_COMPACT_MODE options:
* false - Standard mode with card wrapper
* 'compact' - Compact mode without card wrapper
* 'ultra-compact' - Ultra-compact mode showing only tooltip with info
* @param {string} reason - Optional reason: 'no_radio' (default) or 'cat_disabled'
*/
function displayOfflineStatus(reason) {
// Display "Working offline" message with tooltip in ultra-compact mode
if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
return;
}
// Default to 'no_radio' for backward compatibility
reason = reason || 'no_radio';
// Use translation variable if available, fallback to English
var offlineText = typeof lang_cat_working_offline !== 'undefined' ? lang_cat_working_offline : 'Working without CAT connection';
const offlineHtml = '<span id="radio_cat_state" class="text-body" style="display: inline-flex; align-items: center; font-size: 0.875rem;">' +
'<i class="fas fa-unlink text-warning" style="margin-right: 5px;"></i>' +
'<span style="margin-right: 5px;">' + offlineText + '</span>' +
'<i id="radio-status-icon" class="fas fa-info-circle text-muted" style="cursor: help;" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
let tooltipContent;
if (reason === 'cat_disabled') {
// Use translation variable if available, fallback to English
tooltipContent = typeof lang_cat_offline_cat_disabled !== 'undefined'
? lang_cat_offline_cat_disabled
: 'CAT connection is currently disabled. Enable CAT connection to work in online mode with your radio.';
} else {
// reason === 'no_radio' (default)
tooltipContent = typeof lang_cat_offline_no_radio !== 'undefined'
? lang_cat_offline_no_radio
: 'To connect your radio to Wavelog, visit the Wavelog Wiki for setup instructions.';
}
// Remove existing radio status if present
$('#radio_cat_state').remove();
// Add offline status
$('#radio_status').append(offlineHtml);
// Initialize tooltip
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
}
}
/**
* Display radio status in the UI
* @param {string} state - One of 'success', 'error', 'timeout', 'not_logged_in'
* @param {object|string} data - Radio data object (success) or radio name string (error/timeout/not_logged_in)
* CAT_COMPACT_MODE options:
* false - Standard mode with card wrapper
* 'compact' - Compact mode without card wrapper
* 'ultra-compact' - Ultra-compact mode showing only tooltip with info
*/
function displayRadioStatus(state, data) {
var iconClass, content;
var baseStyle = '<div style="display: flex; align-items: center; font-size: calc(1rem - 2px);">';
// On bandmap page, only show radio status when CAT Control is enabled
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
// CAT Control is OFF on bandmap
// In ultra-compact mode, show "Working offline" with CAT disabled message
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
// Check if a radio is selected
var selectedRadio = $('.radios option:selected').val();
if (selectedRadio && selectedRadio !== '0') {
// Radio selected but CAT disabled
displayOfflineStatus('cat_disabled');
return;
}
}
// Standard behavior: remove radio status
$('#radio_cat_state').remove();
return;
}
}
if (state === 'success') {
var iconClass, content;
var baseStyle = '<div style="display: flex; align-items: center; font-size: calc(1rem - 2px);">'; if (state === 'success') {
// Success state - display radio info
iconClass = 'text-success'; // Bootstrap green for success
@@ -361,7 +458,7 @@ $(document).ready(function() {
connectionType = ' (' + lang_cat_live + ')';
} else {
connectionType = ' (' + lang_cat_polling + ')';
connectionTooltip = ' <i class="fas fa-question-circle" style="font-size: 0.9em; cursor: help;" data-bs-toggle="tooltip" title="' + lang_cat_polling_tooltip + '"></i>';
connectionTooltip = ' <span class="fas fa-question-circle" style="font-size: 0.9em; cursor: help;" data-bs-toggle="tooltip" title="' + lang_cat_polling_tooltip + '"></span>';
}
// Build radio info line
@@ -440,41 +537,154 @@ $(document).ready(function() {
var icon = '<i id="radio-status-icon" class="fas fa-radio ' + iconClass + '" style="margin-right: 10px; font-size: 1.2em;"></i>';
var html = baseStyle + icon + content + '</div>';
// Update DOM
if (!$('#radio_cat_state').length) {
// Create panel if it doesn't exist
$('#radio_status').prepend('<div id="radio_cat_state" class="card"><div class="card-body">' + html + '</div></div>');
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
});
// Update existing panel content
$('#radio_cat_state .card-body').html(html);
} // Initialize Bootstrap tooltips for any new tooltip elements in the radio panel
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
new bootstrap.Tooltip(this);
});
// Update DOM based on global CAT_COMPACT_MODE setting
if (window.CAT_COMPACT_MODE === 'ultra-compact') {
// Ultra-compact mode: show radio icon, radio name, and question mark with tooltip
var tooltipContent = '';
var radioName = '';
// Trigger blink animation on successful updates
if (state === 'success') {
$('#radio-status-icon').addClass('radio-icon-blink');
setTimeout(function() {
$('#radio-status-icon').removeClass('radio-icon-blink');
}, 400);
}
}
if (state === 'success') {
// Build tooltip content with all radio information
// Use the full dropdown text (includes "Polling - " and "(last updated)" etc.)
radioName = $('select.radios option:selected').text();
var connectionType = '';
if ($(".radios option:selected").val() == 'ws') {
connectionType = lang_cat_live;
} else {
connectionType = lang_cat_polling;
} tooltipContent = '<b>' + radioName + '</b> (' + connectionType + ')';
// Add frequency info
if(data.frequency_rx != null && data.frequency_rx != 0) {
tooltipContent += '<br><b>' + lang_cat_tx + ':</b> ' + data.frequency_formatted;
data.frequency_rx_formatted = format_frequency(data.frequency_rx);
if (data.frequency_rx_formatted) {
tooltipContent += '<br><b>' + lang_cat_rx + ':</b> ' + data.frequency_rx_formatted;
}
} else {
tooltipContent += '<br><b>' + lang_cat_tx_rx + ':</b> ' + data.frequency_formatted;
}
// Add mode
if(data.mode != null) {
tooltipContent += '<br><b>' + lang_cat_mode + ':</b> ' + data.mode;
}
// Add power
if(data.power != null && data.power != 0) {
tooltipContent += '<br><b>' + lang_cat_power + ':</b> ' + data.power + 'W';
}
// Add polling tooltip if applicable
if ($(".radios option:selected").val() != 'ws') {
tooltipContent += '<br><br><i>' + lang_cat_polling_tooltip + '</i>';
}
} else if (state === 'error') {
radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
tooltipContent = lang_cat_connection_error + ': <b>' + radioName + '</b><br>' + lang_cat_connection_lost;
} else if (state === 'timeout') {
radioName = typeof data === 'string' ? data : $('select.radios option:selected').text();
tooltipContent = lang_cat_connection_timeout + ': <b>' + radioName + '</b><br>' + lang_cat_data_stale;
} else if (state === 'not_logged_in') {
radioName = '';
tooltipContent = lang_cat_not_logged_in;
}
var ultraCompactHtml = '<span id="radio_cat_state" style="display: inline-flex; align-items: center; font-size: 0.875rem;">' +
'<i class="fas fa-radio ' + iconClass + '" style="margin-right: 5px;"></i>' +
'<span style="margin-right: 5px;">' + radioName + '</span>' +
'<i id="radio-status-icon" class="fas fa-info-circle" style="cursor: help;" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom"></i>' +
'</span>';
if (!$('#radio_cat_state').length) {
$('#radio_status').append(ultraCompactHtml);
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
});
$('#radio_cat_state').replaceWith(ultraCompactHtml);
}
// Initialize tooltip with dynamic content
var tooltipElement = document.querySelector('#radio_status [data-bs-toggle="tooltip"]');
if (tooltipElement) {
new bootstrap.Tooltip(tooltipElement, {
title: tooltipContent,
html: true,
placement: 'bottom'
});
}
// Add blink animation to radio icon on update
$('#radio_status .fa-radio').addClass('blink-once');
setTimeout(function() {
$('#radio_status .fa-radio').removeClass('blink-once');
}, 600);
} else if (window.CAT_COMPACT_MODE === 'compact' || window.CAT_COMPACT_MODE === true) {
// Compact mode: inject directly without card wrapper
if (!$('#radio_cat_state').length) {
$('#radio_status').prepend('<div id="radio_cat_state">' + html + '</div>');
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
});
$('#radio_cat_state').html(html);
}
} else {
// Standard mode: create card wrapper (default for backward compatibility)
if (!$('#radio_cat_state').length) {
// Create panel if it doesn't exist
$('#radio_status').prepend('<div id="radio_cat_state" class="card"><div class="card-body">' + html + '</div></div>');
} else {
// Dispose of existing tooltips before updating content
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
var tooltipInstance = bootstrap.Tooltip.getInstance(this);
if (tooltipInstance) {
tooltipInstance.dispose();
}
});
// Update existing panel content
$('#radio_cat_state .card-body').html(html);
}
}
// Initialize Bootstrap tooltips for any new tooltip elements in the radio panel (except ultra-compact which handles its own)
if (window.CAT_COMPACT_MODE !== 'ultra-compact') {
$('#radio_cat_state [data-bs-toggle="tooltip"]').each(function() {
new bootstrap.Tooltip(this);
});
}
// Trigger blink animation on successful updates
if (state === 'success') {
$('#radio-status-icon').addClass('radio-icon-blink');
setTimeout(function() {
$('#radio-status-icon').removeClass('radio-icon-blink');
}, 400);
}
}
/**
* Process CAT data and update UI elements
* Performs timeout check, updates form fields, and displays radio status
* Handles both WebSocket and polling data sources
* Exposed globally for extension by other components (e.g., bandmap)
* @param {object} data - CAT data object from radio (includes frequency, mode, power, etc.)
*/
function updateCATui(data) {
window.updateCATui = function updateCATui(data) {
// Store last CAT data globally for other components (e.g., bandmap)
window.lastCATData = data;
// Check if data is too old FIRST - before any UI updates
// cat_timeout_minutes is set in footer.php from PHP config
var minutes = cat_timeout_minutes;
@@ -590,6 +800,7 @@ $(document).ready(function() {
/**
* Periodic AJAX polling function for radio status updates
* Only runs for non-WebSocket radios (skips if radio is 'ws')
* On bandmap, only polls when CAT Control is enabled
* Fetches CAT data every 3 seconds and updates UI
* Includes lock mechanism to prevent simultaneous requests
*/
@@ -602,6 +813,13 @@ $(document).ready(function() {
return;
}
// On bandmap page, only poll when CAT Control is enabled
if (typeof window.isCatTrackingEnabled !== 'undefined') {
if (!window.isCatTrackingEnabled) {
return; // Skip polling when CAT Control is OFF
}
}
if ((typeof radioID !== 'undefined') && (radioID !== null) && (radioID !== '') && (updateFromCAT_lock == 0)) {
updateFromCAT_lock = 1;
@@ -670,6 +888,14 @@ $(document).ready(function() {
radioCatUrlCache = {};
radioNameCache = {};
// If switching to None, disable CAT tracking FIRST before stopping connections
// This prevents any pending updates from interfering with the offline status
if (selectedRadioId == '0') {
if (typeof window.isCatTrackingEnabled !== 'undefined') {
window.isCatTrackingEnabled = false;
}
}
// Hide radio status box (both success and error states)
$('#radio_cat_state').remove();
@@ -693,6 +919,12 @@ $(document).ready(function() {
if (typeof dxwaterfall_cat_state !== 'undefined') {
dxwaterfall_cat_state = "none";
}
// Disable CAT Connection button when no radio is selected
$('#toggleCatTracking').prop('disabled', true).addClass('disabled');
// Also turn OFF CAT Connection (remove green button state)
$('#toggleCatTracking').removeClass('btn-success').addClass('btn-secondary');
// Display offline status when no radio selected
displayOfflineStatus('no_radio');
} else if (selectedRadioId == 'ws') {
websocketIntentionallyClosed = false; // Reset flag when opening WebSocket
reconnectAttempts = 0; // Reset reconnect attempts
@@ -700,17 +932,33 @@ $(document).ready(function() {
if (typeof dxwaterfall_cat_state !== 'undefined') {
dxwaterfall_cat_state = "websocket";
}
// Enable CAT Control button when radio is selected
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always initialize WebSocket connection
initializeWebSocketConnection();
// In ultra-compact mode, show offline status if CAT Control is disabled
if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
} else {
// Set DX Waterfall CAT state to polling if variable exists
if (typeof dxwaterfall_cat_state !== 'undefined') {
dxwaterfall_cat_state = "polling";
}
// Update frequency at configured interval
// Enable CAT Control button when radio is selected
$('#toggleCatTracking').prop('disabled', false).removeClass('disabled');
// Always start polling
CATInterval=setInterval(updateFromCAT, CAT_CONFIG.POLL_INTERVAL);
// In ultra-compact mode, show offline status if CAT Control is disabled
if (window.CAT_COMPACT_MODE === 'ultra-compact' && typeof window.isCatTrackingEnabled !== 'undefined' && !window.isCatTrackingEnabled) {
displayOfflineStatus('cat_disabled');
}
}
});
// Trigger initial radio change to start monitoring selected radio
$('.radios').change();
// Expose displayOfflineStatus globally for other components (e.g., bandmap CAT Control toggle)
window.displayOfflineStatus = displayOfflineStatus;
});

View File

@@ -0,0 +1,478 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('leaflet')) :
typeof define === 'function' && define.amd ? define(['leaflet'], factory) :
(factory(global.L));
}(this, (function (L$1) { 'use strict';
L$1 = L$1 && L$1.hasOwnProperty('default') ? L$1['default'] : L$1;
// functional re-impl of L.Point.distanceTo,
// with no dependency on Leaflet for easier testing
function pointDistance(ptA, ptB) {
var x = ptB.x - ptA.x;
var y = ptB.y - ptA.y;
return Math.sqrt(x * x + y * y);
}
var computeSegmentHeading = function computeSegmentHeading(a, b) {
return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI + 90 + 360) % 360;
};
var asRatioToPathLength = function asRatioToPathLength(_ref, totalPathLength) {
var value = _ref.value,
isInPixels = _ref.isInPixels;
return isInPixels ? value / totalPathLength : value;
};
function parseRelativeOrAbsoluteValue(value) {
if (typeof value === 'string' && value.indexOf('%') !== -1) {
return {
value: parseFloat(value) / 100,
isInPixels: false
};
}
var parsedValue = value ? parseFloat(value) : 0;
return {
value: parsedValue,
isInPixels: parsedValue > 0
};
}
var pointsEqual = function pointsEqual(a, b) {
return a.x === b.x && a.y === b.y;
};
function pointsToSegments(pts) {
return pts.reduce(function (segments, b, idx, points) {
// this test skips same adjacent points
if (idx > 0 && !pointsEqual(b, points[idx - 1])) {
var a = points[idx - 1];
var distA = segments.length > 0 ? segments[segments.length - 1].distB : 0;
var distAB = pointDistance(a, b);
segments.push({
a: a,
b: b,
distA: distA,
distB: distA + distAB,
heading: computeSegmentHeading(a, b)
});
}
return segments;
}, []);
}
function projectPatternOnPointPath(pts, pattern) {
// 1. split the path into segment infos
var segments = pointsToSegments(pts);
var nbSegments = segments.length;
if (nbSegments === 0) {
return [];
}
var totalPathLength = segments[nbSegments - 1].distB;
var offset = asRatioToPathLength(pattern.offset, totalPathLength);
var endOffset = asRatioToPathLength(pattern.endOffset, totalPathLength);
var repeat = asRatioToPathLength(pattern.repeat, totalPathLength);
var repeatIntervalPixels = totalPathLength * repeat;
var startOffsetPixels = offset > 0 ? totalPathLength * offset : 0;
var endOffsetPixels = endOffset > 0 ? totalPathLength * endOffset : 0;
// 2. generate the positions of the pattern as offsets from the path start
var positionOffsets = [];
var positionOffset = startOffsetPixels;
do {
positionOffsets.push(positionOffset);
positionOffset += repeatIntervalPixels;
} while (repeatIntervalPixels > 0 && positionOffset < totalPathLength - endOffsetPixels);
// 3. projects offsets to segments
var segmentIndex = 0;
var segment = segments[0];
return positionOffsets.map(function (positionOffset) {
// find the segment matching the offset,
// starting from the previous one as offsets are ordered
while (positionOffset > segment.distB && segmentIndex < nbSegments - 1) {
segmentIndex++;
segment = segments[segmentIndex];
}
var segmentRatio = (positionOffset - segment.distA) / (segment.distB - segment.distA);
return {
pt: interpolateBetweenPoints(segment.a, segment.b, segmentRatio),
heading: segment.heading
};
});
}
/**
* Finds the point which lies on the segment defined by points A and B,
* at the given ratio of the distance from A to B, by linear interpolation.
*/
function interpolateBetweenPoints(ptA, ptB, ratio) {
if (ptB.x !== ptA.x) {
return {
x: ptA.x + ratio * (ptB.x - ptA.x),
y: ptA.y + ratio * (ptB.y - ptA.y)
};
}
// special case where points lie on the same vertical axis
return {
x: ptA.x,
y: ptA.y + (ptB.y - ptA.y) * ratio
};
}
(function() {
// save these original methods before they are overwritten
var proto_initIcon = L.Marker.prototype._initIcon;
var proto_setPos = L.Marker.prototype._setPos;
var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
L.Marker.addInitHook(function () {
var iconOptions = this.options.icon && this.options.icon.options;
var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
if (iconAnchor) {
iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
}
this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ;
this.options.rotationAngle = this.options.rotationAngle || 0;
// Ensure marker keeps rotated during dragging
this.on('drag', function(e) { e.target._applyRotation(); });
});
L.Marker.include({
_initIcon: function() {
proto_initIcon.call(this);
},
_setPos: function (pos) {
proto_setPos.call(this, pos);
this._applyRotation();
},
_applyRotation: function () {
if(this.options.rotationAngle) {
this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin;
if(oldIE) {
// for IE 9, use the 2D rotation
this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
} else {
// for modern browsers, prefer the 3D accelerated version
this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)';
}
}
},
setRotationAngle: function(angle) {
this.options.rotationAngle = angle;
this.update();
return this;
},
setRotationOrigin: function(origin) {
this.options.rotationOrigin = origin;
this.update();
return this;
}
});
})();
L$1.Symbol = L$1.Symbol || {};
/**
* A simple dash symbol, drawn as a Polyline.
* Can also be used for dots, if 'pixelSize' option is given the 0 value.
*/
L$1.Symbol.Dash = L$1.Class.extend({
options: {
pixelSize: 10,
pathOptions: {}
},
initialize: function initialize(options) {
L$1.Util.setOptions(this, options);
this.options.pathOptions.clickable = false;
},
buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
var opts = this.options;
var d2r = Math.PI / 180;
// for a dot, nothing more to compute
if (opts.pixelSize <= 1) {
return L$1.polyline([dirPoint.latLng, dirPoint.latLng], opts.pathOptions);
}
var midPoint = map.project(dirPoint.latLng);
var angle = -(dirPoint.heading - 90) * d2r;
var a = L$1.point(midPoint.x + opts.pixelSize * Math.cos(angle + Math.PI) / 2, midPoint.y + opts.pixelSize * Math.sin(angle) / 2);
// compute second point by central symmetry to avoid unecessary cos/sin
var b = midPoint.add(midPoint.subtract(a));
return L$1.polyline([map.unproject(a), map.unproject(b)], opts.pathOptions);
}
});
L$1.Symbol.dash = function (options) {
return new L$1.Symbol.Dash(options);
};
L$1.Symbol.ArrowHead = L$1.Class.extend({
options: {
polygon: true,
pixelSize: 10,
headAngle: 60,
pathOptions: {
stroke: false,
weight: 2
}
},
initialize: function initialize(options) {
L$1.Util.setOptions(this, options);
this.options.pathOptions.clickable = false;
},
buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) {
return this.options.polygon ? L$1.polygon(this._buildArrowPath(dirPoint, map), this.options.pathOptions) : L$1.polyline(this._buildArrowPath(dirPoint, map), this.options.pathOptions);
},
_buildArrowPath: function _buildArrowPath(dirPoint, map) {
var d2r = Math.PI / 180;
var tipPoint = map.project(dirPoint.latLng);
var direction = -(dirPoint.heading - 90) * d2r;
var radianArrowAngle = this.options.headAngle / 2 * d2r;
var headAngle1 = direction + radianArrowAngle;
var headAngle2 = direction - radianArrowAngle;
var arrowHead1 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle1), tipPoint.y + this.options.pixelSize * Math.sin(headAngle1));
var arrowHead2 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle2), tipPoint.y + this.options.pixelSize * Math.sin(headAngle2));
return [map.unproject(arrowHead1), dirPoint.latLng, map.unproject(arrowHead2)];
}
});
L$1.Symbol.arrowHead = function (options) {
return new L$1.Symbol.ArrowHead(options);
};
L$1.Symbol.Marker = L$1.Class.extend({
options: {
markerOptions: {},
rotate: false
},
initialize: function initialize(options) {
L$1.Util.setOptions(this, options);
this.options.markerOptions.clickable = false;
this.options.markerOptions.draggable = false;
},
buildSymbol: function buildSymbol(directionPoint, latLngs, map, index, total) {
if (this.options.rotate) {
this.options.markerOptions.rotationAngle = directionPoint.heading + (this.options.angleCorrection || 0);
}
return L$1.marker(directionPoint.latLng, this.options.markerOptions);
}
});
L$1.Symbol.marker = function (options) {
return new L$1.Symbol.Marker(options);
};
var isCoord = function isCoord(c) {
return c instanceof L$1.LatLng || Array.isArray(c) && c.length === 2 && typeof c[0] === 'number';
};
var isCoordArray = function isCoordArray(ll) {
return Array.isArray(ll) && isCoord(ll[0]);
};
L$1.PolylineDecorator = L$1.FeatureGroup.extend({
options: {
patterns: []
},
initialize: function initialize(paths, options) {
L$1.FeatureGroup.prototype.initialize.call(this);
L$1.Util.setOptions(this, options);
this._map = null;
this._paths = this._initPaths(paths);
this._bounds = this._initBounds();
this._patterns = this._initPatterns(this.options.patterns);
},
/**
* Deals with all the different cases. input can be one of these types:
* array of LatLng, array of 2-number arrays, Polyline, Polygon,
* array of one of the previous.
*/
_initPaths: function _initPaths(input, isPolygon) {
var _this = this;
if (isCoordArray(input)) {
// Leaflet Polygons don't need the first point to be repeated, but we do
var coords = isPolygon ? input.concat([input[0]]) : input;
return [coords];
}
if (input instanceof L$1.Polyline) {
// we need some recursivity to support multi-poly*
return this._initPaths(input.getLatLngs(), input instanceof L$1.Polygon);
}
if (Array.isArray(input)) {
// flatten everything, we just need coordinate lists to apply patterns
return input.reduce(function (flatArray, p) {
return flatArray.concat(_this._initPaths(p, isPolygon));
}, []);
}
return [];
},
// parse pattern definitions and precompute some values
_initPatterns: function _initPatterns(patternDefs) {
return patternDefs.map(this._parsePatternDef);
},
/**
* Changes the patterns used by this decorator
* and redraws the new one.
*/
setPatterns: function setPatterns(patterns) {
this.options.patterns = patterns;
this._patterns = this._initPatterns(this.options.patterns);
this.redraw();
},
/**
* Changes the patterns used by this decorator
* and redraws the new one.
*/
setPaths: function setPaths(paths) {
this._paths = this._initPaths(paths);
this._bounds = this._initBounds();
this.redraw();
},
/**
* Parse the pattern definition
*/
_parsePatternDef: function _parsePatternDef(patternDef, latLngs) {
return {
symbolFactory: patternDef.symbol,
// Parse offset and repeat values, managing the two cases:
// absolute (in pixels) or relative (in percentage of the polyline length)
offset: parseRelativeOrAbsoluteValue(patternDef.offset),
endOffset: parseRelativeOrAbsoluteValue(patternDef.endOffset),
repeat: parseRelativeOrAbsoluteValue(patternDef.repeat)
};
},
onAdd: function onAdd(map) {
this._map = map;
this._draw();
this._map.on('moveend', this.redraw, this);
},
onRemove: function onRemove(map) {
this._map.off('moveend', this.redraw, this);
this._map = null;
L$1.FeatureGroup.prototype.onRemove.call(this, map);
},
/**
* As real pattern bounds depends on map zoom and bounds,
* we just compute the total bounds of all paths decorated by this instance.
*/
_initBounds: function _initBounds() {
var allPathCoords = this._paths.reduce(function (acc, path) {
return acc.concat(path);
}, []);
return L$1.latLngBounds(allPathCoords);
},
getBounds: function getBounds() {
return this._bounds;
},
/**
* Returns an array of ILayers object
*/
_buildSymbols: function _buildSymbols(latLngs, symbolFactory, directionPoints) {
var _this2 = this;
return directionPoints.map(function (directionPoint, i) {
return symbolFactory.buildSymbol(directionPoint, latLngs, _this2._map, i, directionPoints.length);
});
},
/**
* Compute pairs of LatLng and heading angle,
* that define positions and directions of the symbols on the path
*/
_getDirectionPoints: function _getDirectionPoints(latLngs, pattern) {
var _this3 = this;
if (latLngs.length < 2) {
return [];
}
var pathAsPoints = latLngs.map(function (latLng) {
return _this3._map.project(latLng);
});
return projectPatternOnPointPath(pathAsPoints, pattern).map(function (point) {
return {
latLng: _this3._map.unproject(L$1.point(point.pt)),
heading: point.heading
};
});
},
redraw: function redraw() {
if (!this._map) {
return;
}
this.clearLayers();
this._draw();
},
/**
* Returns all symbols for a given pattern as an array of FeatureGroup
*/
_getPatternLayers: function _getPatternLayers(pattern) {
var _this4 = this;
var mapBounds = this._map.getBounds().pad(0.1);
return this._paths.map(function (path) {
var directionPoints = _this4._getDirectionPoints(path, pattern)
// filter out invisible points
.filter(function (point) {
return mapBounds.contains(point.latLng);
});
return L$1.featureGroup(_this4._buildSymbols(path, pattern.symbolFactory, directionPoints));
});
},
/**
* Draw all patterns
*/
_draw: function _draw() {
var _this5 = this;
this._patterns.map(function (pattern) {
return _this5._getPatternLayers(pattern);
}).forEach(function (layers) {
_this5.addLayer(L$1.featureGroup(layers));
});
}
});
/*
* Allows compact syntax to be used
*/
L$1.polylineDecorator = function (paths, options) {
return new L$1.PolylineDecorator(paths, options);
};
})));

View File

@@ -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) {

File diff suppressed because it is too large Load Diff

View File

@@ -647,39 +647,138 @@ function save_fav() {
var bc_bandmap = new BroadcastChannel('qso_window');
bc_bandmap.onmessage = function (ev) {
if (ev.data == 'ping' && qso_manual == 0) {
// Always respond to ping, regardless of manual mode
// This allows bandmap to detect existing QSO windows
if (ev.data == 'ping') {
bc_bandmap.postMessage('pong');
}
}
// Store pending references from bandmap to populate AFTER callsign lookup completes
var pendingReferences = null;
// Track last lookup to prevent duplicate calls
var lastLookupCallsign = null;
var lookupInProgress = false;
// Helper function to populate reference fields after callsign lookup completes
function populatePendingReferences(refsToPopulate) {
// Use provided references or fall back to global pendingReferences
var refs = refsToPopulate || pendingReferences;
if (!refs) {
return;
}
// POTA - uses selectize
if (refs.pota_ref && $('#pota_ref').length) {
try {
var $select = $('#pota_ref').selectize();
if ($select.length && $select[0].selectize) {
var selectize = $select[0].selectize;
selectize.addOption({name: refs.pota_ref});
selectize.setValue(refs.pota_ref, false);
$('#pota_ref').trigger('change');
}
} catch (e) {
console.warn('Could not set POTA reference:', e);
}
}
// SOTA - uses selectize
if (refs.sota_ref && $('#sota_ref').length) {
try {
var $select = $('#sota_ref').selectize();
if ($select.length && $select[0].selectize) {
var selectize = $select[0].selectize;
selectize.addOption({name: refs.sota_ref});
selectize.setValue(refs.sota_ref, false);
$('#sota_ref').trigger('change');
}
} catch (e) {
console.warn('Could not set SOTA reference:', e);
}
}
// WWFF - uses selectize
if (refs.wwff_ref && $('#wwff_ref').length) {
try {
var $select = $('#wwff_ref').selectize();
if ($select.length && $select[0].selectize) {
var selectize = $select[0].selectize;
selectize.addOption({name: refs.wwff_ref});
selectize.setValue(refs.wwff_ref, false);
$('#wwff_ref').trigger('change');
}
} catch (e) {
console.warn('Could not set WWFF reference:', e);
}
}
// IOTA - regular select dropdown (not selectize)
if (refs.iota_ref && $('#iota_ref').length) {
try {
let $iotaSelect = $('#iota_ref');
if ($iotaSelect.find('option[value="' + refs.iota_ref + '"]').length === 0) {
$iotaSelect.append(new Option(refs.iota_ref, refs.iota_ref));
}
$iotaSelect.val(refs.iota_ref).trigger('change');
} catch (e) {
console.warn('Could not set IOTA reference:', e);
}
}
// Only clear global pendingReferences if we used it (not a captured copy)
if (!refsToPopulate && pendingReferences) {
pendingReferences = null;
}
}
var bc = new BroadcastChannel('qso_wish');
bc.onmessage = function (ev) {
if (qso_manual == 0) {
if (ev.data.ping) {
// Handle ping/pong only when manual mode is disabled (qso_manual == 0)
if (ev.data.ping) {
if (qso_manual == 0) {
let message = {};
message.pong = true;
bc.postMessage(message);
} else {
// console.log(ev.data);
let delay = 0;
if ($("#callsign").val() != "") {
reset_fields();
delay = 600;
}
setTimeout(() => {
if (ev.data.frequency != null) {
$('#frequency').val(ev.data.frequency).trigger("change");
$("#band").val(frequencyToBand(ev.data.frequency));
}
if (ev.data.frequency_rx != "") {
$('#frequency_rx').val(ev.data.frequency_rx);
$("#band_rx").val(frequencyToBand(ev.data.frequency_rx));
}
$("#callsign").val(ev.data.call);
$("#callsign").focusout();
$("#callsign").blur();
}, delay);
}
} else {
// Always process frequency, callsign, and reference data from bandmap
// (regardless of manual mode - bandmap should control the form)
// Store references for later population (after callsign lookup completes)
pendingReferences = {
pota_ref: ev.data.pota_ref,
sota_ref: ev.data.sota_ref,
wwff_ref: ev.data.wwff_ref,
iota_ref: ev.data.iota_ref
};
let delay = 0;
// Only reset if callsign is different from what we're about to set
if ($("#callsign").val() != "" && $("#callsign").val() != ev.data.call) {
reset_fields();
delay = 600;
}
setTimeout(() => {
if (ev.data.frequency != null) {
$('#frequency').val(ev.data.frequency).trigger("change");
$("#band").val(frequencyToBand(ev.data.frequency));
}
if (ev.data.frequency_rx != "") {
$('#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);
}
$("#callsign").val(ev.data.call);
$("#callsign").focusout();
$("#callsign").blur();
}, delay);
}
} /* receive */
@@ -1097,6 +1196,20 @@ function get_note_status(callsign){
$("#callsign").on("focusout", function () {
if ($(this).val().length >= 3 && preventLookup == false) {
var currentCallsign = $(this).val().toUpperCase().replaceAll('Ø', '0');
// Prevent duplicate lookups for the same callsign if already in progress
if (lookupInProgress && lastLookupCallsign === currentCallsign) {
return;
}
// If callsign changed, allow new lookup even if one is in progress
lastLookupCallsign = currentCallsign;
lookupInProgress = true;
// Capture pendingReferences for THIS lookup (before it gets overwritten by another click)
var capturedReferences = pendingReferences ? Object.assign({}, pendingReferences) : null;
// Disable Save QSO button and show fetch status
$('#saveQso').prop('disabled', true);
$('#fetch_status').show();
@@ -1645,6 +1758,13 @@ $("#callsign").on("focusout", function () {
clearTimeout(fetchTimeout);
$('#saveQso').prop('disabled', false);
$('#fetch_status').hide();
// Populate pending references from bandmap (after all lookup logic completes)
// Small delay to ensure DOM is fully updated
// Use the captured references from when THIS lookup started
setTimeout(function() {
populatePendingReferences(capturedReferences);
}, 100);
}
// Trigger custom event to notify that callsign lookup is complete
@@ -1660,6 +1780,9 @@ $("#callsign").on("focusout", function () {
clearTimeout(fetchTimeout);
$('#saveQso').prop('disabled', false);
$('#fetch_status').hide();
// Reset lookup in progress flag
lookupInProgress = false;
});
} else {
// Reset QSO fields