mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
[QSO Map] Tweaks, features, refactor
This commit is contained in:
@@ -24,30 +24,35 @@ class Map extends CI_Controller {
|
|||||||
public function qso_map() {
|
public function qso_map() {
|
||||||
$this->load->library('Geojson');
|
$this->load->library('Geojson');
|
||||||
$this->load->model('Map_model');
|
$this->load->model('Map_model');
|
||||||
|
$this->load->model('stations');
|
||||||
|
|
||||||
// Get supported DXCC countries with state data
|
// Get supported DXCC countries with state data
|
||||||
$supported_dxccs = $this->geojson->getSupportedDxccs();
|
$data['supported_dxccs'] = $this->geojson->getSupportedDxccs();
|
||||||
|
|
||||||
|
$supported_country_codes = array_keys($data['supported_dxccs']);
|
||||||
|
|
||||||
// Fetch available countries from the logbook
|
// Fetch available countries from the logbook
|
||||||
$countries = $this->Map_model->get_available_countries();
|
$data['countries'] = $this->Map_model->get_available_countries($supported_country_codes);
|
||||||
|
|
||||||
// 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
|
// Fetch station profiles
|
||||||
$station_profiles = $this->Map_model->get_station_profiles();
|
$data['station_profiles'] = $this->stations->all_of_user()->result();
|
||||||
|
|
||||||
|
$data['homegrid'] = explode(',', $this->stations->find_gridsquare());
|
||||||
|
|
||||||
$data['countries'] = $filtered_countries;
|
|
||||||
$data['station_profiles'] = $station_profiles;
|
|
||||||
$data['supported_dxccs'] = $supported_dxccs;
|
|
||||||
$data['page_title'] = __("QSO Map");
|
$data['page_title'] = __("QSO Map");
|
||||||
|
|
||||||
|
$footerData = [];
|
||||||
|
$footerData['scripts'] = [
|
||||||
|
'assets/js/leaflet/geocoding.js',
|
||||||
|
'assets/js/leaflet/L.Maidenhead.js',
|
||||||
|
'assets/js/sections/qso_map.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/qso_map.js")),
|
||||||
|
'assets/js/sections/itumap_geojson.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/itumap_geojson.js")),
|
||||||
|
'assets/js/sections/cqmap_geojson.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/cqmap_geojson.js")),
|
||||||
|
];
|
||||||
|
|
||||||
$this->load->view('interface_assets/header', $data);
|
$this->load->view('interface_assets/header', $data);
|
||||||
$this->load->view('map/qso_map', $data);
|
$this->load->view('map/qso_map');
|
||||||
$this->load->view('interface_assets/footer');
|
$this->load->view('interface_assets/footer', $footerData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,32 +5,18 @@ class Map_model extends CI_Model {
|
|||||||
/**
|
/**
|
||||||
* Get available countries from the logbook with QSOs
|
* Get available countries from the logbook with QSOs
|
||||||
*/
|
*/
|
||||||
public function get_available_countries() {
|
public function get_available_countries($supported_country_codes) {
|
||||||
$this->db->select('DISTINCT dxcc_entities.name AS COL_COUNTRY, COL_DXCC, COUNT(*) as qso_count', FALSE);
|
$sql = "select DISTINCT dxcc_entities.name AS dxcc_name, dxcc_entities.prefix, COL_DXCC, COUNT(*) as qso_count
|
||||||
$this->db->from($this->config->item('table_name'));
|
from " . $this->config->item('table_name') . " thcv
|
||||||
$this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
|
join station_profile ON station_profile.station_id = thcv.station_id
|
||||||
$this->db->join('dxcc_entities', 'dxcc_entities.adif = ' . $this->config->item('table_name') . '.COL_DXCC');
|
join dxcc_entities ON dxcc_entities.adif = thcv.COL_DXCC
|
||||||
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
|
where station_profile.user_id = ?
|
||||||
$this->db->where("LENGTH(COL_GRIDSQUARE) >=", 6); // At least 6 chars
|
and thcv.COL_DXCC IN (" . implode(',', array_fill(0, count($supported_country_codes), '?')) . ")
|
||||||
$this->db->group_by('COL_COUNTRY, COL_DXCC');
|
and LENGTH(thcv.COL_GRIDSQUARE) >= 6
|
||||||
$this->db->order_by('COL_COUNTRY');
|
group by dxcc_name, thcv.COL_DXCC, dxcc_entities.prefix
|
||||||
|
order by prefix ASC";
|
||||||
|
|
||||||
$query = $this->db->get();
|
$query = $this->db->query($sql, array_merge([$this->session->userdata('user_id')], $supported_country_codes));
|
||||||
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();
|
return $query->result_array();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,21 +31,25 @@ class Map_model extends CI_Model {
|
|||||||
$this->load->library('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);
|
$sql = "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
|
||||||
$this->db->from($this->config->item('table_name'));
|
from " . $this->config->item('table_name') . "
|
||||||
$this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
|
join station_profile ON station_profile.station_id = " . $this->config->item('table_name') . ".station_id
|
||||||
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
|
where station_profile.user_id = ?
|
||||||
$this->db->where('COL_COUNTRY', $country);
|
and COL_COUNTRY = ?";
|
||||||
|
|
||||||
|
$bindings[] = $this->session->userdata('user_id');
|
||||||
|
$bindings[] = $country;
|
||||||
|
|
||||||
// Add station filter if specified
|
// Add station filter if specified
|
||||||
if ($station_id !== null && $station_id !== '') {
|
if ($station_id !== null && $station_id !== '') {
|
||||||
$this->db->where('station_profile.station_id', $station_id);
|
$sql .= " and station_profile.station_id = ?";
|
||||||
|
$bindings[] = $station_id;
|
||||||
}
|
}
|
||||||
$this->db->where("LENGTH(COL_GRIDSQUARE) >=", 6); // At least 6 chars
|
|
||||||
|
|
||||||
$this->db->order_by('COL_TIME_ON', 'DESC');
|
$sql .= "and LENGTH(COL_GRIDSQUARE) >= 6
|
||||||
|
order by COL_TIME_ON DESC";
|
||||||
|
|
||||||
$query = $this->db->get();
|
$query = $this->db->query($sql, $bindings);
|
||||||
$qsos = $query->result_array();
|
$qsos = $query->result_array();
|
||||||
|
|
||||||
// Process QSOs and convert gridsquares to coordinates
|
// Process QSOs and convert gridsquares to coordinates
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<script>
|
||||||
|
// Pass supported DXCC list from PHP to JavaScript
|
||||||
|
const supportedDxccs = <?php echo json_encode(array_keys($supported_dxccs)); ?>;
|
||||||
|
const homegrid = "<?php echo strtoupper($homegrid[0]); ?>";
|
||||||
|
let lang_gen_hamradio_cq_zones = '<?= _pgettext("Map Options", "CQ Zones"); ?>';
|
||||||
|
let lang_gen_hamradio_itu_zones = '<?= _pgettext("Map Options", "ITU Zones"); ?>';
|
||||||
|
let lang_gen_hamradio_gridsquares = '<?= _pgettext("Map Options", "Gridsquares"); ?>';
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2><?= ('GeoJSON QSO Map'); ?></h2>
|
<h2><?= ('GeoJSON QSO Map'); ?></h2>
|
||||||
|
|
||||||
@@ -7,9 +16,9 @@
|
|||||||
<select class="form-select" id="countrySelect" style="min-width: 200px;">
|
<select class="form-select" id="countrySelect" style="min-width: 200px;">
|
||||||
<option value=""><?= __("Choose a country...") ?></option>
|
<option value=""><?= __("Choose a country...") ?></option>
|
||||||
<?php foreach ($countries as $country): ?>
|
<?php foreach ($countries as $country): ?>
|
||||||
<option value="<?php echo htmlspecialchars(ucwords(strtolower(($country['COL_COUNTRY'])), "- (/")); ?>"
|
<option value="<?php echo htmlspecialchars(ucwords(strtolower(($country['dxcc_name'])), "- (/")); ?>"
|
||||||
data-dxcc="<?php echo htmlspecialchars($country['COL_DXCC']); ?>">
|
data-dxcc="<?php echo htmlspecialchars($country['COL_DXCC']); ?>">
|
||||||
<?php echo htmlspecialchars(ucwords(strtolower(($country['COL_COUNTRY'])), "- (/") . ' (' . $country['qso_count'] . ' ' . __("QSOs") . ')'); ?>
|
<?php echo htmlspecialchars($country['prefix']) . ' - ' . htmlspecialchars(ucwords(strtolower(($country['dxcc_name'])), "- (/") . ' (' . $country['qso_count'] . ' ' . __("QSOs") . ')'); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
@@ -19,8 +28,8 @@
|
|||||||
<select class="form-select" id="locationSelect" style="min-width: 200px;">
|
<select class="form-select" id="locationSelect" style="min-width: 200px;">
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
<?php foreach ($station_profiles as $profile): ?>
|
<?php foreach ($station_profiles as $profile): ?>
|
||||||
<option value="<?php echo htmlspecialchars($profile['station_id']); ?>">
|
<option value="<?php echo htmlspecialchars($profile->station_id); ?>">
|
||||||
<?php echo htmlspecialchars($profile['station_profile_name']); ?>
|
<?php echo htmlspecialchars($profile->station_profile_name); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</select>
|
</select>
|
||||||
@@ -46,14 +55,32 @@
|
|||||||
|
|
||||||
<div id="mapContainer" class="mt-3" style="display: none;">
|
<div id="mapContainer" class="mt-3" style="display: none;">
|
||||||
<div id="mapgeojson" style="border: 1px solid #ccc;"></div>
|
<div id="mapgeojson" style="border: 1px solid #ccc;"></div>
|
||||||
<div class="mt-2">
|
<div class="coordinates d-flex">
|
||||||
<small class="text-muted">
|
<div class="cohidden"><?= __("Latitude") ?>: </div>
|
||||||
<i class="fas fa-info-circle"></i>
|
<div class="cohidden col-auto text-success fw-bold" id="latDeg"></div>
|
||||||
<?= ('Map shows QSOs with 6+ character gridsquares.') ?>
|
<div class="cohidden"><?= __("Longitude") ?>: </div>
|
||||||
</small>
|
<div class="cohidden col-auto text-success fw-bold" id="lngDeg"></div>
|
||||||
</div>
|
<div class="cohidden"><?= __("Gridsquare") ?>: </div>
|
||||||
|
<div class="cohidden col-auto text-success fw-bold" id="locator"></div>
|
||||||
|
<div class="cohidden"><?= __("Distance") ?>: </div>
|
||||||
|
<div class="cohidden col-auto text-success fw-bold" id="distance"></div>
|
||||||
|
<div class="cohidden"><?= __("Bearing") ?>: </div>
|
||||||
|
<div class="cohidden col-auto text-success fw-bold" id="bearing"></div>
|
||||||
|
<div class="cohidden"><?= __("CQ Zone") ?>: </div>
|
||||||
|
<div class="cohidden col-auto text-success fw-bold" id="cqzonedisplay"></div>
|
||||||
|
<div class="cohidden"><?= __("ITU Zone") ?>: </div>
|
||||||
|
<div class="cohidden col-auto text-success fw-bold" id="ituzonedisplay"></div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="noDataMessage" class="alert alert-warning mt-3" style="display: none;">
|
<div id="noDataMessage" class="alert alert-warning mt-3" style="display: none;">
|
||||||
<i class="fas fa-exclamation-triangle"></i>
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
<?= ('No QSOs with 6+ character grids found for the selected country.') ?>
|
<?= ('No QSOs with 6+ character grids found for the selected country.') ?>
|
||||||
@@ -63,9 +90,9 @@
|
|||||||
<style>
|
<style>
|
||||||
#mapgeojson {
|
#mapgeojson {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 1000px !important;
|
height: calc(100vh - 250px);
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-height: 600px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
.leaflet-popup-content {
|
.leaflet-popup-content {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
@@ -79,10 +106,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
.leaflet-container {
|
|
||||||
height: 600px !important;
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
.custom-div-icon {
|
.custom-div-icon {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -116,347 +139,3 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</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: base_url + "index.php/map/get_country_geojson/",
|
|
||||||
type: 'post',
|
|
||||||
data: { dxcc: dxcc },
|
|
||||||
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>
|
|
||||||
|
|||||||
527
assets/js/sections/qso_map.js
Normal file
527
assets/js/sections/qso_map.js
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
let maidenhead;
|
||||||
|
let zonemarkers = [];
|
||||||
|
let ituzonemarkers = [];
|
||||||
|
let map = null;
|
||||||
|
|
||||||
|
// 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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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: '<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: base_url + "index.php/map/get_country_geojson/",
|
||||||
|
type: 'post',
|
||||||
|
data: { dxcc: dxcc },
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '<br />';
|
||||||
|
html += '<h4>Toggle layers</h4>';
|
||||||
|
html += '<input type="checkbox" onclick="toggleGridsquares(this.checked)" ' + (typeof gridsquare_layer !== 'undefined' && gridsquare_layer ? 'checked' : '') + ' style="outline: none;"><span> ' + lang_gen_hamradio_gridsquares + '</span><br>';
|
||||||
|
html += '<input type="checkbox" onclick="toggleCqZones(this.checked)" ' + (typeof cqzones_layer !== 'undefined' && cqzones_layer ? 'checked' : '') + ' style="outline: none;"><span> ' + lang_gen_hamradio_cq_zones + '</span><br>';
|
||||||
|
html += '<input type="checkbox" onclick="toggleItuZones(this.checked)" ' + (typeof ituzones_layer !== 'undefined' && ituzones_layer ? 'checked' : '') + ' style="outline: none;"><span> ' + lang_gen_hamradio_itu_zones + '</span><br>';
|
||||||
|
|
||||||
|
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 = '<span class="grid-text" style="cursor: default"><font style="color: \'white\'; font-size: 1.5em; font-weight: 900;">' + (Number(i)+Number(1)) + '</font></span>';
|
||||||
|
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 = '<span class="grid-text" style="cursor: default"><font style="color: \'white\'; font-size: 1.5em; font-weight: 900;">' + (Number(i)+Number(1)) + '</font></span>';
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user