Merge pull request #2800 from AndreasK79/dbtools_iota_dxcc

This commit is contained in:
Andreas Kristiansen
2026-01-12 09:55:51 +01:00
committed by GitHub
4 changed files with 325 additions and 40 deletions

View File

@@ -1646,6 +1646,8 @@ class Logbookadvanced_model extends CI_Model {
return $this->getIncorrectCqZones();
case 'checkincorrectituzones':
return $this->getIncorrectItuZones();
case 'checkiota':
return $this->checkIota();
default:
return null;
}
@@ -2075,4 +2077,117 @@ class Logbookadvanced_model extends CI_Model {
return $query->result();
}
public function checkIota() {
$result1 = $this->checkSingleIota();
$result2 = $this->checkMultiDxccIota();
$merged = array_merge($result1, $result2);
// Sort merged results by station_profile_name, then col_time_on DESC
usort($merged, function($a, $b) {
$stationCompare = strcmp($a->station_profile_name, $b->station_profile_name);
if ($stationCompare !== 0) {
return $stationCompare;
}
// If same station, sort by time_on descending (newest first)
return strtotime($b->col_time_on) - strtotime($a->col_time_on);
});
return $merged;
}
/*
* Get list of QSOs with IOTA that do not match the IOTAs listed for the DXCC.
* Some islands are excluded as they can be in multiple DXCCs.
*
* These are excluded by not having a dxccid or dxccid = 0
*
*/
public function checkSingleIota() {
$sql = "select col_primary_key, col_time_on, col_call, col_band, col_gridsquare, col_dxcc, col_country, station_profile_name, col_lotw_qsl_rcvd, col_mode, col_submode, col_iota, iotadxcc.name as correctdxcc
from " . $this->config->item('table_name') . " thcv
join station_profile on thcv.station_id = station_profile.station_id
join dxcc_entities on dxcc_entities.adif = thcv.COL_DXCC
join iota on thcv.col_iota = iota.tag
join dxcc_entities iotadxcc on iota.dxccid = iotadxcc.adif
where station_profile.user_id = ?
and thcv.col_dxcc > 0
and thcv.col_dxcc <> iota.dxccid
and iota.dxccid > 0
order by station_profile_name, col_time_on desc";
$bindings[] = [$this->session->userdata('user_id')];
$query = $this->db->query($sql, $bindings);
return $query->result();
}
/*
* Get list of QSOs with multi-DXCC IOTA tags where the DXCC prefix doesn't match
* any of the valid prefixes for that IOTA.
*/
public function checkMultiDxccIota() {
// Define IOTA tags that span multiple DXCCs with their valid prefixes
$multiDxccIotas = [
'AS-004' => [215, 283], // 5B4, ZC4
'EU-053' => [167, 284], // OJ0, SM
'EU-115' => [245, 265], // EI, GI
'EU-117' => [151, 224], // R1M, OH
'EU-129' => [230, 269], // DL, SP
'EU-191' => [275, 288], // YO, UR
'EU-192' => [284, 224], // SM, OH
'NA-015' => [70, 105], // CO, KG4
'NA-096' => [72, 78], // HH, HI
'NA-105' => [213, 518], // FS, PJ7
'OC-034' => [163, 327], // P2, YB
'OC-088' => [46, 327, 345], // 9M6, V8, YB
'OC-148' => [327, 511], // YB, 4W
'SA-008' => [100, 112] // LU, CE
];
$allResults = [];
foreach ($multiDxccIotas as $iotaTag => $adifList) {
// Build IN clause for SQL
$adifListStr = implode(',', $adifList);
$sql = "SELECT thcv.col_primary_key, thcv.col_time_on, thcv.col_call, thcv.col_band, thcv.col_gridsquare,
thcv.col_dxcc, thcv.col_country, station_profile.station_profile_name, thcv.col_lotw_qsl_rcvd,
thcv.col_mode, thcv.col_submode, thcv.col_iota,
(
SELECT GROUP_CONCAT(DISTINCT d.name ORDER BY d.name SEPARATOR ', ')
FROM dxcc_entities d
WHERE d.adif IN ($adifListStr)
) as correctdxcc
FROM " . $this->config->item('table_name') . " thcv
JOIN station_profile ON thcv.station_id = station_profile.station_id
JOIN dxcc_entities ON dxcc_entities.adif = thcv.COL_DXCC
JOIN iota ON thcv.col_iota = iota.tag
WHERE station_profile.user_id = ?
AND thcv.col_iota = ?
AND dxcc_entities.adif NOT IN ($adifListStr)
ORDER BY station_profile_name, col_time_on DESC";
$bindings = [$this->session->userdata('user_id'), $iotaTag];
$query = $this->db->query($sql, $bindings);
$results = $query->result();
if (!empty($results)) {
$allResults = array_merge($allResults, $results);
}
}
// Sort the merged results by station_profile_name, then col_time_on DESC
usort($allResults, function($a, $b) {
$stationCompare = strcmp($a->station_profile_name, $b->station_profile_name);
if ($stationCompare !== 0) {
return $stationCompare;
}
// If same station, sort by time_on descending (newest first)
return strtotime($b->col_time_on) - strtotime($a->col_time_on);
});
return $allResults;
}
}

