mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
[Map] Added map for plotting QSOs in geojson
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
183
application/models/Map_model.php
Normal file
183
application/models/Map_model.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 } ?>
|
||||
|
||||
461
application/views/map/qso_map.php
Normal file
461
application/views/map/qso_map.php
Normal 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>
|
||||
Reference in New Issue
Block a user