|
|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|