Merge pull request #2820 from AndreasK79/dbtools_grid_map

[DBTools] Added a map to show map for incorrect grids
This commit is contained in:
Andreas Kristiansen
2026-01-20 07:39:41 +01:00
committed by GitHub
5 changed files with 324 additions and 1 deletions

View File

@@ -100,6 +100,7 @@ class Logbookadvanced extends CI_Controller {
'assets/js/leaflet/geocoding.js',
'assets/js/globe/globe.gl.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/globe/globe.gl.js")),
'assets/js/bootstrap-multiselect.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/bootstrap-multiselect.js")),
'assets/js/leaflet/L.MaidenheadColouredGridMap.js',
];
$this->load->view('interface_assets/header', $data);
@@ -927,4 +928,20 @@ class Logbookadvanced extends CI_Controller {
print json_encode($result);
}
function showMapForIncorrectGrid() {
if(!clubaccess_check(9)) return;
$this->load->model('logbookadvanced_model');
$dxcc = $this->input->post('dxcc', true);
$data['grids'] = $this->logbookadvanced_model->getGridsForDxcc($dxcc);
$data['dxcc'] = $dxcc;
$data['gridsquare'] = $this->input->post('gridsquare', true);
$dxccname = $this->input->post('dxccname', true);
$data['title'] = sprintf(__("Map for DXCC %s and gridsquare %s."), $dxccname, $data['gridsquare']);
header("Content-Type: application/json");
print json_encode($data);
}
}

View File

@@ -2190,4 +2190,15 @@ class Logbookadvanced_model extends CI_Model {
return $allResults;
}
function getGridsForDxcc($dxcc) {
$sql = "select group_concat(distinct gridsquare order by gridsquare separator ', ') grids
from vuccgrids
where adif = ?";
$query = $this->db->query($sql, array($dxcc));
$row = $query->row();
return $row->grids;
}
}

View File

