[Map] Added map for plotting QSOs in geojson

This commit is contained in:
Andreas Kristiansen
2025-12-13 14:00:23 +01:00
parent 3d0949cbdc
commit 87a81f5c44
4 changed files with 835 additions and 13 deletions

View File

@@ -3,10 +3,199 @@
class Map extends CI_Controller {
function __construct()
{
parent::__construct();
$this->load->helper(array('form', 'url', 'psr4_autoloader'));
$this->load->model('user_model');
if (!$this->user_model->authorize(2)) {
$this->session->set_flashdata('error', __("You're not allowed to do that!"));
redirect('dashboard');
}
}
function index() {
redirect('dashboard');
}
/**
* QSO Map with country selection and OpenStreetMap
*/
public function qso_map() {
$this->load->library('Geojson');
$this->load->model('Map_model');
// Get supported DXCC countries with state data
$supported_dxccs = $this->geojson->getSupportedDxccs();
// Fetch available countries from the logbook
$countries = $this->Map_model->get_available_countries();
// Filter countries to only include those with GeoJSON support
$supported_country_codes = array_keys($supported_dxccs);
$filtered_countries = array_filter($countries, function($country) use ($supported_country_codes) {
return in_array($country['COL_DXCC'], $supported_country_codes);
});
// Fetch station profiles
$station_profiles = $this->Map_model->get_station_profiles();
$data['countries'] = $filtered_countries;
$data['station_profiles'] = $station_profiles;
$data['supported_dxccs'] = $supported_dxccs;
$this->load->view('interface_assets/header', $data);
$this->load->view('map/qso_map', $data);
$this->load->view('interface_assets/footer');
}
/**
* AJAX endpoint to get QSO data for a specific country
*/
public function get_qsos_for_country() {
$this->load->model('Map_model');
$this->load->library('Geojson');
$country = $this->input->post('country');
$dxcc = $this->input->post('dxcc');
$station_id = $this->input->post('station_id');
if (empty($country)) {
while (ob_get_level()) ob_end_clean();
$this->output
->set_content_type('application/json')
->set_output(json_encode(['error' => 'Country not specified']));
return;
}
// Convert "all" to null for all stations
$station_id = ($station_id === 'all') ? null : $station_id;
try {
$qsos = $this->Map_model->get_qsos_by_country($country, $station_id, $limit);
if (empty($qsos)) {
while (ob_get_level()) ob_end_clean();
$this->output
->set_content_type('application/json')
->set_output(json_encode(['error' => 'No QSOs found with 6+ character gridsquares']));
return;
}
} catch (Exception $e) {
while (ob_get_level()) ob_end_clean();
$this->output
->set_content_type('application/json')
->set_output(json_encode(['error' => 'Database query failed: ' . $e->getMessage()]));
return;
}
// Check if QSOs are inside GeoJSON boundaries
try {
if ($country === 'all') {
// For all countries, optimize by caching GeoJSON files and checking in batches
$geojsonCache = [];
foreach ($qsos as &$qso) {
if ($qso['COL_DXCC'] && $this->geojson->isStateSupported($qso['COL_DXCC'])) {
$dxcc = $qso['COL_DXCC'];
// Cache GeoJSON data to avoid repeated file loading
if (!isset($geojsonCache[$dxcc])) {
$geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
$geojsonCache[$dxcc] = $this->geojson->loadGeoJsonFile($geojsonFile);
}
$geojsonData = $geojsonCache[$dxcc];
if ($geojsonData !== null) {
$state = $this->geojson->findFeatureContainingPoint($qso['lat'], $qso['lng'], $geojsonData);
$qso['inside_geojson'] = ($state !== null);
$qso['state_info'] = $state;
} else {
$qso['inside_geojson'] = true; // Assume inside if no GeoJSON file
$qso['state_info'] = null;
}
} else {
$qso['inside_geojson'] = true; // Assume inside for countries without GeoJSON
$qso['state_info'] = null;
}
}
// Free cache memory
unset($geojsonCache);
} elseif ($dxcc && $this->geojson->isStateSupported($dxcc)) {
// For single country, use original logic
$geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
$geojsonData = $this->geojson->loadGeoJsonFile($geojsonFile);
if ($geojsonData !== null) {
// Check each QSO if it's inside the GeoJSON
foreach ($qsos as &$qso) {
$state = $this->geojson->findFeatureContainingPoint($qso['lat'], $qso['lng'], $geojsonData);
$qso['inside_geojson'] = ($state !== null);
$qso['state_info'] = $state;
}
}
}
} catch (Exception $e) {
// If GeoJSON processing fails, log error but continue without boundary checking
log_message('error', 'GeoJSON processing error: ' . $e->getMessage());
foreach ($qsos as &$qso) {
if (!isset($qso['inside_geojson'])) {
$qso['inside_geojson'] = true;
$qso['state_info'] = null;
}
}
}
// Clear any output buffers that might contain warnings/errors
while (ob_get_level()) {
ob_end_clean();
}
// Set proper content type header
$this->output
->set_content_type('application/json')
->set_output(json_encode($qsos));
}
/**
* Get country boundaries as GeoJSON
*/
public function get_country_geojson($dxcc) {
$this->load->library('Geojson');
$geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
$geojsonData = $this->geojson->loadGeoJsonFile($geojsonFile);
if ($geojsonData === null) {
echo json_encode(['error' => 'GeoJSON file not found']);
return;
}
$this->output
->set_content_type('application/json')
->set_output(json_encode($geojsonData));
}
/**
* Get all supported DXCC countries with GeoJSON
*/
public function get_all_supported_countries() {
$this->load->library('Geojson');
$supported_dxccs = $this->geojson->getSupportedDxccs();
$country_list = [];
foreach ($supported_dxccs as $dxcc => $data) {
$geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
if (file_exists(FCPATH . $geojsonFile)) {
$country_list[] = [
'dxcc' => $dxcc,
'name' => $data['name'],
'geojson_file' => $geojsonFile
];
}
}
echo json_encode($country_list);
}
// Generic fonction for return Json for MAP //
public function map_plot_json() {
$this->load->model('Stations');
@@ -25,17 +214,4 @@ class Map extends CI_Controller {
echo json_encode(array_merge($plot_array, $station_array));
}
// Generic fonction for return Json for MAP //
public function glob_plot() {
$footerData = [];
$footerData['scripts'] = [
'assets/js/globe/globe.gl.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/globe/globe.gl.js")),
'assets/js/sections/globe.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/globe.js")),
];
$this->load->view('interface_assets/header');
$this->load->view('globe/index');
$this->load->view('interface_assets/footer',$footerData);
}
}

View File

@@ -0,0 +1,183 @@
<?php if (! defined('BASEPATH')) exit('No direct script access allowed');
class Map_model extends CI_Model {
/**
* Get available countries from the logbook with QSOs
*/
public function get_available_countries() {
$this->db->select('DISTINCT COL_COUNTRY, COL_DXCC, COUNT(*) as qso_count', FALSE);
$this->db->from($this->config->item('table_name'));
$this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->where('COL_COUNTRY IS NOT NULL');
$this->db->where('COL_COUNTRY !=', '');
$this->db->where("LENGTH(COL_GRIDSQUARE) >=", 6); // At least 6 chars
$this->db->group_by('COL_COUNTRY, COL_DXCC');
$this->db->order_by('COL_COUNTRY');
$query = $this->db->get();
return $query->result_array();
}
/**
* Get available station profiles for the user
*/
public function get_station_profiles() {
$this->db->select('station_profile.station_id, station_profile.station_profile_name', FALSE);
$this->db->from($this->config->item('table_name'));
$this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->group_by('station_profile.station_id, station_profile.station_profile_name');
$this->db->order_by('station_profile.station_profile_name');
$query = $this->db->get();
return $query->result_array();
}
/**
* Get QSOs for a specific country with 6+ character grids
*/
public function get_qsos_by_country($country, $station_id = null) {
if (!$this->load->is_loaded('Qra')) {
$this->load->library('Qra');
}
if (!$this->load->is_loaded('DxccFlag')) {
$this->load->library('DxccFlag');
}
$this->db->select('COL_PRIMARY_KEY, COL_CALL, COL_GRIDSQUARE, COL_COUNTRY, COL_DXCC, COL_MODE, COL_BAND, COL_TIME_ON, COL_RST_SENT, COL_RST_RCVD, station_profile.station_profile_name', FALSE);
$this->db->from($this->config->item('table_name'));
$this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->where('COL_COUNTRY', $country);
// Add station filter if specified
if ($station_id !== null && $station_id !== '') {
$this->db->where('station_profile.station_id', $station_id);
}
$this->db->where("LENGTH(COL_GRIDSQUARE) >=", 6); // At least 6 chars
$this->db->order_by('COL_TIME_ON', 'DESC');
$query = $this->db->get();
$qsos = $query->result_array();
// Process QSOs and convert gridsquares to coordinates
$result = [];
foreach ($qsos as $qso) {
$gridsquare = strtoupper(trim($qso['COL_GRIDSQUARE']));
// Only include QSOs with 6+ character grids
if (strlen($gridsquare) >= 6) {
$coords = $this->qra->qra2latlong($gridsquare);
if ($coords !== false && is_array($coords) && count($coords) >= 2) {
$result[] = [
'call' => $qso['COL_CALL'],
'gridsquare' => $gridsquare,
'country' => $qso['COL_COUNTRY'],
'dxcc' => $qso['COL_DXCC'],
'mode' => $qso['COL_MODE'],
'band' => $qso['COL_BAND'],
'time_on' => $qso['COL_TIME_ON'],
'rst_sent' => $qso['COL_RST_SENT'],
'rst_rcvd' => $qso['COL_RST_RCVD'],
'lat' => $coords[0],
'lng' => $coords[1],
'profile' => $qso['station_profile_name'],
'popup' => $this->createContentMessageDx($qso)
];
}
}
}
return $result;
}
/**
* Generate HTML content for QSO popup display
*/
public function createContentMessageDx($qso) {
$table = '<table><tbody>';
// Callsign with flag
$table .= '<tr>';
$table .= '<td colspan="2"><div class="big-flag">';
if (!empty($qso['COL_DXCC'])) {
$dxccFlag = $this->dxccflag->get($qso['COL_DXCC']);
$table .= '<div class="flag">' . htmlspecialchars($dxccFlag) . '</div>';
}
// Replace zeros with Ø in callsign
$callsign = str_replace('0', 'Ø', $qso['COL_CALL']);
$table .= '<a id="edit_qso" href="javascript:displayQso(' . $qso['COL_PRIMARY_KEY'] . ')">' . htmlspecialchars($callsign) . '</a></div>';
$table .= '</td>';
$table .= '</tr>';
// Date/Time
$table .= '<tr>';
$table .= '<td>Date/Time</td>';
$datetime = date('Y-m-d H:i', strtotime($qso['COL_TIME_ON']));
$table .= '<td>' . htmlspecialchars($datetime) . '</td>';
$table .= '</tr>';
// Band/Satellite
$table .= '<tr>';
if (!empty($qso['COL_SAT_NAME'])) {
$table .= '<td>Band</td>';
$table .= '<td>SAT ' . htmlspecialchars($qso['COL_SAT_NAME']);
if (!empty($qso['COL_SAT_MODE'])) {
$table .= ' (' . htmlspecialchars($qso['COL_SAT_MODE']) . ')';
}
$table .= '</td>';
} else {
$table .= '<td>Band</td>';
$table .= '<td>' . htmlspecialchars($qso['COL_BAND']) . '</td>';
}
$table .= '</tr>';
// Mode
$table .= '<tr>';
$table .= '<td>Mode</td>';
$table .= '<td>' . htmlspecialchars($qso['COL_MODE']) . '</td>';
$table .= '</tr>';
// Gridsquare
if (!empty($qso['COL_GRIDSQUARE'])) {
$table .= '<tr>';
$table .= '<td>Gridsquare</td>';
$table .= '<td>' . htmlspecialchars($qso['COL_GRIDSQUARE']) . '</td>';
$table .= '</tr>';
}
// Distance (if available)
if (isset($qso['distance'])) {
$table .= '<tr>';
$table .= '<td>Distance</td>';
$table .= '<td>' . htmlspecialchars($qso['distance']) . '</td>';
$table .= '</tr>';
}
// Bearing (if available)
if (isset($qso['bearing'])) {
$table .= '<tr>';
$table .= '<td>Bearing</td>';
$table .= '<td>' . htmlspecialchars($qso['bearing']) . '</td>';
$table .= '</tr>';
}
// Station Profile
if (!empty($qso['station_profile_name'])) {
$table .= '<tr>';
$table .= '<td>Station</td>';
$table .= '<td>' . htmlspecialchars($qso['station_profile_name']) . '</td>';
$table .= '</tr>';
}
$table .= '</tbody></table>';
return $table;
}
}

View File

@@ -340,6 +340,8 @@
<a class="dropdown-item" href="<?php echo site_url('cron'); ?>" title="Cron Manager"><i class="fas fa-clock"></i> <?= __("Cron Manager"); ?></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="<?php echo site_url('debug'); ?>" title="Debug Information"><i class="fas fa-tools"></i> <?= __("Debug Information"); ?></a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="<?php echo site_url('map/qso_map'); ?>" title="GeoJSON Map Plots"><i class="fas fa-globe-europe"></i> <?= __("GeoJSON Map Plots"); ?></a>
</div>
</li>
<?php } ?>

