mirror of
https://github.com/magicbug/Cloudlog
synced 2026-08-13 17:49:35 -04:00
Add DXCC worked status and radio QSY to DX Cluster
Enhances DX Cluster by showing DXCC entity and worked status per spot, including new band/country indicators. Adds radio selection and QSY functionality, updates UI with improved filters and spot marker styling, and provides user feedback for frequency actions.
This commit is contained in:
parent
38b3003f90
commit
d26068bd3b
3 changed files with 322 additions and 76 deletions
|
|
@ -14,6 +14,10 @@ class Dxcluster extends CI_Controller {
|
|||
function index()
|
||||
{
|
||||
$data['page_title'] = "DX Cluster Spots";
|
||||
|
||||
// Load radio data for CAT control
|
||||
$this->load->model('cat');
|
||||
$data['radios'] = $this->cat->radios();
|
||||
|
||||
/// Load layout
|
||||
|
||||
|
|
@ -166,6 +170,10 @@ class Dxcluster extends CI_Controller {
|
|||
$callsign = $item['callsign'];
|
||||
$band = isset($item['band']) ? $item['band'] : null;
|
||||
|
||||
// Get DXCC entity for this callsign
|
||||
$dxcc_info = $this->logbook_model->dxcc_lookup($callsign, date('Ymd'));
|
||||
$dxcc = isset($dxcc_info['adif']) ? $dxcc_info['adif'] : null;
|
||||
|
||||
// Check if worked on this band
|
||||
$worked_on_band = false;
|
||||
if ($band) {
|
||||
|
|
@ -175,10 +183,37 @@ class Dxcluster extends CI_Controller {
|
|||
// Check if worked on any band
|
||||
$worked_overall = $this->logbook_model->check_if_callsign_worked_in_logbook($callsign, $logbooks_locations_array, null) > 0;
|
||||
|
||||
// Check if DXCC entity is worked on this band
|
||||
$dxcc_worked_on_band = false;
|
||||
if ($dxcc && $band) {
|
||||
$this->db->select('COL_DXCC');
|
||||
$this->db->where_in('station_id', $logbooks_locations_array);
|
||||
$this->db->where('COL_DXCC', $dxcc);
|
||||
$this->db->where('COL_BAND', $band);
|
||||
$this->db->limit(1);
|
||||
$query = $this->db->get($this->config->item('table_name'));
|
||||
$dxcc_worked_on_band = $query->num_rows() > 0;
|
||||
}
|
||||
|
||||
// Check if DXCC entity is worked on any band
|
||||
$dxcc_worked_overall = false;
|
||||
if ($dxcc) {
|
||||
$this->db->select('COL_DXCC');
|
||||
$this->db->where_in('station_id', $logbooks_locations_array);
|
||||
$this->db->where('COL_DXCC', $dxcc);
|
||||
$this->db->limit(1);
|
||||
$query = $this->db->get($this->config->item('table_name'));
|
||||
$dxcc_worked_overall = $query->num_rows() > 0;
|
||||
}
|
||||
|
||||
$results[$callsign] = [
|
||||
'worked_on_band' => $worked_on_band,
|
||||
'worked_overall' => $worked_overall,
|
||||
'band' => $band
|
||||
'band' => $band,
|
||||
'dxcc' => $dxcc,
|
||||
'dxcc_worked_on_band' => $dxcc_worked_on_band,
|
||||
'dxcc_worked_overall' => $dxcc_worked_overall,
|
||||
'country' => isset($dxcc_info['entity']) ? $dxcc_info['entity'] : null
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,6 +191,42 @@
|
|||
.spot-marker.new {
|
||||
animation: spotAppear 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Worked status styling */
|
||||
.spot-marker.worked-on-band {
|
||||
background: #27ae60;
|
||||
border-color: #229954;
|
||||
}
|
||||
|
||||
.spot-marker.worked-on-band:hover {
|
||||
background: #2ecc71;
|
||||
border-color: #27ae60;
|
||||
}
|
||||
|
||||
.spot-marker.worked-other-band {
|
||||
background: #3498db;
|
||||
border-color: #2980b9;
|
||||
}
|
||||
|
||||
.spot-marker.worked-other-band:hover {
|
||||
background: #5dade2;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.spot-marker.not-worked {
|
||||
background: #e74c3c;
|
||||
border-color: #c0392b;
|
||||
}
|
||||
|
||||
.spot-marker.new-dxcc {
|
||||
box-shadow: 0 0 10px #f39c12, 0 0 20px #f39c12;
|
||||
border: 2px solid #f39c12;
|
||||
}
|
||||
|
||||
.spot-marker.new-band-dxcc {
|
||||
box-shadow: 0 0 10px #e74c3c, 0 0 20px #e74c3c;
|
||||
border: 2px solid #e74c3c;
|
||||
}
|
||||
|
||||
.tune-indicator {
|
||||
position: absolute;
|
||||
|
|
@ -404,7 +440,7 @@
|
|||
|
||||
<div class="stats-display">
|
||||
<div class="stat-item">Band: <span class="stat-value" id="currentBand">20m</span></div>
|
||||
<div class="stat-item">Range: <span class="stat-value" id="freqRange">14.000-14.350</span></div>
|
||||
<div class="stat-item">Viewing: <span class="stat-value" id="freqRange">14.000-14.350</span> MHz</div>
|
||||
<div class="stat-item">Spots: <span class="stat-value" id="bandSpotCount">0</span></div>
|
||||
<div class="stat-item">Total: <span class="stat-value" id="totalSpots">0</span></div>
|
||||
</div>
|
||||
|
|
@ -422,6 +458,8 @@
|
|||
<script>
|
||||
let ws;
|
||||
let spots = new Map();
|
||||
let workedStatus = {}; // Cache for worked status
|
||||
let checkWorkedTimeout = null;
|
||||
let currentBandMeters = 20;
|
||||
let bandStart = 14000;
|
||||
let bandEnd = 14350;
|
||||
|
|
@ -492,6 +530,12 @@
|
|||
|
||||
updateDisplay();
|
||||
updateStats();
|
||||
|
||||
// Check worked status after a short delay (debounce)
|
||||
clearTimeout(checkWorkedTimeout);
|
||||
checkWorkedTimeout = setTimeout(() => {
|
||||
checkWorkedStatus();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handleBandChange() {
|
||||
|
|
@ -526,16 +570,87 @@
|
|||
return `${meters}m`;
|
||||
}
|
||||
|
||||
// Determine band from frequency (in kHz)
|
||||
function getBandFromFrequency(freqKhz) {
|
||||
const freq = parseFloat(freqKhz);
|
||||
|
||||
if (freq >= 1800 && freq <= 2000) return '160m';
|
||||
if (freq >= 3500 && freq <= 4000) return '80m';
|
||||
if (freq >= 5250 && freq <= 5450) return '60m';
|
||||
if (freq >= 7000 && freq <= 7300) return '40m';
|
||||
if (freq >= 10100 && freq <= 10150) return '30m';
|
||||
if (freq >= 14000 && freq <= 14350) return '20m';
|
||||
if (freq >= 18068 && freq <= 18168) return '17m';
|
||||
if (freq >= 21000 && freq <= 21450) return '15m';
|
||||
if (freq >= 24890 && freq <= 24990) return '12m';
|
||||
if (freq >= 28000 && freq <= 29700) return '10m';
|
||||
if (freq >= 50000 && freq <= 54000) return '6m';
|
||||
if (freq >= 70000 && freq <= 71000) return '4m';
|
||||
if (freq >= 144000 && freq <= 148000) return '2m';
|
||||
if (freq >= 420000 && freq <= 450000) return '70cm';
|
||||
if (freq >= 1240000 && freq <= 1300000) return '23cm';
|
||||
if (freq >= 1000000) return 'ghz';
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// Check worked status for callsigns
|
||||
async function checkWorkedStatus() {
|
||||
const callsignsToCheck = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (let [id, spot] of spots) {
|
||||
if (workedStatus[spot.dx]) continue;
|
||||
if (seen.has(spot.dx)) continue;
|
||||
|
||||
const band = getBandFromFrequency(spot.frequency);
|
||||
callsignsToCheck.push({
|
||||
callsign: spot.dx,
|
||||
band: band
|
||||
});
|
||||
seen.add(spot.dx);
|
||||
}
|
||||
|
||||
if (callsignsToCheck.length === 0) return;
|
||||
|
||||
// Limit to 30 callsigns per request
|
||||
const batch = callsignsToCheck.slice(0, 30);
|
||||
|
||||
try {
|
||||
const response = await fetch('<?php echo site_url('dxcluster/check_worked'); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ callsigns: batch })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
Object.assign(workedStatus, data.results);
|
||||
updateDisplay();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking worked status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function selectBand(meters, start, end) {
|
||||
currentBandMeters = meters;
|
||||
bandStart = start;
|
||||
bandEnd = end;
|
||||
|
||||
// Add 5% buffer padding on each end so spots near edges are visible
|
||||
const bandRange = end - start;
|
||||
const padding = bandRange * 0.05;
|
||||
|
||||
bandStart = start - padding;
|
||||
bandEnd = end + padding;
|
||||
|
||||
// Apply default zoom level for better initial view
|
||||
const center = (start + end) / 2;
|
||||
const newRange = (end - start) / zoomLevel;
|
||||
viewStart = Math.max(start, center - newRange / 2);
|
||||
viewEnd = Math.min(end, center + newRange / 2);
|
||||
const newRange = (bandEnd - bandStart) / zoomLevel;
|
||||
viewStart = Math.max(bandStart, center - newRange / 2);
|
||||
viewEnd = Math.min(bandEnd, center + newRange / 2);
|
||||
|
||||
// Handle band display properly for all bands
|
||||
document.getElementById('currentBand').textContent = formatBandName(meters);
|
||||
|
|
@ -666,6 +781,25 @@
|
|||
spotElement.classList.add('aged');
|
||||
}
|
||||
|
||||
// Add worked status styling
|
||||
const status = workedStatus[spot.dx];
|
||||
if (status) {
|
||||
if (status.worked_on_band) {
|
||||
spotElement.classList.add('worked-on-band');
|
||||
} else if (status.worked_overall) {
|
||||
spotElement.classList.add('worked-other-band');
|
||||
} else {
|
||||
spotElement.classList.add('not-worked');
|
||||
}
|
||||
|
||||
// Add DXCC new badge styling
|
||||
if (status.dxcc && !status.dxcc_worked_on_band) {
|
||||
spotElement.classList.add('new-band-dxcc');
|
||||
} else if (status.dxcc && !status.dxcc_worked_overall) {
|
||||
spotElement.classList.add('new-dxcc');
|
||||
}
|
||||
}
|
||||
|
||||
spotElement.textContent = spot.dx;
|
||||
spotElement.style.top = `${y - 10}px`;
|
||||
spotElement.style.left = `${spotLeft}px`;
|
||||
|
|
@ -804,7 +938,7 @@
|
|||
// Mouse wheel scrolling
|
||||
bandmapDisplay.addEventListener('wheel', (event) => {
|
||||
event.preventDefault();
|
||||
const delta = event.deltaY > 0 ? 1 : -1;
|
||||
const delta = event.deltaY > 0 ? -1 : 1; // Inverted: scroll down = move down freq scale
|
||||
const scrollStep = (viewEnd - viewStart) * 0.1;
|
||||
|
||||
viewStart += delta * scrollStep;
|
||||
|
|
|
|||
|
|
@ -13,42 +13,66 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="hideRbnSpots" checked>
|
||||
<label class="form-check-label" for="hideRbnSpots">
|
||||
<i class="fas fa-filter"></i> Hide RBN Spots
|
||||
</label>
|
||||
<!-- Filters and Radio Control Panel -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body py-2">
|
||||
<div id="radio_status"></div>
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap">
|
||||
<!-- Radio Selection -->
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="radio" class="form-label mb-0 me-2" style="white-space: nowrap;">
|
||||
<i class="fas fa-broadcast-tower"></i> Radio:
|
||||
</label>
|
||||
<select class="form-select form-select-sm radios" id="radio" name="radio" style="min-width: 150px;">
|
||||
<option value="0" selected="selected"><?php echo lang('general_word_none'); ?></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>
|
||||
</div>
|
||||
|
||||
<!-- Band Filter -->
|
||||
<div class="d-flex align-items-center">
|
||||
<label for="bandFilter" class="form-label mb-0 me-2" style="white-space: nowrap;">
|
||||
<i class="fas fa-wave-square"></i> Band:
|
||||
</label>
|
||||
<select class="form-select form-select-sm" id="bandFilter" style="min-width: 120px;">
|
||||
<option value="all">All Bands</option>
|
||||
<option value="160m">160m</option>
|
||||
<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>
|
||||
<option value="6m">6m</option>
|
||||
<option value="4m">4m</option>
|
||||
<option value="2m">2m</option>
|
||||
<option value="70cm">70cm</option>
|
||||
<option value="23cm">23cm</option>
|
||||
<option value="ghz">GHz+</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- RBN Filter -->
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="hideRbnSpots" checked>
|
||||
<label class="form-check-label" for="hideRbnSpots">
|
||||
<i class="fas fa-filter"></i> Hide RBN Spots
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<small class="text-muted ms-auto">
|
||||
<i class="fas fa-info-circle"></i> Filters active
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label for="bandFilter" class="form-label mb-0">
|
||||
<i class="fas fa-wave-square"></i> Band:
|
||||
</label>
|
||||
<select class="form-select form-select-sm" id="bandFilter" style="width: auto; min-width: 120px;">
|
||||
<option value="all">All Bands</option>
|
||||
<option value="160m">160m</option>
|
||||
<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>
|
||||
<option value="6m">6m</option>
|
||||
<option value="4m">4m</option>
|
||||
<option value="2m">2m</option>
|
||||
<option value="70cm">70cm</option>
|
||||
<option value="23cm">23cm</option>
|
||||
<option value="ghz">GHz+</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Filter spots by band and hide RBN spots
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="dxSpotsTable" class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
|
|
@ -67,6 +91,21 @@
|
|||
</table>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div class="alert alert-info mb-2">
|
||||
<strong><i class="fas fa-info-circle"></i> Legend:</strong>
|
||||
<div class="mt-2">
|
||||
<span class="me-3"><i class="fas fa-check text-success"></i> = Worked on this band</span>
|
||||
<span class="me-3"><i class="fas fa-check text-info"></i> = Worked on another band</span>
|
||||
<span class="me-3"><i class="fas fa-times text-secondary"></i> = Not worked</span>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<span class="me-3"><span class="badge bg-danger">New Band</span> = New DXCC on this band</span>
|
||||
<span class="me-3"><span class="badge bg-warning text-dark">New DXCC</span> = New DXCC entity (never worked)</span>
|
||||
</div>
|
||||
<div class="mt-2 pt-2 border-top">
|
||||
<small><i class="fas fa-mouse-pointer"></i> <strong>Tip:</strong> Click any frequency to QSY your radio (if selected above)</small>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Real-time DX cluster spots. Data refreshes automatically via WebSocket connection.
|
||||
|
|
@ -227,19 +266,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (type === 'display') {
|
||||
const callsign = data;
|
||||
const status = workedStatus[callsign];
|
||||
let iconHtml = '';
|
||||
let html = `<span class="dx-callsign" data-callsign="${callsign}">${callsign}</span>`;
|
||||
|
||||
if (status) {
|
||||
// Worked status icon
|
||||
if (status.worked_on_band) {
|
||||
iconHtml = `<i class="fas fa-check text-success ms-2" title="Worked on ${status.band}"></i>`;
|
||||
html += `<i class="fas fa-check text-success ms-2" title="Worked on ${status.band}"></i>`;
|
||||
} else if (status.worked_overall) {
|
||||
iconHtml = `<i class="fas fa-check text-info ms-2" title="Worked on another band"></i>`;
|
||||
html += `<i class="fas fa-check text-info ms-2" title="Worked on another band"></i>`;
|
||||
} else {
|
||||
iconHtml = `<i class="fas fa-times text-secondary ms-2" title="Not worked yet"></i>`;
|
||||
html += `<i class="fas fa-times text-secondary ms-2" title="Not worked yet"></i>`;
|
||||
}
|
||||
|
||||
// New country badges
|
||||
if (status.dxcc && !status.dxcc_worked_on_band) {
|
||||
html += `<span class="badge bg-danger ms-2" title="New DXCC entity on this band">New Band</span>`;
|
||||
} else if (status.dxcc && !status.dxcc_worked_overall) {
|
||||
html += `<span class="badge bg-warning text-dark ms-2" title="New DXCC entity">New DXCC</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<span class="dx-callsign" data-callsign="${callsign}">${callsign}</span>${iconHtml}`;
|
||||
return html;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
@ -491,37 +538,67 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
|
||||
function handleFrequencyClick(frequency) {
|
||||
// You can implement frequency tuning functionality here
|
||||
// For example, send to radio or copy to clipboard
|
||||
console.log('Frequency clicked:', frequency);
|
||||
const radioId = document.getElementById('radio').value;
|
||||
|
||||
// Copy to clipboard as example
|
||||
if (navigator.clipboard) {
|
||||
const freqInMHz = (parseFloat(frequency) / 1000).toFixed(3);
|
||||
navigator.clipboard.writeText(freqInMHz).then(() => {
|
||||
// Show a temporary notification
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'toast align-items-center text-white bg-primary border-0 position-fixed top-0 end-0 m-3';
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.style.zIndex = '9999';
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
Frequency ${freqInMHz} MHz copied to clipboard
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
});
|
||||
if (radioId === '0') {
|
||||
// No radio selected - copy to clipboard
|
||||
if (navigator.clipboard) {
|
||||
const freqInMHz = (parseFloat(frequency) / 1000).toFixed(3);
|
||||
navigator.clipboard.writeText(freqInMHz).then(() => {
|
||||
showToast('Frequency ' + freqInMHz + ' MHz copied to clipboard', 'info');
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// QSY the radio
|
||||
const freqInMHz = (parseFloat(frequency) / 1000).toFixed(3);
|
||||
|
||||
fetch('<?php echo site_url('dxcluster/qsy'); ?>', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
radio_id: radioId,
|
||||
frequency: freqInMHz
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('QSY command sent to radio: ' + freqInMHz + ' MHz', 'success');
|
||||
} else {
|
||||
showToast('Failed to QSY radio: ' + data.message, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending QSY command:', error);
|
||||
showToast('Error sending QSY command', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message, type = 'primary') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type} border-0 position-fixed top-0 end-0 m-3`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.style.zIndex = '9999';
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast);
|
||||
bsToast.show();
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Helper function for DataTables language URL (if it exists in Cloudlog)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue