mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
Merge pull request #2479 from szporwolik/dev_dx_cluster_ui
[DX Cluster] Bandplan List Update
This commit is contained in:
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
974
assets/css/bandmap_list.css
Normal 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;
|
||||
}
|
||||
}
|
||||
316
assets/js/cat.js
316
assets/js/cat.js
@@ -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;
|
||||
});
|
||||
|
||||
478
assets/js/leaflet.polylineDecorator.js
Normal file
478
assets/js/leaflet.polylineDecorator.js
Normal 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);
|
||||
};
|
||||
|
||||
})));
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user