= ('No QSOs with 6+ character grids found for the selected country.') ?>
@@ -63,9 +90,9 @@
-
-
-
diff --git a/assets/js/sections/qso_map.js b/assets/js/sections/qso_map.js
new file mode 100644
index 000000000..779cd9b89
--- /dev/null
+++ b/assets/js/sections/qso_map.js
@@ -0,0 +1,589 @@
+let maidenhead;
+let zonemarkers = [];
+let ituzonemarkers = [];
+let map = null;
+let info;
+let geojsonlayer;
+
+// Wait for jQuery to be loaded
+function initMap() {
+ let markers = [];
+ let geojsonLayers = []; // Store multiple GeoJSON layers
+ let allQsos = []; // Store all QSOs for filtering
+ let legendAdded = false; // Track if legend has been added
+ let legendControl = null; // Store legend control for updates
+
+ // Enable/disable load button based on country selection
+ $('#countrySelect, #locationSelect').on('change', function() {
+ const countrySelected = $('#countrySelect').val();
+ $('#loadMapBtn').prop('disabled', !countrySelected);
+ $('#showOnlyOutside').prop('disabled', !countrySelected);
+ $('#mapContainer, #noDataMessage').hide();
+ });
+
+ // Handle checkbox change
+ $('#showOnlyOutside').on('change', function() {
+ if (allQsos.length > 0) {
+ filterAndDisplayMarkers(allQsos, $(this).is(':checked'));
+ }
+ });
+
+ // Load map when button is clicked
+ $('#loadMapBtn').on('click', function() {
+ const country = $('#countrySelect').val();
+ const dxcc = $('#countrySelect option:selected').data('dxcc');
+ const stationId = $('#locationSelect').val();
+ if (!country) return;
+
+ // Fetch QSO data
+ const loadingText = country === 'all' ? 'Loading QSOs for all countries (this may take a moment)...' : 'Loading QSO data...';
+ $('#loadingSpinner').removeClass('d-none');
+ $('#loadingText').text(loadingText).removeClass('d-none');
+ $('#loadMapBtn').prop('disabled', true);
+
+ // Set timeout for long-running requests
+ const timeout = setTimeout(function() {
+ $('#loadingText').text('Still loading... Processing large dataset, please wait...');
+ }, 5000);
+
+ $.ajax({
+ url: base_url + 'index.php/map/get_qsos_for_country',
+ method: 'POST',
+ dataType: 'json',
+ data: {
+ country: country,
+ dxcc: dxcc,
+ station_id: stationId
+ },
+ success: function(response) {
+ clearTimeout(timeout);
+ $('#loadingSpinner').addClass('d-none');
+ $('#loadingText').addClass('d-none');
+ $('#loadMapBtn').prop('disabled', false);
+
+ // Check if response is a string and parse it if needed
+ if (typeof response === 'string') {
+ try {
+ response = JSON.parse(response);
+ } catch (e) {
+ alert('Error parsing response: ' + e.message);
+ return;
+ }
+ }
+
+ if (response.error) {
+ alert('Error: ' + response.error);
+ return;
+ }
+
+ if (!Array.isArray(response)) {
+ console.log('Response is not an array:', response);
+ alert('Error: Expected an array of QSOs but received something else');
+ return;
+ }
+
+ if (response.length === 0) {
+ $('#noDataMessage').show();
+ return;
+ }
+
+ // Store all QSOs and initialize map
+ allQsos = response;
+ const showOnlyOutside = $('#showOnlyOutside').is(':checked');
+ filterAndDisplayMarkers(allQsos, showOnlyOutside);
+ }
+ }).fail(function() {
+ clearTimeout(timeout);
+ $('#loadingSpinner').addClass('d-none');
+ $('#loadingText').addClass('d-none');
+ $('#loadMapBtn').prop('disabled', false);
+ alert('Failed to load QSO data. Please try again.');
+ });
+ });
+
+ async function filterAndDisplayMarkers(qsos, showOnlyOutside = false) {
+ // Clear existing markers and layers
+ clearMap();
+
+ // Filter QSOs if checkbox is checked
+ const filteredQsos = showOnlyOutside ? qsos.filter(qso => qso.inside_geojson === false) : qsos;
+
+ // Create map if it doesn't exist
+ if (!map) {
+ map = L.map('mapgeojson').setView([40, 0], 2);
+
+ // Add OpenStreetMap tiles
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 18,
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(map);
+ }
+
+ maidenhead = L.maidenheadqrb().addTo(map);
+ map.on('mousemove', onMapMove);
+ $('.cohidden').show();
+
+ if (typeof gridsquare_layer !== 'undefined') {
+ toggleGridsquares(gridsquare_layer);
+ } else {
+ toggleGridsquares(false);
+ }
+
+ // Check if we have country boundaries
+ const selectedOption = $('#countrySelect option:selected');
+ const dxcc = selectedOption.data('dxcc');
+ const country = $('#countrySelect').val();
+
+ // Add QSO markers first
+ let bounds = [];
+ let outsideCount = 0;
+ let insideCount = 0;
+
+ filteredQsos.forEach(function(qso) {
+ let marker;
+ let icon;
+
+ // Check if QSO is inside GeoJSON boundary
+ if (qso.inside_geojson === false) {
+ // Create red X icon for QSOs outside GeoJSON
+ icon = L.divIcon({
+ html: '
✕
',
+ iconSize: [24, 24],
+ className: 'custom-div-icon'
+ });
+ outsideCount++;
+ } else {
+ // Create green checkmark icon for QSOs inside GeoJSON
+ icon = L.divIcon({
+ html: '
✓
',
+ iconSize: [24, 24],
+ className: 'custom-div-icon'
+ });
+ insideCount++;
+ }
+
+ marker = L.marker([qso.lat, qso.lng], { icon: icon })
+ .bindPopup(qso.popup +
+ (qso.inside_geojson === false ? '
⚠ Outside country boundaries' :
+ '
✓ Inside country boundaries'))
+ .addTo(map);
+
+ markers.push(marker);
+ bounds.push([qso.lat, qso.lng]);
+ });
+
+ // Try to load GeoJSON for the country/countries
+ if (dxcc && supportedDxccs.includes(parseInt(dxcc))) {
+ // Single country GeoJSON
+ $.ajax({
+ url: base_url + "index.php/map/get_country_geojson/",
+ type: 'post',
+ data: { dxcc: dxcc },
+ success: function(geojson) {
+ if (geojson && !geojson.error) {
+ geojsonlayer = L.geoJSON(geojson, {
+ style: {
+ color: '#ff0000',
+ weight: 2,
+ opacity: 0.5,
+ fillOpacity: 0.2
+ },
+ onEachFeature: onEachFeature
+ }).addTo(map);
+ geojsonLayers.push(geojsonlayer);
+
+
+
+ // Fit map to show both GeoJSON and markers
+ setTimeout(function() {
+ const geoBounds = geojsonlayer.getBounds();
+ if (bounds.length > 0) {
+ const markerBounds = L.latLngBounds(bounds);
+ // Combine bounds
+ geoBounds.extend(markerBounds);
+ }
+ map.fitBounds(geoBounds, { padding: [20, 20] });
+ }, 100);
+ } else {
+ // No GeoJSON, fit to markers only
+ if (bounds.length > 0) {
+ const markerBounds = L.latLngBounds(bounds);
+ map.fitBounds(markerBounds, { padding: [50, 50] });
+ }
+ }
+ },
+ error: function() {
+ // GeoJSON failed to load, fit to markers only
+ if (bounds.length > 0) {
+ const markerBounds = L.latLngBounds(bounds);
+ map.fitBounds(markerBounds, { padding: [50, 50] });
+ }
+ }
+ });
+ } else {
+ // No GeoJSON support, fit to markers only
+ if (bounds.length > 0) {
+ const markerBounds = L.latLngBounds(bounds);
+ map.fitBounds(markerBounds, { padding: [50, 50] });
+ }
+ }
+
+ $('#mapContainer').show();
+
+ // Remove existing info control if it exists
+ if (info) {
+ map.removeControl(info);
+ }
+
+ // Add or update legend
+ if (!legendAdded) {
+ addLegend(insideCount, outsideCount, qsos.length, showOnlyOutside);
+ legendAdded = true;
+ } else {
+ // Update existing legend counts
+ updateLegend(insideCount, outsideCount, qsos.length, showOnlyOutside);
+ }
+
+ // Always re-add info control after legend to ensure correct order
+ info = L.control();
+
+ info.onAdd = function (map) {
+ this._div = L.DomUtil.create('div', 'info');
+ this.update();
+ return this._div;
+ };
+
+ info.update = function (props) {
+ this._div.innerHTML = '
Region
' + (props ?
+ '
' + props.code + ' - ' + props.name + '' : 'Hover over a region');
+ };
+
+ info.addTo(map);
+
+ // Force map to recalculate its size
+ setTimeout(function() {
+ if (map) {
+ map.invalidateSize();
+
+ // Re-fit bounds after size invalidation
+ if (bounds.length > 0) {
+ const markerBounds = L.latLngBounds(bounds);
+ map.fitBounds(markerBounds, { padding: [50, 50] });
+ }
+ }
+ }, 100);
+ }
+
+ function onEachFeature(feature, layer) {
+ layer.on({
+ mouseover: highlightFeature,
+ mouseout: resetHighlight,
+ click: onClick2
+ });
+ }
+
+ function highlightFeature(e) {
+ var layer = e.target;
+
+ layer.setStyle({
+ weight: 3,
+ // color: 'white',
+ dashArray: '',
+ fillOpacity: 0.6
+ });
+
+ layer.bringToFront();
+ info.update(layer.feature.properties);
+ }
+
+ function zoomToFeature(e) {
+ map.fitBounds(e.target.getBounds());
+ }
+
+ function onClick2(e) {
+ zoomToFeature(e);
+ let marker = e.target;
+ }
+
+ function resetHighlight(e) {
+ geojsonlayer.resetStyle(e.target);
+ info.update();
+ }
+
+ function addLegend(insideCount, outsideCount, totalCount, showOnlyOutside) {
+ const legend = L.control({ position: 'topright' });
+
+ legend.onAdd = function(map) {
+ const div = L.DomUtil.create('div', 'legend');
+
+ let html = '
Legend
';
+
+ // Inside boundaries
+ html += '
';
+ html += '
';
+ html += '
✓
';
+ html += '
';
+ html += '
Inside boundaries (' + insideCount + ')';
+ html += '
';
+
+ // Outside boundaries
+ html += '
';
+ html += '
';
+ html += '
✕
';
+ html += '
';
+ html += '
Outside boundaries (' + outsideCount + ')';
+ html += '
';
+
+ // GeoJSON boundaries
+ html += '
';
+ html += '
';
+ html += '';
+ html += '
';
+ html += '
Country/State boundaries';
+ html += '
';
+
+ // Total QSOs (shown differently when filtering)
+ if (showOnlyOutside) {
+ html += '
';
+ html += 'Showing ' + outsideCount + ' of ' + totalCount + ' total QSOs';
+ html += '
';
+ } else {
+ html += '
';
+ html += 'Total: ' + totalCount + ' QSOs with 6+ char grids';
+ html += '
';
+ }
+
+ html += '
';
+ html += '
Toggle layers
';
+ html += '
' + lang_gen_hamradio_gridsquares + '';
+ html += '
' + lang_gen_hamradio_cq_zones + '';
+ html += '
' + lang_gen_hamradio_itu_zones + '';
+
+ div.innerHTML = html;
+
+ // Prevent map events on the legend
+ L.DomEvent.disableClickPropagation(div);
+ L.DomEvent.disableScrollPropagation(div);
+
+ return div;
+ };
+
+ legendControl = legend;
+ legend.addTo(map);
+ }
+
+ function updateLegend(insideCount, outsideCount, totalCount, showOnlyOutside) {
+ if (!legendControl) return;
+
+ // Remove the legend and re-add it with updated counts
+ map.removeControl(legendControl);
+ addLegend(insideCount, outsideCount, totalCount, showOnlyOutside);
+ }
+
+ function clearMap() {
+ // Remove existing markers
+ markers.forEach(function(marker) {
+ map.removeLayer(marker);
+ });
+ markers = [];
+
+ // Remove all GeoJSON layers
+ geojsonLayers.forEach(function(layer) {
+ map.removeLayer(layer);
+ });
+ geojsonLayers = [];
+ }
+}
+
+// Check if jQuery is loaded, if not wait for it
+if (typeof $ === 'undefined') {
+ // jQuery not yet loaded, add event listener
+ document.addEventListener('DOMContentLoaded', function() {
+ if (typeof $ === 'undefined') {
+ // Wait for jQuery to load
+ var checkJQuery = setInterval(function() {
+ if (typeof $ !== 'undefined') {
+ clearInterval(checkJQuery);
+ initMap();
+ }
+ }, 100);
+ } else {
+ initMap();
+ }
+ });
+} else {
+ // jQuery already loaded
+ $(document).ready(function() {
+ initMap();
+ });
+}
+
+
+function toggleCqZones(bool) {
+ if(!bool) {
+ zonemarkers.forEach(function (item) {
+ map.removeLayer(item);
+ });
+ if (geojson != undefined) {
+ map.removeLayer(geojson);
+ }
+ } else {
+ geojson = L.geoJson(zonestuff, {style: style}).addTo(map);
+ for (var i = 0; i < cqzonenames.length; i++) {
+
+ var title = '
' + (Number(i)+Number(1)) + '';
+ var myIcon = L.divIcon({className: 'my-div-icon', html: title});
+
+ var marker = L.marker(
+ [cqzonenames[i][0], cqzonenames[i][1]], {
+ icon: myIcon,
+ title: (Number(i)+Number(1)),
+ zIndex: 1000,
+ }
+ ).addTo(map);
+ zonemarkers.push(marker);
+ }
+ }
+}
+
+function toggleItuZones(bool) {
+ if(!bool) {
+ ituzonemarkers.forEach(function (item) {
+ map.removeLayer(item);
+ });
+ if (itugeojson != undefined) {
+ map.removeLayer(itugeojson);
+ }
+ } else {
+ itugeojson = L.geoJson(ituzonestuff, {style: style}).addTo(map);
+ for (var i = 0; i < ituzonenames.length; i++) {
+
+ var title = '
' + (Number(i)+Number(1)) + '';
+ var myIcon = L.divIcon({className: 'my-div-icon', html: title});
+
+ var marker = L.marker(
+ [ituzonenames[i][0], ituzonenames[i][1]], {
+ icon: myIcon,
+ title: (Number(i)+Number(1)),
+ zIndex: 1000,
+ }
+ ).addTo(map);
+ ituzonemarkers.push(marker);
+ }
+ }
+}
+
+function toggleGridsquares(bool) {
+ if(!bool) {
+ map.removeLayer(maidenhead);
+ } else {
+ maidenhead.addTo(map);
+ }
+};
+
+const ituzonenames = [
+ ["60","-160"],
+ ["55","-125"],
+ ["55","-100"],
+ ["55","-78"],
+ ["73","-40"],
+ ["40","-119"],
+ ["40","-100"],
+ ["40","-80"],
+ ["55","-60"],
+ ["20","-102"],
+ ["21","-75"],
+ ["-3","-72"],
+ ["-5","-45"],
+ ["-30","-65"],
+ ["-25","-45"],
+ ["-50","-65"],
+ ["61","-26"],
+ ["70","10"],
+ ["70","40"],
+ ["70","62.5"],
+ ["70","82.5"],
+ ["70","100"],
+ ["70","122.5"],
+ ["70","142.5"],
+ ["70","162.5"],
+ ["70","180"],
+ ["52","2"],
+ ["45","18"],
+ ["53","36"],
+ ["53","62.5"],
+ ["53","82.5"],
+ ["53","100"],
+ ["53","122.5"],
+ ["53","142"],
+ ["55","160"],
+ ["35","-25"],
+ ["35","0"],
+ ["27.5","22.5"],
+ ["27","42"],
+ ["32","56"],
+ ["10","75"],
+ ["39","82.5"],
+ ["33","100"],
+ ["33","118"],
+ ["33","140"],
+ ["15","-10"],
+ ["12.5","22"],
+ ["5","40"],
+ ["15","100"],
+ ["10","120"],
+ ["-4","150"],
+ ["-7","17"],
+ ["-12.5","45"],
+ ["-2","115"],
+ ["-20","140"],
+ ["-20","170"],
+ ["-30","24"],
+ ["-25","120"],
+ ["-40","140"],
+ ["-40","170"],
+ ["15","-170"],
+ ["-15","-170"],
+ ["-15","-135"],
+ ["10","140"],
+ ["10","162"],
+ ["-23","-11"],
+ ["-70","10"],
+ ["-47.5","60"],
+ ["-70","70"],
+ ["-70","130"],
+ ["-70","-170"],
+ ["-70","-110"],
+ ["-70","-050"],
+ ["-82.5","0"],
+ ["82.5","0"],
+ ["40","-150"],
+ ["15","-135"],
+ ["-15","-95"],
+ ["-40","-160"],
+ ["-40","-125"],
+ ["-40","-90"],
+ ["50","-30"],
+ ["25","-47.5"],
+ ["-45","-40"],
+ ["-45","10"],
+ ["-25","70"],
+ ["-25","95"],
+ ["-50","95"],
+ ["-54","140"],
+ ["39","165"]
+];
+
+function style(feature) {
+ var bordercolor = "black";
+ if (isDarkModeTheme()) {
+ bordercolor = "white";
+ }
+ return {
+ fillColor: "white",
+ fillOpacity: 0,
+ opacity: 0.65,
+ color: bordercolor,
+ weight: 1,
+ };
+}