@@ -165,6 +165,7 @@ function check_incorrect_gridsquares($result, $custom_date_format) { ?>
<th class="select-filter" scope="col"><?= __("DXCC"); ?></th>
<th><?= __("Gridsquare"); ?></th>
<th><?= __("DXCC Gridsquare"); ?></th>
<th><?= __("Map"); ?></th>
</tr>
</thead>
<tbody>
@@ -193,6 +194,7 @@ function check_incorrect_gridsquares($result, $custom_date_format) { ?>
}
?>
</td>
<td><a href="javascript:showMapForIncorrectGrid('<?php echo $qso->col_gridsquare; ?>','<?php echo $qso->col_dxcc; ?>','<?php echo htmlspecialchars(ucwords(strtolower($qso->col_country), "- (/"), ENT_QUOTES, 'UTF-8'); ?>')"><i class="fas fa-map-marker-alt"></i> <?php echo __('View on map'); ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -207,6 +209,7 @@ function check_incorrect_gridsquares($result, $custom_date_format) { ?>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</tfoot>
</table>

View File

@@ -77,7 +77,8 @@
let lang_gen_advanced_logbook_show_more = '<?= __("Show more"); ?>';
let lang_gen_advanced_logbook_show_less = '<?= __("Show less"); ?>';
let lang_gen_advanced_logbook_confirmedLabel = '<?= __("Gridsquares for"); ?>';
let lang_gen_advanced_logbook_workedLabel = '<?= __("Non DXCC matching gridsquare"); ?>';
let homegrid ='<?php echo strtoupper($homegrid[0]); ?>';
<?php

View File

@@ -2914,3 +2914,294 @@ function saveOptions() {
}
});
}
// Helper function to convert maidenhead grid to lat/lng bounds
function maidenheadToBounds(grid) {
if (!grid || grid.length < 2) return null;
grid = grid.toUpperCase();
const d1 = "ABCDEFGHIJKLMNOPQR";
const d2 = "ABCDEFGHIJKLMNOPQRSTUVWX";
let lon = -180;
let lat = -90;
let lonWidth = 20;
let latHeight = 10;
// First pair (field)
if (grid.length >= 2) {
const lonIdx = d1.indexOf(grid[0]);
const latIdx = d1.indexOf(grid[1]);
if (lonIdx >= 0 && latIdx >= 0) {
lon += lonIdx * 20;
lat += latIdx * 10;
lonWidth = 20;
latHeight = 10;
}
}
// Second pair (square)
if (grid.length >= 4) {
const lonIdx = parseInt(grid[2]);
const latIdx = parseInt(grid[3]);
if (!isNaN(lonIdx) && !isNaN(latIdx)) {
lon += lonIdx * 2;
lat += latIdx * 1;
lonWidth = 2;
latHeight = 1;
}
}
// Third pair (subsquare)
if (grid.length >= 6) {
const lonIdx = d2.indexOf(grid[4]);
const latIdx = d2.indexOf(grid[5]);
if (lonIdx >= 0 && latIdx >= 0) {
lon += lonIdx * (2 / 24);
lat += latIdx * (1 / 24);
lonWidth = 2 / 24;
latHeight = 1 / 24;
}
}
return L.latLngBounds([lat, lon], [lat + latHeight, lon + lonWidth]);
}
function showMapForIncorrectGrid(gridsquare, dxcc, dxccname) {
$.ajax({
url: base_url + 'index.php/logbookadvanced/showMapForIncorrectGrid',
type: 'post',
data: {
gridsquare: gridsquare,
dxcc: dxcc,
dxccname: dxccname
},
success: function (data) {
// Add metadata to data object
data.gridsquareDisplay = gridsquare;
data.dxccnameDisplay = dxccname;
BootstrapDialog.show({
title: data.title,
size: BootstrapDialog.SIZE_WIDE,
cssClass: 'mapdialog',
nl2br: false,
message: '<div class="mapgridcontent"><div id="mapgridcontainer" style="Height: 70vh"></div></div>',
onshown: function(dialog) {
drawMap(data);
},
buttons: [{
label: lang_admin_close,
action: function (dialogItself) {
dialogItself.close();
}
}]
});
}
});
}
function drawMap(data) {
if (typeof(user_map_custom.qsoconfirm) !== 'undefined') {
confirmedColor = user_map_custom.qsoconfirm.color;
}
if (typeof(user_map_custom.qso) !== 'undefined') {
workedColor = user_map_custom.qso.color;
}
let container = L.DomUtil.get('mapgridcontainer');
if(container != null){
container._leaflet_id = null;
container.remove();
$(".mapgridcontent").html('<div id="mapgridcontainer" style="Height:70vh"></div>');
}
// Initialize global arrays for colored maidenhead overlay
if (typeof grid_two === 'undefined') grid_two = [];
if (typeof grid_four === 'undefined') grid_four = [];
if (typeof grid_six === 'undefined') grid_six = [];
if (typeof grid_two_confirmed === 'undefined') grid_two_confirmed = [];
if (typeof grid_four_confirmed === 'undefined') grid_four_confirmed = [];
if (typeof grid_six_confirmed === 'undefined') grid_six_confirmed = [];
// Clear arrays
grid_two.length = 0;
grid_four.length = 0;
grid_six.length = 0;
grid_two_confirmed.length = 0;
grid_four_confirmed.length = 0;
grid_six_confirmed.length = 0;
grids = data.grids;
// Process data.grids - mark in green (confirmed)
if (data.grids) {
// data.grids can be a comma-separated string or an array
let gridsArray = Array.isArray(data.grids) ? data.grids : data.grids.split(',').map(g => g.trim());
gridsArray.forEach(function(grid) {
let gridUpper = grid.toUpperCase();
if (gridUpper.length === 2) {
grid_two_confirmed.push(gridUpper);
grid_two.push(gridUpper); // Also add to worked so it shows up
} else if (gridUpper.length === 4) {
grid_four_confirmed.push(gridUpper);
grid_four.push(gridUpper); // Also add to worked so it shows up
} else if (gridUpper.length === 6) {
grid_six_confirmed.push(gridUpper);
grid_six.push(gridUpper); // Also add to worked so it shows up
}
});
}
// Process data.gridsquare - mark first 4 letters in red (worked)
if (data.gridsquare) {
let gridsquareUpper = data.gridsquare.toUpperCase().substring(0, 4);
if (gridsquareUpper.length >= 2) {
let twoChar = gridsquareUpper.substring(0, 2);
if (!grid_two_confirmed.includes(twoChar)) {
grid_two.push(twoChar);
}
}
if (gridsquareUpper.length >= 4) {
let fourChar = gridsquareUpper.substring(0, 4);
if (!grid_four_confirmed.includes(fourChar)) {
grid_four.push(fourChar);
}
}
}
// Collect all grids to calculate bounds for auto-zoom
// Include both data.grids (green) and data.gridsquare (red)
let allGrids = [];
if (data.grids) {
let gridsArray = Array.isArray(data.grids) ? data.grids : data.grids.split(',').map(g => g.trim());
allGrids = allGrids.concat(gridsArray);
}
if (data.gridsquare) {
allGrids.push(data.gridsquare.substring(0, Math.min(4, data.gridsquare.length)));
}
// Calculate bounds and center for auto-zoom
let bounds = null;
let centerLat = 0;
let centerLng = 0;
let minLat = 90;
let maxLat = -90;
let allLngs = [];
allGrids.forEach(function(grid) {
let gridBounds = maidenheadToBounds(grid);
if (gridBounds) {
// Track center points and extents for better handling
let gridCenter = gridBounds.getCenter();
centerLat += gridCenter.lat;
allLngs.push(gridCenter.lng);
if (gridBounds.getSouth() < minLat) minLat = gridBounds.getSouth();
if (gridBounds.getNorth() > maxLat) maxLat = gridBounds.getNorth();
if (bounds) {
bounds.extend(gridBounds);
} else {
bounds = gridBounds;
}
}
});
// Calculate average center
if (allLngs.length > 0) {
centerLat = centerLat / allGrids.length;
// Check if longitudes span more than 180° (crossing antimeridian or covering large area)
let minLng = Math.min(...allLngs);
let maxLng = Math.max(...allLngs);
let lngSpan = maxLng - minLng;
if (lngSpan > 300) {
// Spans nearly the entire globe (like Asiatic Russia from -180 to 180)
// Use a predefined sensible center for such cases
centerLng = 120; // Center of Asiatic Russia/mainland Russia
} else if (lngSpan > 180) {
// When spanning >180°, we should go the "other way around" the globe
// Add 360° to any negative longitudes, then average, then normalize back
let wrappedLngs = allLngs.map(lng => lng < 0 ? lng + 360 : lng);
let avgWrapped = wrappedLngs.reduce((a, b) => a + b, 0) / wrappedLngs.length;
// Normalize to -180 to 180 range
if (avgWrapped > 180) avgWrapped -= 360;
centerLng = avgWrapped;
} else {
// Normal case - simple average
centerLng = allLngs.reduce((a, b) => a + b, 0) / allLngs.length;
}
}
// Make map global for L.MaidenheadColouredGridMap.js
window.map = new L.Map('mapgridcontainer', {
fullscreenControl: true,
fullscreenControlOptions: {
position: 'topleft'
},
});
let maidenhead = L.maidenhead().addTo(window.map);
let osmUrl = option_map_tile_server;
let osmAttrib= option_map_tile_server_copyright;
let osm = new L.TileLayer(osmUrl, {minZoom: 1, maxZoom: 12, attribution: osmAttrib});
let redIcon = L.icon({
iconUrl: icon_dot_url,
iconSize: [10, 10], // size of the icon
});
window.map.addLayer(osm);
// Add legend
let legend = L.control({position: 'topright'});
legend.onAdd = function (map) {
let div = L.DomUtil.create('div', 'info legend');
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.borderRadius = '5px';
div.style.boxShadow = '0 0 10px rgba(0,0,0,0.2)';
div.innerHTML =
'<div style="display: flex; align-items: center; margin-bottom: 8px;">' +
'<div style="width: 20px; height: 20px; background-color: ' + confirmedColor + '; border: 1px solid #ccc; margin-right: 8px;"></div>' +
'<span style="font-size: 12px;">' + lang_gen_advanced_logbook_confirmedLabel + ' ' + data.dxccnameDisplay + '</span>' +
'</div>' +
'<div style="display: flex; align-items: center;">' +
'<div style="width: 20px; height: 20px; background-color: ' + workedColor + '; border: 1px solid #ccc; margin-right: 8px;"></div>' +
'<span style="font-size: 12px;">' + lang_gen_advanced_logbook_workedLabel + ' ' + data.gridsquareDisplay + '</span>' +
'</div>';
return div;
};
legend.addTo(window.map);
// Zoom to fit all grids with padding
if (bounds) {
const latSpan = maxLat - minLat;
const lngSpan = Math.max(...allLngs) - Math.min(...allLngs);
// For extremely large spans (near 360° like Asiatic Russia), use manual center
// For moderate spans (100-200° like Japan+GM05), use fitBounds with lower maxZoom
// For smaller spans, use fitBounds normally
if (lngSpan > 300) {
// Spans nearly the entire globe - use calculated center with fixed zoom
let zoom = 3; // Increased from 2 to 3 for better detail
window.map.setView([centerLat, centerLng], zoom);
} else if (lngSpan > 100) {
// Large span (like Japan to western hemisphere) - use fitBounds but limit zoom
window.map.fitBounds(bounds, { padding: [30, 30], maxZoom: 3 });
} else {
// Normal case - use fitBounds
let maxZoom = 10;
if (lngSpan < 50) maxZoom = 7;
if (lngSpan < 20) maxZoom = 10;
window.map.fitBounds(bounds, { padding: [50, 50], maxZoom: maxZoom });
}
} else {
window.map.setView([30, 0], 1.5);
}
}