View File

@@ -0,0 +1,461 @@
<div class="container">
<h2><?= ('GeoJSON QSO Map'); ?></h2>
<div class="row mb-3 align-items-end">
<div class="col-auto">
<label for="countrySelect" class="form-label">Select Country:</label>
<select class="form-select" id="countrySelect" style="min-width: 200px;">
<option value="">Choose a country...</option>
<?php foreach ($countries as $country): ?>
<option value="<?php echo htmlspecialchars($country['COL_COUNTRY']); ?>"
data-dxcc="<?php echo htmlspecialchars($country['COL_DXCC']); ?>">
<?php echo htmlspecialchars($country['COL_COUNTRY'] . ' (' . $country['qso_count'] . ' QSOs)'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<label for="locationSelect" class="form-label">Location:</label>
<select class="form-select" id="locationSelect" style="min-width: 200px;">
<option value="all">All</option>
<?php foreach ($station_profiles as $profile): ?>
<option value="<?php echo htmlspecialchars($profile['station_id']); ?>">
<?php echo htmlspecialchars($profile['station_profile_name']); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<button id="loadMapBtn" class="btn btn-primary" disabled>Load Map</button>
</div>
<div class="col-auto">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showOnlyOutside" disabled>
<label class="form-check-label" for="showOnlyOutside">
Show only QSOs outside boundaries
</label>
</div>
</div>
<div class="col-auto d-flex align-items-center">
<div id="loadingSpinner" class="spinner-border text-primary d-none" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div id="loadingText" class="ms-2 text-muted d-none"></div>
</div>
</div>
<div id="mapContainer" class="mt-3" style="display: none;">
<div id="mapgeojson" style="border: 1px solid #ccc;"></div>
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-info-circle"></i>
Map shows QSOs with 6+ character gridsquares.
</small>
</div>
</div>
<div id="noDataMessage" class="alert alert-warning mt-3" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
No QSOs with 6+ character grids found for the selected country.
</div>
</div>
<style>
#mapgeojson {
border-radius: 4px;
height: 1000px !important;
width: 100% !important;
min-height: 600px;
}
.leaflet-popup-content {
min-width: 200px;
}
.marker-cluster {
background-color: rgba(110, 204, 57, 0.6);
}
.leaflet-marker-qso {
background-color: #3388ff;
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.leaflet-container {
height: 600px !important;
width: 100% !important;
}
.custom-div-icon {
background: transparent;
border: none;
}
.custom-div-icon i {
color: red;
}
.legend {
background: rgba(255, 255, 255, 0.95);
padding: 12px;
border-radius: 6px;
box-shadow: 0 3px 8px rgba(0,0,0,0.4);
line-height: 1.6;
border: 1px solid #ccc;
min-width: 200px;
}
.legend h4 {
margin: 0 0 10px 0;
font-size: 15px;
font-weight: bold;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}
.legend-item {
display: flex;
align-items: center;
margin: 8px 0;
}
.legend-icon {
margin-right: 10px;
flex-shrink: 0;
}
</style>
<script>
// Pass supported DXCC list from PHP to JavaScript
const supportedDxccs = <?php echo json_encode(array_keys($supported_dxccs)); ?>;
// Wait for jQuery to be loaded
function initMap() {
let map = null;
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: '<?php echo site_url("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.');
});
});
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);
}
// 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: '<div style="background-color: #ff0000; color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.5);">✕</div>',
iconSize: [24, 24],
className: 'custom-div-icon'
});
outsideCount++;
} else {
// Create green checkmark icon for QSOs inside GeoJSON
icon = L.divIcon({
html: '<div style="background-color: #28a745; color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.5);">✓</div>',
iconSize: [24, 24],
className: 'custom-div-icon'
});
insideCount++;
}
marker = L.marker([qso.lat, qso.lng], { icon: icon })
.bindPopup(qso.popup +
(qso.inside_geojson === false ? '<br><span style="color: red;"><strong>⚠ Outside country boundaries</strong></span>' :
'<br><span style="color: green;"><strong>✓ Inside country boundaries</strong></span>'))
.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: '<?php echo site_url("map/get_country_geojson"); ?>/' + dxcc,
method: 'GET',
success: function(geojson) {
if (geojson && !geojson.error) {
const layer = L.geoJSON(geojson, {
style: {
color: '#ff0000',
weight: 2,
opacity: 0.5,
fillOpacity: 0.1
}
}).addTo(map);
geojsonLayers.push(layer);
// Fit map to show both GeoJSON and markers
setTimeout(function() {
const geoBounds = layer.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();
// Add legend to the map only once
if (!legendAdded) {
addLegend(insideCount, outsideCount, qsos.length, showOnlyOutside);
legendAdded = true;
} else {
// Update existing legend counts
updateLegend(insideCount, outsideCount, qsos.length, showOnlyOutside);
}
// 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 addLegend(insideCount, outsideCount, totalCount, showOnlyOutside) {
const legend = L.control({ position: 'topright' });
legend.onAdd = function(map) {
const div = L.DomUtil.create('div', 'legend');
let html = '<h4>Legend</h4>';
// Inside boundaries
html += '<div class="legend-item">';
html += '<div class="legend-icon">';
html += '<div style="background-color: #28a745; color: white; width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3);">✓</div>';
html += '</div>';
html += '<span>Inside boundaries <strong>(' + insideCount + ')</strong></span>';
html += '</div>';
// Outside boundaries
html += '<div class="legend-item">';
html += '<div class="legend-icon">';
html += '<div style="background-color: #ff0000; color: white; width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; border: 2px solid white; box-shadow: 0 1px 3px rgba(0,0,0,0.3);">✕</div>';
html += '</div>';
html += '<span>Outside boundaries <strong>(' + outsideCount + ')</strong></span>';
html += '</div>';
// GeoJSON boundaries
html += '<div class="legend-item">';
html += '<div class="legend-icon">';
html += '<svg width="20" height="3"><line x1="0" y1="1.5" x2="20" y2="1.5" stroke="#ff0000" stroke-width="2"/></svg>';
html += '</div>';
html += '<span>Country/State boundaries</span>';
html += '</div>';
// Total QSOs (shown differently when filtering)
if (showOnlyOutside) {
html += '<div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 12px;">';
html += '<em>Showing ' + outsideCount + ' of ' + totalCount + ' total QSOs</em>';
html += '</div>';
} else {
html += '<div style="margin-top: 10px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 12px;">';
html += '<em>Total: ' + totalCount + ' QSOs with 6+ char grids</em>';
html += '</div>';
}
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();
});
}
</script>