mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-27 01:24:16 +00:00
Merge pull request #2551 from szporwolik/dev_cluster_logic_in_api
[DX Cluster] Final fine-tuning and bugfixing
This commit is contained in:
@@ -798,20 +798,40 @@ $config['max_login_attempts'] = 3;
|
||||
|
||||
$config['enable_dcl_interface'] = true;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DXCluster File Cache
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Controls whether DXCluster data is cached to files on the server.
|
||||
|
|
||||
| Set to TRUE to enable file caching (may cause high disk usage on large installations)
|
||||
| Set to FALSE to disable file caching (recommended for most installations)
|
||||
|
|
||||
| Default: false (file caching disabled)
|
||||
|
|
||||
| Warning: This is experimental and may not work as expected in all environments.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DXCluster File Cache
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Controls file-based caching for DXCluster features. Two independent settings:
|
||||
|
|
||||
| 1. enable_dxcluster_file_cache_band
|
||||
| - Caches processed spot lists (WITHOUT worked status) from DXCache API
|
||||
| - Cache is INSTANCE-WIDE (shared by all users)
|
||||
| - Cache duration: 59 seconds
|
||||
| - Cache key format: dxcluster_raw_{maxage}_{continent}_{mode}_{band}
|
||||
| - Example: dxcluster_raw_60_Any_All_20m
|
||||
| - Contains: callsign, frequency, DXCC, mode, age, metadata
|
||||
| - Does NOT contain: worked/confirmed status (always false in cache)
|
||||
| - Set to TRUE to reduce API calls and speed up spot list loading
|
||||
| - Set to FALSE to always fetch fresh data from API
|
||||
|
|
||||
| 2. enable_dxcluster_file_cache_worked
|
||||
| - Caches worked/confirmed status lookups from database
|
||||
| - Cache is USER-SPECIFIC (separate for each user/logbook)
|
||||
| - Cache duration: 15 minutes (900 seconds)
|
||||
| - Cache includes: All bands/modes combinations per callsign/DXCC/continent
|
||||
| - Set to TRUE to significantly reduce database load
|
||||
| - Set to FALSE to always query database for fresh status
|
||||
|
|
||||
| Default: false (both caching disabled)
|
||||
|
|
||||
| Recommendation:
|
||||
| - Enable BAND cache on all installations to reduce API load (instance-wide)
|
||||
| - Enable WORKED cache on high-traffic multi-user installations to reduce DB queries
|
||||
| Warning: WORKED cache may cause high disk usage on large multi-user installations.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$config['enable_dxcluster_file_cache'] = false;
|
||||
$config['enable_dxcluster_file_cache_band'] = false;
|
||||
$config['enable_dxcluster_file_cache_worked'] = false;
|
||||
|
||||
@@ -41,7 +41,12 @@ class Bandmap extends CI_Controller {
|
||||
$footerData = [];
|
||||
$footerData['scripts'] = [
|
||||
'assets/js/moment.min.js',
|
||||
'assets/js/datetime-moment.js',
|
||||
'assets/js/datetime-moment.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/datetime-moment.js")),
|
||||
'assets/js/cat.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/cat.js")),
|
||||
'assets/js/leaflet/leaflet.geodesic.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/leaflet.geodesic.js")),
|
||||
'assets/js/leaflet.polylineDecorator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet.polylineDecorator.js")),
|
||||
'assets/js/leaflet/L.Terminator.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/leaflet/L.Terminator.js")),
|
||||
'assets/js/sections/bandmap_list.js?' . filemtime(realpath(__DIR__ . "/../../assets/js/sections/bandmap_list.js")),
|
||||
];
|
||||
|
||||
// Get Date format
|
||||
|
||||
@@ -48,23 +48,14 @@ class Dxcluster_model extends CI_Model {
|
||||
$this->load->helper(array('psr4_autoloader'));
|
||||
|
||||
// Check if file caching is enabled in config
|
||||
$cache_enabled = $this->config->item('enable_dxcluster_file_cache') === true;
|
||||
$cache_band_enabled = $this->config->item('enable_dxcluster_file_cache_band') === true;
|
||||
$cache_worked_enabled = $this->config->item('enable_dxcluster_file_cache_worked') === true;
|
||||
|
||||
// Only load cache driver if caching is enabled
|
||||
if ($cache_enabled) {
|
||||
if ($cache_band_enabled || $cache_worked_enabled) {
|
||||
$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'file'));
|
||||
}
|
||||
|
||||
// Check cache first for processed spot list (only if caching is enabled)
|
||||
$user_id = $this->session->userdata('user_id');
|
||||
$logbook_id = $this->session->userdata('active_station_logbook');
|
||||
$cache_key = "spotlist_{$band}_{$maxage}_{$de}_{$mode}_{$user_id}_{$logbook_id}";
|
||||
|
||||
// Try to get cached processed results (59 second cache) only if caching is enabled
|
||||
if ($cache_enabled && ($cached_spots = $this->cache->get($cache_key))) {
|
||||
return $cached_spots;
|
||||
}
|
||||
|
||||
if($this->session->userdata('user_date_format')) {
|
||||
$custom_date_format = $this->session->userdata('user_date_format');
|
||||
} else {
|
||||
@@ -82,14 +73,17 @@ class Dxcluster_model extends CI_Model {
|
||||
$this->load->model('logbook_model');
|
||||
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
||||
|
||||
// Check cache for raw DX cluster data (only if caching is enabled)
|
||||
$jsonraw = null;
|
||||
if ($cache_enabled) {
|
||||
$jsonraw = $this->cache->get('dxcache'.$band);
|
||||
// Cache key for RAW cluster response (instance-wide, no worked status)
|
||||
$raw_cache_key = "dxcluster_raw_{$maxage}_{$de}_{$mode}_{$band}";
|
||||
|
||||
// Check cache for raw processed spots (without worked status)
|
||||
$spotsout = null;
|
||||
if ($cache_band_enabled) {
|
||||
$spotsout = $this->cache->get($raw_cache_key);
|
||||
}
|
||||
|
||||
if (!$jsonraw) {
|
||||
// CURL Functions
|
||||
if (!$spotsout) {
|
||||
// Fetch raw DX cluster data from API
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $dxcache_url);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog '.$this->optionslib->get_option('version').' DXLookup');
|
||||
@@ -106,57 +100,65 @@ class Dxcluster_model extends CI_Model {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Save to cache only if caching is enabled
|
||||
if ($cache_enabled) {
|
||||
$this->cache->save('dxcache'.$band, $jsonraw, 59); // Cache DXClusterCache Instancewide for 59seconds
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JSON before decoding
|
||||
if (empty($jsonraw) || strlen($jsonraw) <= 20) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = json_decode($jsonraw);
|
||||
|
||||
// Check for JSON decode errors
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($json)) {
|
||||
log_message('error', 'DXCluster: Invalid JSON received: ' . json_last_error_msg());
|
||||
return [];
|
||||
}
|
||||
$date = date('Ymd', time());
|
||||
|
||||
$dxccObj = new DXCC($date);
|
||||
|
||||
// DXCC lookup cache to avoid duplicate lookups
|
||||
$dxcc_cache = [];
|
||||
|
||||
$spotsout=[];
|
||||
|
||||
// Cache current time outside loop (avoid creating DateTime on every iteration)
|
||||
$currentTimestamp = time();
|
||||
|
||||
// Normalize continent filter once
|
||||
$de_lower = strtolower($de);
|
||||
$filter_continent = ($de != '' && $de != 'Any');
|
||||
|
||||
foreach($json as $singlespot){
|
||||
// Early filtering - skip invalid spots immediately
|
||||
if (!is_object($singlespot) || !isset($singlespot->frequency) || !is_numeric($singlespot->frequency)) {
|
||||
continue;
|
||||
// Validate JSON before decoding
|
||||
if (empty($jsonraw) || strlen($jsonraw) <= 20) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Ensure frequency is always a number (not a string)
|
||||
$singlespot->frequency = floatval($singlespot->frequency);
|
||||
$json = json_decode($jsonraw);
|
||||
|
||||
$spotband = $this->frequency->GetBand($singlespot->frequency*1000); // Apply band filter early (before expensive operations)
|
||||
// Check for JSON decode errors
|
||||
if (json_last_error() !== JSON_ERROR_NONE || !is_array($json)) {
|
||||
log_message('error', 'DXCluster: Invalid JSON received: ' . json_last_error_msg());
|
||||
return [];
|
||||
}
|
||||
$date = date('Ymd', time());
|
||||
|
||||
$dxccObj = new DXCC($date);
|
||||
|
||||
// DXCC lookup cache to avoid duplicate lookups
|
||||
$dxcc_cache = [];
|
||||
|
||||
$spotsout=[];
|
||||
|
||||
// Cache current time outside loop (avoid creating DateTime on every iteration)
|
||||
$currentTimestamp = time();
|
||||
|
||||
// Normalize continent filter once
|
||||
$de_lower = strtolower($de);
|
||||
$filter_continent = ($de != '' && $de != 'Any');
|
||||
|
||||
foreach($json as $singlespot){
|
||||
// Early filtering - skip invalid spots immediately
|
||||
if (!is_object($singlespot) || !isset($singlespot->frequency) || !is_numeric($singlespot->frequency)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure frequency is always a number (not a string)
|
||||
$singlespot->frequency = floatval($singlespot->frequency);
|
||||
|
||||
$spotband = $this->frequency->GetBand($singlespot->frequency*1000); // Apply band filter early (before expensive operations)
|
||||
if (($band != 'All') && ($band != $spotband)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$singlespot->band = $spotband;
|
||||
$singlespot->mode = $this->get_mode($singlespot);
|
||||
$singlespot->submode = $this->get_submode($singlespot);
|
||||
|
||||
// Only determine mode if not provided by cluster
|
||||
if (!isset($singlespot->mode) || empty($singlespot->mode)) {
|
||||
$singlespot->mode = $this->get_mode($singlespot);
|
||||
} else {
|
||||
// Normalize cluster-provided mode to lowercase
|
||||
$singlespot->mode = strtolower($singlespot->mode);
|
||||
}
|
||||
|
||||
// Only determine submode if not provided by cluster
|
||||
if (!isset($singlespot->submode) || empty($singlespot->submode)) {
|
||||
$singlespot->submode = $this->get_submode($singlespot);
|
||||
} else {
|
||||
// Normalize cluster-provided submode to uppercase
|
||||
$singlespot->submode = strtoupper($singlespot->submode);
|
||||
}
|
||||
|
||||
// Apply mode filter early
|
||||
if (($mode != 'All') && !$this->modefilter($singlespot, $mode)) {
|
||||
@@ -219,12 +221,17 @@ class Dxcluster_model extends CI_Model {
|
||||
// Extract park references from message
|
||||
$singlespot = $this->enrich_spot_metadata($singlespot);
|
||||
|
||||
// Collect spots for batch processing
|
||||
$spotsout[] = $singlespot;
|
||||
// Collect spots for batch processing
|
||||
$spotsout[] = $singlespot;
|
||||
}
|
||||
|
||||
// Cache the RAW processed spots (WITHOUT worked status) - instance-wide
|
||||
if ($cache_band_enabled && !empty($spotsout)) {
|
||||
$this->cache->save($raw_cache_key, $spotsout, 59);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Batch process all spot statuses in a single optimized database query
|
||||
// NOW add worked status if enabled (user-specific)
|
||||
if (!empty($spotsout)) {
|
||||
$batch_statuses = $this->logbook_model->get_batch_spot_statuses(
|
||||
$spotsout,
|
||||
@@ -234,21 +241,20 @@ class Dxcluster_model extends CI_Model {
|
||||
);
|
||||
|
||||
// Collect callsigns that need last_worked info (only those that are worked)
|
||||
$worked_callsigns = [];
|
||||
$worked_spots = [];
|
||||
foreach ($spotsout as $spot) {
|
||||
$callsign = $spot->spotted;
|
||||
if (isset($batch_statuses[$callsign]) && $batch_statuses[$callsign]['worked_call']) {
|
||||
$worked_callsigns[] = $callsign;
|
||||
$worked_spots[] = $spot;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch fetch last_worked info for all worked callsigns
|
||||
// Batch fetch last_worked info for all worked spots (with their specific bands)
|
||||
$last_worked_batch = [];
|
||||
if (!empty($worked_callsigns)) {
|
||||
if (!empty($worked_spots)) {
|
||||
$last_worked_batch = $this->logbook_model->get_batch_last_worked(
|
||||
$worked_callsigns,
|
||||
$logbooks_locations_array,
|
||||
$band
|
||||
$worked_spots,
|
||||
$logbooks_locations_array
|
||||
);
|
||||
}
|
||||
|
||||
@@ -295,18 +301,31 @@ class Dxcluster_model extends CI_Model {
|
||||
|
||||
$spotsout[$index] = $spot;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the processed results for 59 seconds (matches DXCache server TTL) only if caching is enabled
|
||||
if ($cache_enabled && !empty($spotsout)) {
|
||||
$this->cache->save($cache_key, $spotsout, 59);
|
||||
} else {
|
||||
// No worked status check - set all to false
|
||||
foreach ($spotsout as $index => $spot) {
|
||||
$spot->worked_dxcc = false;
|
||||
$spot->worked_call = false;
|
||||
$spot->cnfmd_dxcc = false;
|
||||
$spot->cnfmd_call = false;
|
||||
$spot->cnfmd_continent = false;
|
||||
$spot->worked_continent = false;
|
||||
$spotsout[$index] = $spot;
|
||||
}
|
||||
}
|
||||
|
||||
return $spotsout;
|
||||
}
|
||||
|
||||
// Determine mode with priority: POTA/SOTA mode > message keywords > frequency-based
|
||||
} // Determine mode with priority: POTA/SOTA mode > message keywords > frequency-based
|
||||
function get_mode($spot) {
|
||||
// Priority 0: If spot already has a valid mode from cluster, use it
|
||||
if (isset($spot->mode) && !empty($spot->mode)) {
|
||||
$existingMode = strtolower($spot->mode);
|
||||
// Validate it's a known mode category
|
||||
if (in_array($existingMode, ['cw', 'phone', 'digi', 'ssb'])) {
|
||||
return $this->mapToModeCategory($existingMode);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: POTA/SOTA mode fields (if present) - check from both dxcc_spotted and direct properties
|
||||
$potaMode = $spot->pota_mode ?? $spot->dxcc_spotted->pota_mode ?? null;
|
||||
$sotaMode = $spot->sota_mode ?? $spot->dxcc_spotted->sota_mode ?? null;
|
||||
@@ -373,6 +392,11 @@ class Dxcluster_model extends CI_Model {
|
||||
|
||||
// Determine submode for more specific mode classification
|
||||
function get_submode($spot) {
|
||||
// Priority 0: If spot already has a valid submode from cluster, use it
|
||||
if (isset($spot->submode) && !empty($spot->submode)) {
|
||||
return strtoupper($spot->submode);
|
||||
}
|
||||
|
||||
$mode = strtolower($spot->mode ?? '');
|
||||
$frequency = floatval($spot->frequency);
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
class Logbook_model extends CI_Model {
|
||||
|
||||
private $station_result = [];
|
||||
private $spot_status_cache = []; // In-memory cache for DX cluster spot statuses
|
||||
|
||||
public function __construct() {
|
||||
$this->oop_populate_modes();
|
||||
$this->load->Model('Modes');
|
||||
@@ -2707,154 +2709,40 @@ class Logbook_model extends CI_Model {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Load cache driver for file caching
|
||||
$cache_enabled = $this->config->item('enable_dxcluster_file_cache_worked') === true;
|
||||
if ($cache_enabled && !isset($this->cache)) {
|
||||
$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'file'));
|
||||
}
|
||||
|
||||
// Cache TTL in seconds (15 minutes = 900 seconds)
|
||||
$cache_ttl = 900;
|
||||
|
||||
// Initialize in-memory cache if not already done
|
||||
if (!isset($this->spot_status_cache)) {
|
||||
$this->spot_status_cache = [];
|
||||
}
|
||||
|
||||
$user_default_confirmation = $this->session->userdata('user_default_confirmation');
|
||||
$qsl_where = $this->qsl_default_where($user_default_confirmation);
|
||||
|
||||
// Build band filter with binding
|
||||
$band_sql = '';
|
||||
$band_param = null;
|
||||
$band = ($band == 'All') ? null : $band;
|
||||
if ($band != null && $band != 'SAT') {
|
||||
$band_sql = " AND COL_BAND = ?";
|
||||
$band_param = $band;
|
||||
} else if ($band == 'SAT') {
|
||||
$band_sql = " AND COL_SAT_NAME != ''";
|
||||
}
|
||||
|
||||
// Build mode filter
|
||||
$mode_sql = '';
|
||||
if (isset($mode) && $mode != 'All') {
|
||||
$mode_sql = " AND COL_MODE IN " . $this->Modes->get_modes_from_qrgmode($mode, true);
|
||||
}
|
||||
|
||||
// Collect unique callsigns, dxccs, and continents
|
||||
// Collect unique callsigns, dxccs, continents (no need for band/mode maps)
|
||||
// Check cache for entire callsign (not per band/mode) for maximum reuse
|
||||
$callsigns = [];
|
||||
$dxccs = [];
|
||||
$continents = [];
|
||||
$statuses = [];
|
||||
|
||||
// Build cache key with user_id, logbook_ids, and confirmation preference
|
||||
$user_id = $this->session->userdata('user_id');
|
||||
$logbook_ids_str = implode('_', $logbooks_locations_array);
|
||||
$confirmation_hash = md5($user_default_confirmation); // Hash to keep key shorter
|
||||
$logbook_ids_key = "{$user_id}_{$logbook_ids_str}_{$confirmation_hash}";
|
||||
$spots_by_callsign = []; // Group spots by callsign for processing
|
||||
|
||||
foreach ($spots as $spot) {
|
||||
// Validate spot has required properties
|
||||
if (!isset($spot->spotted) || !isset($spot->dxcc_spotted->dxcc_id) || !isset($spot->dxcc_spotted->cont)) {
|
||||
continue;
|
||||
}
|
||||
$callsigns[$spot->spotted] = true;
|
||||
$dxccs[$spot->dxcc_spotted->dxcc_id] = true;
|
||||
$continents[$spot->dxcc_spotted->cont] = true;
|
||||
}
|
||||
|
||||
// If no valid spots, return empty
|
||||
if (empty($callsigns) && empty($dxccs) && empty($continents)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build placeholders for station IDs (fix SQL injection)
|
||||
$station_ids_placeholders = implode(',', array_fill(0, count($logbooks_locations_array), '?'));
|
||||
|
||||
// Build placeholders and bind parameters for IN clauses
|
||||
$callsigns_array = array_keys($callsigns);
|
||||
$dxccs_array = array_keys($dxccs);
|
||||
$continents_array = array_keys($continents);
|
||||
|
||||
$callsigns_placeholders = implode(',', array_fill(0, count($callsigns_array), '?'));
|
||||
$dxccs_placeholders = implode(',', array_fill(0, count($dxccs_array), '?'));
|
||||
$continents_placeholders = implode(',', array_fill(0, count($continents_array), '?'));
|
||||
|
||||
// Three separate queries for callsigns, DXCCs, and continents
|
||||
// This is necessary because the OR logic doesn't work correctly with GROUP BY
|
||||
|
||||
// Query 1: Check callsigns
|
||||
$sql_calls = "
|
||||
SELECT
|
||||
COL_CALL as callsign,
|
||||
MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as is_confirmed
|
||||
FROM {$this->config->item('table_name')}
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CALL IN ({$callsigns_placeholders})
|
||||
{$band_sql}
|
||||
{$mode_sql}
|
||||
GROUP BY COL_CALL
|
||||
";
|
||||
|
||||
$bind_params_calls = array_merge($logbooks_locations_array, $callsigns_array);
|
||||
if ($band_param !== null) {
|
||||
$bind_params_calls[] = $band_param;
|
||||
}
|
||||
$query = $this->db->query($sql_calls, $bind_params_calls);
|
||||
$results_calls = $query->result_array();
|
||||
|
||||
// Query 2: Check DXCCs
|
||||
$sql_dxccs = "
|
||||
SELECT
|
||||
COL_DXCC as dxcc,
|
||||
MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as is_confirmed
|
||||
FROM {$this->config->item('table_name')}
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_DXCC IN ({$dxccs_placeholders})
|
||||
{$band_sql}
|
||||
{$mode_sql}
|
||||
GROUP BY COL_DXCC
|
||||
";
|
||||
|
||||
$bind_params_dxccs = array_merge($logbooks_locations_array, $dxccs_array);
|
||||
if ($band_param !== null) {
|
||||
$bind_params_dxccs[] = $band_param;
|
||||
}
|
||||
$query = $this->db->query($sql_dxccs, $bind_params_dxccs);
|
||||
$results_dxccs = $query->result_array();
|
||||
|
||||
// Query 3: Check continents
|
||||
$sql_conts = "
|
||||
SELECT
|
||||
COL_CONT as continent,
|
||||
MAX(CASE WHEN ({$qsl_where}) THEN 1 ELSE 0 END) as is_confirmed
|
||||
FROM {$this->config->item('table_name')}
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CONT IN ({$continents_placeholders})
|
||||
{$band_sql}
|
||||
{$mode_sql}
|
||||
GROUP BY COL_CONT
|
||||
";
|
||||
|
||||
$bind_params_conts = array_merge($logbooks_locations_array, $continents_array);
|
||||
if ($band_param !== null) {
|
||||
$bind_params_conts[] = $band_param;
|
||||
}
|
||||
$query = $this->db->query($sql_conts, $bind_params_conts);
|
||||
$results_conts = $query->result_array();
|
||||
|
||||
// Build lookup maps
|
||||
$worked_calls = [];
|
||||
$cnfmd_calls = [];
|
||||
$worked_dxccs = [];
|
||||
$cnfmd_dxccs = [];
|
||||
$worked_conts = [];
|
||||
$cnfmd_conts = [];
|
||||
|
||||
foreach ($results_calls as $row) {
|
||||
$worked_calls[$row['callsign']] = true;
|
||||
if ($row['is_confirmed'] == 1) {
|
||||
$cnfmd_calls[$row['callsign']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($results_dxccs as $row) {
|
||||
$worked_dxccs[$row['dxcc']] = true;
|
||||
if ($row['is_confirmed'] == 1) {
|
||||
$cnfmd_dxccs[$row['dxcc']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($results_conts as $row) {
|
||||
$worked_conts[$row['continent']] = true;
|
||||
if ($row['is_confirmed'] == 1) {
|
||||
$cnfmd_conts[$row['continent']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Map results back to spots
|
||||
$statuses = [];
|
||||
foreach ($spots as $spot) {
|
||||
// Skip spots with missing data
|
||||
if (!isset($spot->spotted) || !isset($spot->dxcc_spotted->dxcc_id) || !isset($spot->dxcc_spotted->cont)) {
|
||||
if (!isset($spot->spotted) || !isset($spot->dxcc_spotted->dxcc_id) || !isset($spot->dxcc_spotted->cont) || !isset($spot->band) || !isset($spot->mode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2862,107 +2750,513 @@ class Logbook_model extends CI_Model {
|
||||
$dxcc = $spot->dxcc_spotted->dxcc_id;
|
||||
$cont = $spot->dxcc_spotted->cont;
|
||||
|
||||
$statuses[$callsign] = [
|
||||
'worked_call' => isset($worked_calls[$callsign]),
|
||||
'worked_dxcc' => isset($worked_dxccs[$dxcc]),
|
||||
'worked_continent' => isset($worked_conts[$cont]),
|
||||
'cnfmd_call' => isset($cnfmd_calls[$callsign]),
|
||||
'cnfmd_dxcc' => isset($cnfmd_dxccs[$dxcc]),
|
||||
'cnfmd_continent' => isset($cnfmd_conts[$cont])
|
||||
];
|
||||
// Collect unique callsigns/dxccs/continents - query once per unique value
|
||||
$callsigns[$callsign] = true;
|
||||
$dxccs[$dxcc] = true;
|
||||
$continents[$cont] = true;
|
||||
|
||||
// Group spots by callsign for later processing
|
||||
if (!isset($spots_by_callsign[$callsign])) {
|
||||
$spots_by_callsign[$callsign] = [];
|
||||
}
|
||||
$spots_by_callsign[$callsign][] = $spot;
|
||||
}
|
||||
|
||||
// Check cache for callsigns we've already queried (get ALL band/mode data once per callsign)
|
||||
// Priority: 1) In-memory cache, 2) File cache, 3) Database query
|
||||
$callsigns_to_query = [];
|
||||
$dxccs_to_query = [];
|
||||
$continents_to_query = [];
|
||||
|
||||
foreach (array_keys($callsigns) as $callsign) {
|
||||
$cache_key = "{$logbook_ids_key}|call|{$callsign}";
|
||||
|
||||
// Check in-memory cache first
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
// Check file cache
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
// Load from file cache into in-memory cache
|
||||
$this->spot_status_cache[$cache_key] = $cached_data;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Not in either cache, need to query
|
||||
$callsigns_to_query[$callsign] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($dxccs) as $dxcc) {
|
||||
$cache_key = "{$logbook_ids_key}|dxcc|{$dxcc}";
|
||||
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
$this->spot_status_cache[$cache_key] = $cached_data;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$dxccs_to_query[$dxcc] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_keys($continents) as $cont) {
|
||||
$cache_key = "{$logbook_ids_key}|cont|{$cont}";
|
||||
|
||||
if (!isset($this->spot_status_cache[$cache_key])) {
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$cached_data = $this->cache->get($file_cache_key);
|
||||
if ($cached_data !== false) {
|
||||
$this->spot_status_cache[$cache_key] = $cached_data;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$continents_to_query[$cont] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If everything is cached, skip queries and just map results
|
||||
if (empty($callsigns_to_query) && empty($dxccs_to_query) && empty($continents_to_query)) {
|
||||
// All data cached, map to spots
|
||||
foreach ($spots_by_callsign as $callsign => $callsign_spots) {
|
||||
foreach ($callsign_spots as $spot) {
|
||||
$statuses[$callsign] = $this->map_spot_status_from_cache($spot, $logbook_ids_key);
|
||||
}
|
||||
}
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
// Build placeholders for station IDs
|
||||
$station_ids_placeholders = implode(',', array_fill(0, count($logbooks_locations_array), '?'));
|
||||
|
||||
// Query only uncached items - get ALL band/mode combinations
|
||||
$callsigns_array = array_keys($callsigns_to_query);
|
||||
$dxccs_array = array_keys($dxccs_to_query);
|
||||
$continents_array = array_keys($continents_to_query);
|
||||
|
||||
// Split into two queries for performance: worked (faster) and confirmed (pre-filtered)
|
||||
$worked_queries = [];
|
||||
$confirmed_queries = [];
|
||||
$worked_bind_params = [];
|
||||
$confirmed_bind_params = [];
|
||||
|
||||
if (!empty($callsigns_array)) {
|
||||
$callsigns_placeholders = implode(',', array_fill(0, count($callsigns_array), '?'));
|
||||
// Query 1: Get all worked combinations
|
||||
// Index: idx_HRD_COL_CALL_station_id (station_id, COL_CALL, COL_TIME_ON)
|
||||
$worked_queries[] = "
|
||||
SELECT 'call' as type, COL_CALL as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_CALL_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CALL IN ({$callsigns_placeholders})
|
||||
GROUP BY COL_CALL, COL_BAND, COL_MODE
|
||||
";
|
||||
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $callsigns_array);
|
||||
|
||||
// Query 2: Get only confirmed combinations (pre-filtered by QSL status)
|
||||
$confirmed_queries[] = "
|
||||
SELECT 'call' as type, COL_CALL as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_CALL_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CALL IN ({$callsigns_placeholders})
|
||||
AND ({$qsl_where})
|
||||
GROUP BY COL_CALL, COL_BAND, COL_MODE
|
||||
";
|
||||
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $callsigns_array);
|
||||
}
|
||||
|
||||
if (!empty($dxccs_array)) {
|
||||
$dxccs_placeholders = implode(',', array_fill(0, count($dxccs_array), '?'));
|
||||
// Index: idx_HRD_COL_DXCC_station_id (station_id, COL_DXCC, COL_TIME_ON)
|
||||
$worked_queries[] = "
|
||||
SELECT 'dxcc' as type, COL_DXCC as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_DXCC_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_DXCC IN ({$dxccs_placeholders})
|
||||
GROUP BY COL_DXCC, COL_BAND, COL_MODE
|
||||
";
|
||||
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $dxccs_array);
|
||||
|
||||
$confirmed_queries[] = "
|
||||
SELECT 'dxcc' as type, COL_DXCC as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_COL_DXCC_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_DXCC IN ({$dxccs_placeholders})
|
||||
AND ({$qsl_where})
|
||||
GROUP BY COL_DXCC, COL_BAND, COL_MODE
|
||||
";
|
||||
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $dxccs_array);
|
||||
}
|
||||
|
||||
if (!empty($continents_array)) {
|
||||
$continents_placeholders = implode(',', array_fill(0, count($continents_array), '?'));
|
||||
// No specific index for COL_CONT - let MySQL optimizer choose
|
||||
$worked_queries[] = "
|
||||
SELECT 'cont' as type, COL_CONT as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CONT IN ({$continents_placeholders})
|
||||
GROUP BY COL_CONT, COL_BAND, COL_MODE
|
||||
";
|
||||
$worked_bind_params = array_merge($worked_bind_params, $logbooks_locations_array, $continents_array);
|
||||
|
||||
$confirmed_queries[] = "
|
||||
SELECT 'cont' as type, COL_CONT as identifier, COL_BAND as band, COL_MODE as mode
|
||||
FROM {$this->config->item('table_name')} FORCE INDEX (idx_HRD_station_id)
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CONT IN ({$continents_placeholders})
|
||||
AND ({$qsl_where})
|
||||
GROUP BY COL_CONT, COL_BAND, COL_MODE
|
||||
";
|
||||
$confirmed_bind_params = array_merge($confirmed_bind_params, $logbooks_locations_array, $continents_array);
|
||||
}
|
||||
|
||||
if (empty($worked_queries)) {
|
||||
// Nothing to query, use cached data
|
||||
foreach ($spots_by_callsign as $callsign => $callsign_spots) {
|
||||
foreach ($callsign_spots as $spot) {
|
||||
$statuses[$callsign] = $this->map_spot_status_from_cache($spot, $logbook_ids_key);
|
||||
}
|
||||
}
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
// Execute worked query (faster - no QSL filter)
|
||||
$worked_sql = implode(' UNION ALL ', $worked_queries);
|
||||
$worked_query = $this->db->query($worked_sql, $worked_bind_params);
|
||||
$worked_results = $worked_query->result_array();
|
||||
|
||||
// Execute confirmed query (only scans confirmed QSOs)
|
||||
$confirmed_sql = implode(' UNION ALL ', $confirmed_queries);
|
||||
$confirmed_query = $this->db->query($confirmed_sql, $confirmed_bind_params);
|
||||
$confirmed_results = $confirmed_query->result_array();
|
||||
|
||||
// Build comprehensive cache structure: identifier => [band|mode => status]
|
||||
// This allows reusing data for ALL spots with same callsign/dxcc/continent
|
||||
$call_data = []; // callsign => [band|mode => ['worked' => bool, 'confirmed' => bool]]
|
||||
$dxcc_data = []; // dxcc => [band|mode => ['worked' => bool, 'confirmed' => bool]]
|
||||
$cont_data = []; // continent => [band|mode => ['worked' => bool, 'confirmed' => bool]]
|
||||
|
||||
// Process worked results first (mark as worked, not confirmed)
|
||||
foreach ($worked_results as $row) {
|
||||
$identifier = $row['identifier'];
|
||||
$band = $row['band'];
|
||||
$logbook_mode = $row['mode'];
|
||||
|
||||
// Convert logbook mode to spot mode category (phone/cw/digi)
|
||||
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
|
||||
$qrgmode_lower = strtolower($qrgmode ?? '');
|
||||
|
||||
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
|
||||
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
|
||||
$mode_category = $qrgmode_lower;
|
||||
if ($mode_category === 'data') {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
} else {
|
||||
// Fallback to hardcoded mapping
|
||||
$logbook_mode_upper = strtoupper($logbook_mode ?? '');
|
||||
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
|
||||
$mode_category = 'phone';
|
||||
} elseif (in_array($logbook_mode_upper, ['CW'])) {
|
||||
$mode_category = 'cw';
|
||||
} else {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
}
|
||||
|
||||
$band_mode_key = $band . '|' . $mode_category;
|
||||
|
||||
if ($row['type'] === 'call') {
|
||||
if (!isset($call_data[$identifier])) {
|
||||
$call_data[$identifier] = [];
|
||||
}
|
||||
$call_data[$identifier][$band_mode_key] = [
|
||||
'worked' => true,
|
||||
'confirmed' => false
|
||||
];
|
||||
} elseif ($row['type'] === 'dxcc') {
|
||||
if (!isset($dxcc_data[$identifier])) {
|
||||
$dxcc_data[$identifier] = [];
|
||||
}
|
||||
$dxcc_data[$identifier][$band_mode_key] = [
|
||||
'worked' => true,
|
||||
'confirmed' => false
|
||||
];
|
||||
} elseif ($row['type'] === 'cont') {
|
||||
if (!isset($cont_data[$identifier])) {
|
||||
$cont_data[$identifier] = [];
|
||||
}
|
||||
$cont_data[$identifier][$band_mode_key] = [
|
||||
'worked' => true,
|
||||
'confirmed' => false
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Now overlay confirmed results (update confirmed flag to true)
|
||||
foreach ($confirmed_results as $row) {
|
||||
$identifier = $row['identifier'];
|
||||
$band = $row['band'];
|
||||
$logbook_mode = $row['mode'];
|
||||
|
||||
// Convert logbook mode to spot mode category (phone/cw/digi)
|
||||
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
|
||||
$qrgmode_lower = strtolower($qrgmode ?? '');
|
||||
|
||||
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
|
||||
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
|
||||
$mode_category = $qrgmode_lower;
|
||||
if ($mode_category === 'data') {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
} else {
|
||||
// Fallback to hardcoded mapping
|
||||
$logbook_mode_upper = strtoupper($logbook_mode ?? '');
|
||||
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
|
||||
$mode_category = 'phone';
|
||||
} elseif (in_array($logbook_mode_upper, ['CW'])) {
|
||||
$mode_category = 'cw';
|
||||
} else {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
}
|
||||
|
||||
$band_mode_key = $band . '|' . $mode_category;
|
||||
|
||||
if ($row['type'] === 'call') {
|
||||
if (isset($call_data[$identifier][$band_mode_key])) {
|
||||
$call_data[$identifier][$band_mode_key]['confirmed'] = true;
|
||||
}
|
||||
} elseif ($row['type'] === 'dxcc') {
|
||||
if (isset($dxcc_data[$identifier][$band_mode_key])) {
|
||||
$dxcc_data[$identifier][$band_mode_key]['confirmed'] = true;
|
||||
}
|
||||
} elseif ($row['type'] === 'cont') {
|
||||
if (isset($cont_data[$identifier][$band_mode_key])) {
|
||||
$cont_data[$identifier][$band_mode_key]['confirmed'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the complete data for each callsign/dxcc/continent (both in-memory and file)
|
||||
// Store worked items with their band/mode data
|
||||
foreach ($call_data as $callsign => $data) {
|
||||
$cache_key = "{$logbook_ids_key}|call|{$callsign}";
|
||||
$this->spot_status_cache[$cache_key] = $data;
|
||||
|
||||
// Save to file cache for 15 minutes
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
}
|
||||
foreach ($dxcc_data as $dxcc => $data) {
|
||||
$cache_key = "{$logbook_ids_key}|dxcc|{$dxcc}";
|
||||
$this->spot_status_cache[$cache_key] = $data;
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
}
|
||||
foreach ($cont_data as $cont => $data) {
|
||||
$cache_key = "{$logbook_ids_key}|cont|{$cont}";
|
||||
$this->spot_status_cache[$cache_key] = $data;
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$this->cache->save($file_cache_key, $data, $cache_ttl);
|
||||
}
|
||||
} // Cache NOT WORKED items (negative results) - store empty arrays
|
||||
// This prevents redundant database queries for callsigns/dxccs/continents not in logbook
|
||||
foreach ($callsigns_array as $callsign) {
|
||||
if (!isset($call_data[$callsign])) {
|
||||
$cache_key = "{$logbook_ids_key}|call|{$callsign}";
|
||||
$this->spot_status_cache[$cache_key] = []; // Empty = not worked
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_call_{$logbook_ids_key}_{$callsign}";
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($dxccs_array as $dxcc) {
|
||||
if (!isset($dxcc_data[$dxcc])) {
|
||||
$cache_key = "{$logbook_ids_key}|dxcc|{$dxcc}";
|
||||
$this->spot_status_cache[$cache_key] = [];
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_dxcc_{$logbook_ids_key}_{$dxcc}";
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($continents_array as $cont) {
|
||||
if (!isset($cont_data[$cont])) {
|
||||
$cache_key = "{$logbook_ids_key}|cont|{$cont}";
|
||||
$this->spot_status_cache[$cache_key] = [];
|
||||
|
||||
if ($cache_enabled) {
|
||||
$file_cache_key = "dxcluster_worked_cont_{$logbook_ids_key}_{$cont}";
|
||||
$this->cache->save($file_cache_key, [], $cache_ttl);
|
||||
}
|
||||
}
|
||||
} // Now map all spots to their status using cached data (query results + previously cached)
|
||||
foreach ($spots_by_callsign as $callsign => $callsign_spots) {
|
||||
foreach ($callsign_spots as $spot) {
|
||||
$statuses[$callsign] = $this->map_spot_status_from_cache($spot, $logbook_ids_key);
|
||||
}
|
||||
}
|
||||
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch version - Get last worked info for multiple callsigns in a single query
|
||||
* Helper function to map spot status from cached data
|
||||
*/
|
||||
private function map_spot_status_from_cache($spot, $logbook_ids_key) {
|
||||
$callsign = $spot->spotted;
|
||||
$dxcc = $spot->dxcc_spotted->dxcc_id;
|
||||
$cont = $spot->dxcc_spotted->cont;
|
||||
$spot_band = ($spot->band == 'SAT') ? 'SAT' : $spot->band;
|
||||
$spot_mode = $spot->mode;
|
||||
|
||||
$band_mode_key = $spot_band . '|' . $spot_mode;
|
||||
|
||||
// Get cached data for this callsign/dxcc/continent
|
||||
$call_cache_key = "{$logbook_ids_key}|call|{$callsign}";
|
||||
$dxcc_cache_key = "{$logbook_ids_key}|dxcc|{$dxcc}";
|
||||
$cont_cache_key = "{$logbook_ids_key}|cont|{$cont}";
|
||||
|
||||
$call_data = $this->spot_status_cache[$call_cache_key] ?? [];
|
||||
$dxcc_data = $this->spot_status_cache[$dxcc_cache_key] ?? [];
|
||||
$cont_data = $this->spot_status_cache[$cont_cache_key] ?? [];
|
||||
|
||||
// Check if worked/confirmed on this specific band+mode
|
||||
$worked_call = isset($call_data[$band_mode_key]) && $call_data[$band_mode_key]['worked'];
|
||||
$cnfmd_call = isset($call_data[$band_mode_key]) && $call_data[$band_mode_key]['confirmed'];
|
||||
$worked_dxcc = isset($dxcc_data[$band_mode_key]) && $dxcc_data[$band_mode_key]['worked'];
|
||||
$cnfmd_dxcc = isset($dxcc_data[$band_mode_key]) && $dxcc_data[$band_mode_key]['confirmed'];
|
||||
$worked_cont = isset($cont_data[$band_mode_key]) && $cont_data[$band_mode_key]['worked'];
|
||||
$cnfmd_cont = isset($cont_data[$band_mode_key]) && $cont_data[$band_mode_key]['confirmed'];
|
||||
|
||||
return [
|
||||
'worked_call' => $worked_call,
|
||||
'worked_dxcc' => $worked_dxcc,
|
||||
'worked_continent' => $worked_cont,
|
||||
'cnfmd_call' => $cnfmd_call,
|
||||
'cnfmd_dxcc' => $cnfmd_dxcc,
|
||||
'cnfmd_continent' => $cnfmd_cont
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch version - Get last worked info for multiple callsigns with their specific bands and modes
|
||||
*
|
||||
* @param array $callsigns Array of callsigns to check
|
||||
* @param array $spots Array of spot objects (must have 'spotted', 'band', and 'mode' properties)
|
||||
* @param array $logbooks_locations_array Station IDs
|
||||
* @param string $band Band filter
|
||||
* @return array Keyed by callsign with last worked info
|
||||
*/
|
||||
function get_batch_last_worked($callsigns, $logbooks_locations_array, $band = null) {
|
||||
if (empty($callsigns) || empty($logbooks_locations_array)) {
|
||||
function get_batch_last_worked($spots, $logbooks_locations_array) {
|
||||
if (empty($spots) || empty($logbooks_locations_array)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build band filter with binding
|
||||
$band_sql = '';
|
||||
$band_param = null;
|
||||
$band = ($band == 'All') ? null : $band;
|
||||
if ($band != null && $band != 'SAT') {
|
||||
$band_sql = " AND COL_BAND = ?";
|
||||
$band_param = $band;
|
||||
} else if ($band == 'SAT') {
|
||||
$band_sql = " AND COL_SAT_NAME != ''";
|
||||
}
|
||||
|
||||
// Build placeholders for station IDs (fix SQL injection)
|
||||
// Build placeholders for station IDs
|
||||
$station_ids_placeholders = implode(',', array_fill(0, count($logbooks_locations_array), '?'));
|
||||
|
||||
// Build placeholders for callsigns IN clause
|
||||
$callsigns_placeholders = implode(',', array_fill(0, count($callsigns), '?'));
|
||||
// Collect unique callsigns and build callsign->band->mode mapping
|
||||
$callsigns = [];
|
||||
$callsign_band_mode_map = []; // callsign => [band|mode => true]
|
||||
|
||||
// Validate we have valid data
|
||||
if (empty($station_ids_placeholders) || empty($callsigns_placeholders)) {
|
||||
foreach ($spots as $spot) {
|
||||
if (!isset($spot->spotted) || !isset($spot->band) || !isset($spot->mode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$callsign = $spot->spotted;
|
||||
$spot_band = ($spot->band == 'SAT') ? 'SAT' : $spot->band;
|
||||
$spot_mode = $spot->mode;
|
||||
|
||||
$callsigns[$callsign] = true;
|
||||
if (!isset($callsign_band_mode_map[$callsign])) {
|
||||
$callsign_band_mode_map[$callsign] = [];
|
||||
}
|
||||
$band_mode_key = $spot_band . '|' . $spot_mode;
|
||||
$callsign_band_mode_map[$callsign][$band_mode_key] = true;
|
||||
}
|
||||
|
||||
if (empty($callsigns)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build bind params: station_ids, callsigns for subquery, band (if set), station_ids again, callsigns for main query, band again (if set)
|
||||
$bind_params = [];
|
||||
// Build IN clause for callsigns
|
||||
$callsigns_array = array_keys($callsigns);
|
||||
$callsigns_placeholders = implode(',', array_fill(0, count($callsigns_array), '?'));
|
||||
|
||||
// First set: station_ids for subquery
|
||||
$bind_params = array_merge($bind_params, $logbooks_locations_array);
|
||||
|
||||
// Second: callsigns for subquery
|
||||
$bind_params = array_merge($bind_params, $callsigns);
|
||||
|
||||
// Third: band for subquery (if set)
|
||||
if ($band_param !== null) {
|
||||
$bind_params[] = $band_param;
|
||||
}
|
||||
|
||||
// Fourth set: station_ids for main query
|
||||
$bind_params = array_merge($bind_params, $logbooks_locations_array);
|
||||
|
||||
// Fifth: callsigns for main query
|
||||
$bind_params = array_merge($bind_params, $callsigns);
|
||||
|
||||
// Sixth: band for main query (if set)
|
||||
if ($band_param !== null) {
|
||||
$bind_params[] = $band_param;
|
||||
}
|
||||
|
||||
// Query to get the most recent QSO for each callsign
|
||||
// Using a subquery to get max time per callsign, then join to get the full record
|
||||
// Added LIMIT 1 in derived table to handle edge case of multiple QSOs at same timestamp
|
||||
// Optimized query using window function (MySQL 8.0+)
|
||||
// Get the most recent QSO for each callsign/band/mode combination
|
||||
// Will use idx_station_call_time index if available, otherwise falls back to existing indexes
|
||||
$sql = "
|
||||
SELECT t1.COL_CALL, t1.COL_TIME_ON as LAST_QSO, t1.COL_MODE as LAST_MODE
|
||||
FROM {$this->config->item('table_name')} t1
|
||||
INNER JOIN (
|
||||
SELECT COL_CALL, MAX(COL_TIME_ON) as max_time
|
||||
SELECT COL_CALL, COL_TIME_ON as LAST_QSO, COL_MODE as LAST_MODE, COL_BAND
|
||||
FROM (
|
||||
SELECT COL_CALL, COL_TIME_ON, COL_MODE, COL_BAND,
|
||||
ROW_NUMBER() OVER (PARTITION BY COL_CALL, COL_BAND, COL_MODE ORDER BY COL_TIME_ON DESC) as rn
|
||||
FROM {$this->config->item('table_name')}
|
||||
WHERE station_id IN ({$station_ids_placeholders})
|
||||
AND COL_CALL IN ({$callsigns_placeholders})
|
||||
{$band_sql}
|
||||
GROUP BY COL_CALL
|
||||
) t2 ON t1.COL_CALL = t2.COL_CALL AND t1.COL_TIME_ON = t2.max_time
|
||||
WHERE t1.station_id IN ({$station_ids_placeholders})
|
||||
AND t1.COL_CALL IN ({$callsigns_placeholders})
|
||||
{$band_sql}
|
||||
) t
|
||||
WHERE rn = 1
|
||||
";
|
||||
|
||||
$bind_params = array_merge($logbooks_locations_array, $callsigns_array);
|
||||
|
||||
$query = $this->db->query($sql, $bind_params);
|
||||
$results = $query->result();
|
||||
|
||||
// Build lookup map keyed by callsign
|
||||
// If multiple QSOs have same timestamp, keep first one returned
|
||||
// Only include results where the band+mode matches what we're looking for
|
||||
$last_worked = [];
|
||||
foreach ($results as $row) {
|
||||
if (!isset($last_worked[$row->COL_CALL])) {
|
||||
$last_worked[$row->COL_CALL] = $row;
|
||||
$callsign = $row->COL_CALL;
|
||||
$band = $row->COL_BAND;
|
||||
$logbook_mode = $row->LAST_MODE;
|
||||
|
||||
// Convert logbook mode to spot mode category
|
||||
$qrgmode = @$this->Modes->get_qrgmode_from_mode($logbook_mode);
|
||||
$qrgmode_lower = strtolower($qrgmode ?? '');
|
||||
|
||||
// Check if qrgmode is valid (phone/cw/data/digi), otherwise use fallback
|
||||
if (!empty($qrgmode) && in_array($qrgmode_lower, ['phone', 'cw', 'data', 'digi'])) {
|
||||
$mode_category = $qrgmode_lower;
|
||||
if ($mode_category === 'data') {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
} else {
|
||||
// Fallback to hardcoded mapping
|
||||
$logbook_mode_upper = strtoupper($logbook_mode ?? '');
|
||||
if (in_array($logbook_mode_upper, ['SSB', 'FM', 'AM', 'PHONE'])) {
|
||||
$mode_category = 'phone';
|
||||
} elseif (in_array($logbook_mode_upper, ['CW'])) {
|
||||
$mode_category = 'cw';
|
||||
} else {
|
||||
$mode_category = 'digi';
|
||||
}
|
||||
}
|
||||
$band_mode_key = $band . '|' . $mode_category;
|
||||
|
||||
// Check if we have a spot for this callsign on this band+mode
|
||||
if (isset($callsign_band_mode_map[$callsign]) && isset($callsign_band_mode_map[$callsign][$band_mode_key])) {
|
||||
// Only store the first (most recent) match for each callsign
|
||||
if (!isset($last_worked[$callsign])) {
|
||||
$last_worked[$callsign] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo base_url(); ?>assets/css/bandmap_list.css" />
|
||||
|
||||
<div class="container" id="bandmapContainer">
|
||||
<div class="container-fluid" id="bandmapContainer">
|
||||
<!-- Messages -->
|
||||
<div class="messages my-1 mx-3"></div>
|
||||
|
||||
@@ -512,6 +512,7 @@
|
||||
<th title="<?= __("Band"); ?>"><i class="fas fa-wave-square"></i></th>
|
||||
<th title="<?= __("Frequency"); ?> [MHz]"><?= __("Freq"); ?></th>
|
||||
<th title="<?= __("Mode"); ?>"><i class="fas fa-broadcast-tower"></i></th>
|
||||
<th title="<?= __("Submode"); ?>"><i class="fas fa-broadcast-tower"></i></th>
|
||||
<th title="<?= __("Spotted Callsign"); ?>"><?= __("DX"); ?></th>
|
||||
<th title="<?= __("Continent"); ?>"><i class="fas fa-globe-americas"></i></th>
|
||||
<th title="<?= __("CQ Zone"); ?>"><i class="fas fa-map-marked"></i></th>
|
||||
@@ -521,9 +522,10 @@
|
||||
<th title="<?= __("Spotter Callsign"); ?>"><?= __("de"); ?></th>
|
||||
<th title="<?= __("Spotter Continent"); ?>"><i class="fas fa-globe-americas"></i></th>
|
||||
<th title="<?= __("Spotter CQ Zone"); ?>"><i class="fas fa-map-marked"></i></th>
|
||||
<th title="<?= __("Last QSO Date"); ?>"><i class="fas fa-history"></i></th>
|
||||
<th title="<?= __("Special Flags"); ?>"><?= __("Special"); ?></th>
|
||||
<th title="<?= __("Message"); ?>"><?= __("Message"); ?></th>
|
||||
</tr>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody class="spots_table_contents">
|
||||
|
||||
@@ -1445,15 +1445,6 @@ mymap.on('mousemove', onQsoMapMove);
|
||||
|
||||
<?php } ?>
|
||||
|
||||
<!--- Bandmap CAT Integration --->
|
||||
<?php if ($this->uri->segment(1) == "bandmap" && $this->uri->segment(2) == "list") { ?>
|
||||
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/cat.js?v=<?php echo time(); ?>"></script>
|
||||
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet/leaflet.geodesic.js?v=<?php echo time(); ?>"></script>
|
||||
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet.polylineDecorator.js?v=<?php echo time(); ?>"></script>
|
||||
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/leaflet/L.Terminator.js?v=<?php echo time(); ?>"></script>
|
||||
<script type="text/javascript" src="<?php echo base_url(); ?>assets/js/sections/bandmap_list.js?v=<?php echo time(); ?>"></script>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($this->uri->segment(1) == "logbook" && $this->uri->segment(2) == "view") { ?>
|
||||
<script>
|
||||
|
||||
|
||||
@@ -152,12 +152,12 @@ tbody a {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.spottable tbody tr td:nth-child(5) a {
|
||||
.spottable tbody tr td:nth-child(6) a {
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.spottable tbody tr td:nth-child(7) i:hover {
|
||||
.spottable tbody tr td:nth-child(8) i:hover {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
transition: all 0.2s ease;
|
||||
@@ -328,6 +328,20 @@ table tbody tr.cat-nearest-above td {
|
||||
|
||||
.spottable thead th {
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Limit container max-width for optimal viewing on 1920px monitors */
|
||||
#bandmapContainer {
|
||||
max-width: 1910px !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Reduce horizontal padding in table cells to fit more content */
|
||||
.spottable th,
|
||||
.spottable td {
|
||||
padding-left: 4px !important;
|
||||
padding-right: 4px !important;
|
||||
}
|
||||
|
||||
/* Column widths - consolidated selectors */
|
||||
@@ -335,17 +349,19 @@ table tbody tr.cat-nearest-above td {
|
||||
.spottable th:nth-child(2), .spottable td:nth-child(2) { width: 53px; } /* Band */
|
||||
.spottable th:nth-child(3), .spottable td:nth-child(3) { width: 90px; } /* Frequency */
|
||||
.spottable th:nth-child(4), .spottable td:nth-child(4) { width: 60px; } /* Mode */
|
||||
.spottable th:nth-child(5), .spottable td:nth-child(5) { width: 115px; } /* Callsign (reduced by 5px) */
|
||||
.spottable th:nth-child(6), .spottable td:nth-child(6) { width: 40px; } /* Continent */
|
||||
.spottable th:nth-child(7), .spottable td:nth-child(7) { width: 50px; } /* CQ Zone */
|
||||
.spottable th:nth-child(8), .spottable td:nth-child(8) { width: 50px; } /* Flag */
|
||||
.spottable th:nth-child(9), .spottable td:nth-child(9) { width: 150px; } /* Entity (DXCC name) */
|
||||
.spottable th:nth-child(10), .spottable td:nth-child(10) { width: 60px; } /* DXCC Number */
|
||||
.spottable th:nth-child(11), .spottable td:nth-child(11) { width: 115px; } /* de Callsign (Spotter) (reduced by 5px) */
|
||||
.spottable th:nth-child(12), .spottable td:nth-child(12) { width: 50px; } /* de Cont */
|
||||
.spottable th:nth-child(13), .spottable td:nth-child(13) { width: 50px; } /* de CQZ */
|
||||
.spottable th:nth-child(14), .spottable td:nth-child(14) { width: 120px; } /* Special (LoTW, POTA, etc) (increased by 10px) */
|
||||
.spottable th:nth-child(15), .spottable td:nth-child(15) { min-width: 100px; width: 100%; } /* Message - fills remaining space */
|
||||
.spottable th:nth-child(5), .spottable td:nth-child(5) { width: 70px; } /* Submode */
|
||||
.spottable th:nth-child(6), .spottable td:nth-child(6) { width: 115px; } /* Callsign (reduced by 5px) */
|
||||
.spottable th:nth-child(7), .spottable td:nth-child(7) { width: 40px; } /* Continent */
|
||||
.spottable th:nth-child(8), .spottable td:nth-child(8) { width: 50px; } /* CQ Zone */
|
||||
.spottable th:nth-child(9), .spottable td:nth-child(9) { width: 50px; } /* Flag */
|
||||
.spottable th:nth-child(10), .spottable td:nth-child(10) { width: 150px; } /* Entity (DXCC name) */
|
||||
.spottable th:nth-child(11), .spottable td:nth-child(11) { width: 60px; } /* DXCC Number */
|
||||
.spottable th:nth-child(12), .spottable td:nth-child(12) { width: 115px; } /* de Callsign (Spotter) (reduced by 5px) */
|
||||
.spottable th:nth-child(13), .spottable td:nth-child(13) { width: 50px; } /* de Cont */
|
||||
.spottable th:nth-child(14), .spottable td:nth-child(14) { width: 50px; } /* de CQZ */
|
||||
.spottable th:nth-child(15), .spottable td:nth-child(15) { width: 95px; } /* Last QSO */
|
||||
.spottable th:nth-child(16), .spottable td:nth-child(16) { width: 120px; } /* Special (LoTW, POTA, etc) (increased by 10px) */
|
||||
.spottable th:nth-child(17), .spottable td:nth-child(17) { min-width: 70px; width: 100%; } /* Message - fills remaining space */
|
||||
|
||||
/* Hidden class for responsive columns (controlled by JavaScript) */
|
||||
.spottable .column-hidden {
|
||||
@@ -358,14 +374,14 @@ table tbody tr.cat-nearest-above td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.spottable td:nth-child(15) {
|
||||
.spottable td:nth-child(17) {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
font-size: calc(1rem - 4px);
|
||||
}
|
||||
|
||||
.spottable td:nth-child(6), .spottable td:nth-child(12) {
|
||||
.spottable td:nth-child(7), .spottable td:nth-child(13) {
|
||||
font-family: 'Segoe UI Emoji', 'Noto Color Emoji', 'Apple Color Emoji', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -388,18 +404,18 @@ table tbody tr.cat-nearest-above td {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.spottable td:nth-child(14) {
|
||||
.spottable td:nth-child(16) {
|
||||
overflow: visible;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Center alignment for specific columns */
|
||||
.spottable th:nth-child(6), .spottable td:nth-child(6), /* Continent (spotted) */
|
||||
.spottable th:nth-child(7), .spottable td:nth-child(7), /* CQ Zone (spotted) */
|
||||
.spottable th:nth-child(8), .spottable td:nth-child(8), /* Flag */
|
||||
.spottable th:nth-child(10), .spottable td:nth-child(10), /* DXCC Number */
|
||||
.spottable th:nth-child(12), .spottable td:nth-child(12), /* de Cont (spotter) */
|
||||
.spottable th:nth-child(13), .spottable td:nth-child(13) /* de CQZ (spotter) */
|
||||
.spottable th:nth-child(7), .spottable td:nth-child(7), /* Continent (spotted) */
|
||||
.spottable th:nth-child(8), .spottable td:nth-child(8), /* CQ Zone (spotted) */
|
||||
.spottable th:nth-child(9), .spottable td:nth-child(9), /* Flag */
|
||||
.spottable th:nth-child(11), .spottable td:nth-child(11), /* DXCC Number */
|
||||
.spottable th:nth-child(13), .spottable td:nth-child(13), /* de Cont (spotter) */
|
||||
.spottable th:nth-child(14), .spottable td:nth-child(14) /* de CQZ (spotter) */
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
@@ -409,13 +425,13 @@ table tbody tr.cat-nearest-above td {
|
||||
.spottable {
|
||||
table-layout: auto !important;
|
||||
}
|
||||
.spottable th:nth-child(9), .spottable td:nth-child(9) {
|
||||
.spottable th:nth-child(10), .spottable td:nth-child(10) {
|
||||
width: auto !important;
|
||||
min-width: 150px !important;
|
||||
}
|
||||
.spottable th:nth-child(1), .spottable td:nth-child(1) { width: 50px !important; } /* Age */
|
||||
.spottable th:nth-child(3), .spottable td:nth-child(3) { width: 90px !important; } /* Frequency */
|
||||
.spottable th:nth-child(5), .spottable td:nth-child(5) { width: 100px !important; } /* Callsign */
|
||||
.spottable th:nth-child(6), .spottable td:nth-child(6) { width: 100px !important; } /* Callsign */
|
||||
}
|
||||
|
||||
.spottable thead th {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user