View File

@@ -30,6 +30,9 @@ switch ($type) {
case 'checkincorrectituzones':
check_incorrect_itu_zones($result, $custom_date_format);
break;
case 'checkiota':
check_iota($result, $custom_date_format);
break;
default:
// Invalid type
break;
@@ -99,7 +102,7 @@ function check_dxcc($result, $custom_date_format) { ?>
<th><?= __("QSO Date"); ?></th>
<th class="select-filter" scope="col"><?= __("Band"); ?></th>
<th class="select-filter" scope="col"><?= __("Mode"); ?></th>
<th class="select-filter" scope="col"><?= __("LoTW"); ?></th>
<th style='text-align: center' class="select-filter" scope="col"><?= __("LoTW"); ?></th>
<th class="select-filter" scope="col"><?= __("Station Profile"); ?></th>
<th class="select-filter" scope="col"><?= __("Existing DXCC"); ?></th>
<th class="select-filter" scope="col"><?= __("Result DXCC"); ?></th>
@@ -113,7 +116,7 @@ function check_dxcc($result, $custom_date_format) { ?>
<td><?php echo date($custom_date_format, strtotime($qso['qso_date'])); ?></td>
<td><?php echo htmlspecialchars($qso['band']); ?></td>
<td><?php echo htmlspecialchars($qso['submode'] ? $qso['submode'] : $qso['mode']); ?></td>
<td><?php echo $qso['lotw_qsl_rcvd'] == 'Y' ? __('Yes') : __('No'); ?></td>
<td style='text-align: center'><div class="<?php echo $qso['lotw_qsl_rcvd'] == 'Y' ? 'bg-success' : 'bg-danger'; ?>"><?php echo $qso['lotw_qsl_rcvd'] == 'Y' ? __('Yes') : __('No'); ?></div></td>
<td><?php echo $qso['station_profile']; ?></td>
<td><?php echo htmlspecialchars(ucwords(strtolower($qso['existing_dxcc']), "- (/"), ENT_QUOTES, 'UTF-8'); ?></td>
<td><?php echo htmlspecialchars(ucwords(strtolower($qso['result_country']), "- (/"), ENT_QUOTES, 'UTF-8'); ?></td>
@@ -157,7 +160,7 @@ function check_incorrect_gridsquares($result, $custom_date_format) { ?>
<th><?= __("QSO Date"); ?></th>
<th class="select-filter" scope="col"><?= __("Band"); ?></th>
<th class="select-filter" scope="col"><?= __("Mode"); ?></th>
<th class="select-filter" scope="col"><?= __("LoTW"); ?></th>
<th style='text-align: center' class="select-filter" scope="col"><?= __("LoTW"); ?></th>
<th class="select-filter" scope="col"><?= __("Station Profile"); ?></th>
<th class="select-filter" scope="col"><?= __("DXCC"); ?></th>
<th><?= __("Gridsquare"); ?></th>
@@ -171,7 +174,7 @@ function check_incorrect_gridsquares($result, $custom_date_format) { ?>
<td><?php echo date($custom_date_format, strtotime($qso->col_time_on)); ?></td>
<td><?php echo htmlspecialchars($qso->col_band); ?></td>
<td><?php echo htmlspecialchars($qso->col_submode ? $qso->col_submode : $qso->col_mode); ?></td>
<td><?php echo $qso->col_lotw_qsl_rcvd == 'Y' ? __('Yes') : __('No'); ?></td>
<td style='text-align: center'><div class="<?php echo $qso->col_lotw_qsl_rcvd == 'Y' ? 'bg-success' : 'bg-danger'; ?>"><?php echo $qso->col_lotw_qsl_rcvd == 'Y' ? __('Yes') : __('No'); ?></div></td>
<td><?php echo $qso->station_profile_name; ?></td>
<td><?php echo htmlspecialchars(ucwords(strtolower($qso->col_country), "- (/"), ENT_QUOTES, 'UTF-8'); ?></td>
<td><?php echo $qso->col_gridsquare; ?></td>
@@ -363,3 +366,62 @@ function check_incorrect_itu_zones($result, $custom_date_format) { ?>
echo '<div class="alert alert-success">' . __("No incorrect CQ Zones were found.") . '</div>';
}
}
function check_iota($result, $custom_date_format) { ?>
<h5><?= __("IOTA Check Results") ?></h5>
<?php
if (is_array($result) && isset($result['status']) && $result['status'] == 'error') {
echo '<div class="alert alert-danger" role="alert">' . htmlspecialchars($result['message']) . '</div>';
return;
}
if ($result) { ?>
<?= __("These QSOs MAY have an incorrect IOTA reference.") ?>
<?= __("Results depends on the correct DXCC, and it will only be checked against current DXCC. False positive results may occur.") ?>
<div class="table-responsive">
<table class="table table-sm table-striped table-bordered table-condensed" id="iotaCheckTable">
<thead>
<tr>
<th class="select-filter" scope="col"><?= __("Callsign"); ?></th>
<th><?= __("QSO Date"); ?></th>
<th class="select-filter" scope="col"><?= __("Band"); ?></th>
<th class="select-filter" scope="col"><?= __("Mode"); ?></th>
<th style='text-align: center' class="select-filter" scope="col"><?= __("LoTW"); ?></th>
<th class="select-filter" scope="col"><?= __("Station Profile"); ?></th>
<th class="select-filter" scope="col"><?= __("QSO DXCC"); ?></th>
<th class="select-filter" scope="col"><?= __("IOTA"); ?></th>
<th class="select-filter" scope="col"><?= __("IOTA DXCC"); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($result as $qso): ?>
<tr id="qsoID-<?php echo $qso->col_primary_key; ?>">
<td><?php echo '<a id="edit_qso" href="javascript:displayQso(' . $qso->col_primary_key . ')">' . htmlspecialchars($qso->col_call) . '</a>'; ?></td>
<td><?php echo date($custom_date_format, strtotime($qso->col_time_on)); ?></td>
<td><?php echo htmlspecialchars($qso->col_band); ?></td>
<td><?php echo htmlspecialchars($qso->col_submode ? $qso->col_submode : $qso->col_mode); ?></td>
<td style='text-align: center'><div class="<?php echo $qso->col_lotw_qsl_rcvd == 'Y' ? 'bg-success' : 'bg-danger'; ?>"><?php echo $qso->col_lotw_qsl_rcvd == 'Y' ? __('Yes') : __('No'); ?></div></td>
<td><?php echo $qso->station_profile_name; ?></td>
<td><?php echo htmlspecialchars(ucwords(strtolower($qso->col_country), "- (/"), ENT_QUOTES, 'UTF-8'); ?></td>
<td><?php echo '<a href=\'javascript:displayContacts("'.$qso->col_iota.'","All","All","All","All","IOTA")\'>' . $qso->col_iota . '</a>'; ?> <a href="https://www.iota-world.org/iotamaps/?grpref=<?php echo $qso->col_iota; ?>" target="_blank"><i class="fas fa-globe"></i></a></td>
<td><?php echo htmlspecialchars(ucwords(strtolower($qso->correctdxcc ?? ''), "- (/"), ENT_QUOTES, 'UTF-8'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
<?php }
}

View File

@@ -98,6 +98,17 @@
</div>
</div>
<?php endif; ?>
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1"><?= __("Check IOTA against DXCC") ?></h6>
<p class="mb-1 small text-muted"><?= __("Use Wavelog to check IOTA against DXCC") ?></p>
</div>
<div class="d-flex nowrap">
<button type="button" class="btn btn-sm btn-success me-1 ld-ext-right" id="checkIotaBtn" onclick="checkIota()">
<?= __("Check") ?><div class="ld ld-ring ld-spin"></div>
</button>
</div>
</div>
</div>
</div>

View File

@@ -2379,17 +2379,22 @@ function saveOptions() {
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column.search(val ? '^' + val + '$' : '', true, false).draw();
// Search in rendered content, not just data
column.search(val ? val : '', true, false).draw();
});
column
.data()
.unique()
.sort()
.each(function (d, j) {
select.append('<option value="' + d + '">' + d + '</option>');
});
// Extract text from rendered cells to get actual displayed content
column.nodes().flatten().to$().each(function () {
var text = $(this).text().trim();
if (text && !select.find('option[value="' + text + '"]').length) {
select.append('<option value="' + text + '">' + text + '</option>');
}
});
// Sort options
select.find('option:not(:first)').sort(function(a, b) {
return a.text.localeCompare(b.text);
}).appendTo(select);
});
rebind_checkbox_trigger_dxcc();
},
@@ -2437,17 +2442,22 @@ function saveOptions() {
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column.search(val ? '^' + val + '$' : '', true, false).draw();
// Search in rendered content, not just data
column.search(val ? val : '', true, false).draw();
});
column
.data()
.unique()
.sort()
.each(function (d, j) {
select.append('<option value="' + d + '">' + d + '</option>');
});
// Extract text from rendered cells to get actual displayed content
column.nodes().flatten().to$().each(function () {
var text = $(this).text().trim();
if (text && !select.find('option[value="' + text + '"]').length) {
select.append('<option value="' + text + '">' + text + '</option>');
}
});
// Sort options
select.find('option:not(:first)').sort(function(a, b) {
return a.text.localeCompare(b.text);
}).appendTo(select);
});
rebind_checkbox_trigger_cq_zone();
@@ -2502,17 +2512,22 @@ function saveOptions() {
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column.search(val ? '^' + val + '$' : '', true, false).draw();
// Search in rendered content, not just data
column.search(val ? val : '', true, false).draw();
});
column
.data()
.unique()
.sort()
.each(function (d, j) {
select.append('<option value="' + d + '">' + d + '</option>');
});
// Extract text from rendered cells to get actual displayed content
column.nodes().flatten().to$().each(function () {
var text = $(this).text().trim();
if (text && !select.find('option[value="' + text + '"]').length) {
select.append('<option value="' + text + '">' + text + '</option>');
}
});
// Sort options
select.find('option:not(:first)').sort(function(a, b) {
return a.text.localeCompare(b.text);
}).appendTo(select);
});
rebind_checkbox_trigger_itu_zone();
},
@@ -2681,17 +2696,22 @@ function saveOptions() {
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
column.search(val ? '^' + val + '$' : '', true, false).draw();
// Search in rendered content, not just data
column.search(val ? val : '', true, false).draw();
});
column
.data()
.unique()
.sort()
.each(function (d, j) {
select.append('<option value="' + d + '">' + d + '</option>');
});
// Extract text from rendered cells to get actual displayed content
column.nodes().flatten().to$().each(function () {
var text = $(this).text().trim();
if (text && !select.find('option[value="' + text + '"]').length) {
select.append('<option value="' + text + '">' + text + '</option>');
}
});
// Sort options
select.find('option:not(:first)').sort(function(a, b) {
return a.text.localeCompare(b.text);
}).appendTo(select);
});
},
});
@@ -2817,3 +2837,80 @@ function saveOptions() {
}
});
}
function checkIota() {
$('#checkIotaBtn').prop("disabled", true).addClass("running");
$('#closeButton').prop("disabled", true);
$.ajax({
url: base_url + 'index.php/logbookadvanced/checkDb',
data: {
type: 'checkiota'
},
type: 'POST',
success: function(response) {
$('#checkIotaBtn').prop("disabled", false).removeClass("running");
$('#closeButton').prop("disabled", false);
$('.result').html(response);
$('#iotaCheckTable').DataTable({
"pageLength": 25,
responsive: false,
ordering: false,
"scrollY": "510px",
"scrollCollapse": true,
"paging": false,
"scrollX": false,
"language": {
url: getDataTablesLanguageUrl(),
},
initComplete: function () {
this.api()
.columns('.select-filter')
.every(function () {
var column = this;
var select = $('<select class="form-select form-select-sm"><option value=""></option></select>')
.appendTo($(column.footer()).empty())
.on('change', function () {
var val = $.fn.dataTable.util.escapeRegex($(this).val());
// Search in rendered content, not just data
column.search(val ? val : '', true, false).draw();
});
// Extract text from rendered cells to get actual displayed content
column.nodes().flatten().to$().each(function () {
// Get text from the first anchor link which contains the IOTA reference
var $anchor = $(this).find('a').first();
var text = $anchor.length ? $anchor.text().trim() : $(this).text().trim();
// Remove any extra whitespace
text = text.split(/\s+/)[0];
if (text && !select.find('option[value="' + text + '"]').length) {
select.append('<option value="' + text + '">' + text + '</option>');
}
});
// Sort options
select.find('option:not(:first)').sort(function(a, b) {
return a.text.localeCompare(b.text);
}).appendTo(select);
});
},
});
},
error: function(xhr, status, error) {
$('#checkIotaBtn').prop("disabled", false).removeClass("running");
$('#closeButton').prop('disabled', false);
let errorMsg = 'Error checking iota information';
if (xhr.responseJSON && xhr.responseJSON.message) {
errorMsg += ': ' + xhr.responseJSON.message;
}
BootstrapDialog.alert({
title: 'Error',
message: errorMsg,
type: BootstrapDialog.TYPE_DANGER
});
}
});
}