mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
706 lines
23 KiB
PHP
706 lines
23 KiB
PHP
<?php
|
|
|
|
use Wavelog\Dxcc\Dxcc;
|
|
|
|
class Dxcluster_model extends CI_Model {
|
|
|
|
protected $bandedges = [];
|
|
|
|
// Contest indicators - moved to class property to avoid recreation on every call
|
|
// Contest indicators - ORDER MATTERS! More specific names must come before generic terms
|
|
// to ensure accurate matching (e.g., "HAM SPIRIT" before "CONTEST")
|
|
protected $contestIndicators = [
|
|
'HAM SPIRIT', 'HAMSPIRIT', 'CQ WW', 'CQ WPX', 'ARRL', 'IARU', 'CQWW', 'CQWPX',
|
|
'SWEEPSTAKES', 'FIELD DAY', 'DX CONTEST', 'SSB CONTEST', 'CW CONTEST',
|
|
'RTTY CONTEST', 'VHF CONTEST', 'SPRINT', 'DXCC', 'WAE', 'IOTA CONTEST',
|
|
'NAQP', 'BARTG', 'RSGB', 'RUNDSPRUCH', 'JARTS', 'CW OPEN', 'SSB OPEN',
|
|
'EU CONTEST', 'NA CONTEST', 'KING OF SPAIN', 'ALL ASIAN', 'CONTEST'
|
|
];
|
|
|
|
// Digital modes for submode detection
|
|
// Note: Order matters! More specific modes (PSK31, PSK63) must come before generic (PSK)
|
|
// to ensure accurate submode detection via strpos() matching
|
|
protected $digitalModes = [
|
|
'FT8', 'FT4', 'RTTY', 'PSK31', 'PSK63', 'PSK', 'SSTV', 'MFSK',
|
|
'OLIVIA', 'CONTESTIA', 'JT65', 'JT9', 'WSPR', 'HELL', 'THOR',
|
|
'DOMINO', 'MT63', 'PACTOR', 'MSK144', 'Q65', 'JS8', 'FSK441',
|
|
'ISCAT', 'JT6M', 'FST4', 'FST4W', 'FREEDV', 'VARA'
|
|
];
|
|
|
|
public function __construct() {
|
|
$this->load->Model('Modes');
|
|
$this->db->where('bandedges.userid', $this->session->userdata('user_id'));
|
|
$query = $this->db->get('bandedges');
|
|
$result = $query->result_array();
|
|
|
|
if ($result) {
|
|
$this->bandedges = $result;
|
|
} else {
|
|
// Load bandedges into a class property
|
|
$this->db->where('userid', -1);
|
|
$query = $this->db->get('bandedges');
|
|
$this->bandedges = $query->result_array();
|
|
}
|
|
}
|
|
|
|
// Main function to get spot list from DXCache and process it
|
|
public function dxc_spotlist($band = '20m', $maxage = 60, $de = '', $mode = 'All') {
|
|
$this->load->helper(array('psr4_autoloader'));
|
|
|
|
// Check if file caching is enabled in config
|
|
$cache_enabled = $this->config->item('enable_dxcluster_file_cache') === true;
|
|
|
|
// Only load cache driver if caching is enabled
|
|
if ($cache_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 {
|
|
$custom_date_format = $this->config->item('qso_date_format');
|
|
}
|
|
|
|
$dxcache_url = ($this->optionslib->get_option('dxcache_url') == '' ? 'https://dxc.jo30.de/dxcache' : $this->optionslib->get_option('dxcache_url'));
|
|
|
|
if ($band == "All") {
|
|
$dxcache_url = $dxcache_url . '/spots/';
|
|
} else {
|
|
$dxcache_url = $dxcache_url . '/spots/'.$band;
|
|
}
|
|
|
|
$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);
|
|
}
|
|
|
|
if (!$jsonraw) {
|
|
// CURL Functions
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $dxcache_url);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog '.$this->optionslib->get_option('version').' DXLookup');
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
curl_setopt($ch, CURLOPT_HEADER, false);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
$jsonraw = curl_exec($ch);
|
|
$curl_error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
// Check for curl errors
|
|
if ($curl_error || $jsonraw === false) {
|
|
log_message('error', 'DXCluster: Failed to fetch spots from ' . $dxcache_url . ': ' . $curl_error);
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
|
|
// Apply mode filter early
|
|
if (($mode != 'All') && !$this->modefilter($singlespot, $mode)) {
|
|
continue;
|
|
}
|
|
|
|
// Faster age calculation using timestamps instead of DateTime objects
|
|
$spotTimestamp = strtotime($singlespot->when);
|
|
$minutes = (int)(($currentTimestamp - $spotTimestamp) / 60);
|
|
|
|
// Apply age filter early (before DXCC lookups)
|
|
if ($minutes > $maxage) {
|
|
continue;
|
|
}
|
|
|
|
$singlespot->age = $minutes;
|
|
$singlespot->when_pretty = date($custom_date_format . " H:i", $spotTimestamp);
|
|
|
|
// Perform DXCC lookups using cached results to prevent redundant database queries
|
|
if (!(property_exists($singlespot,'dxcc_spotted'))) {
|
|
$spotted_call = $singlespot->spotted ?? '';
|
|
if (empty($spotted_call)) {
|
|
continue;
|
|
}
|
|
if (!isset($dxcc_cache[$spotted_call])) {
|
|
$dxcc_cache[$spotted_call] = $dxccObj->dxcc_lookup($spotted_call, $date);
|
|
}
|
|
$dxcc = $dxcc_cache[$spotted_call];
|
|
$singlespot->dxcc_spotted = (object)[
|
|
'dxcc_id' => $dxcc['adif'] ?? 0,
|
|
'cont' => $dxcc['cont'] ?? '',
|
|
'cqz' => $dxcc['cqz'] ?? '',
|
|
'flag' => '',
|
|
'entity' => $dxcc['entity'] ?? 'Unknown'
|
|
];
|
|
}
|
|
if (!(property_exists($singlespot,'dxcc_spotter'))) {
|
|
$spotter_call = $singlespot->spotter ?? '';
|
|
if (empty($spotter_call)) {
|
|
continue;
|
|
}
|
|
if (!isset($dxcc_cache[$spotter_call])) {
|
|
$dxcc_cache[$spotter_call] = $dxccObj->dxcc_lookup($spotter_call, $date);
|
|
}
|
|
$dxcc = $dxcc_cache[$spotter_call];
|
|
$singlespot->dxcc_spotter = (object)[
|
|
'dxcc_id' => $dxcc['adif'] ?? 0,
|
|
'cont' => $dxcc['cont'] ?? '',
|
|
'cqz' => $dxcc['cqz'] ?? '',
|
|
'flag' => '',
|
|
'entity' => $dxcc['entity'] ?? 'Unknown'
|
|
];
|
|
}
|
|
// Apply continent filter early
|
|
if ($filter_continent && (!property_exists($singlespot->dxcc_spotter, 'cont') ||
|
|
$de_lower != strtolower($singlespot->dxcc_spotter->cont ?? ''))) {
|
|
continue;
|
|
}
|
|
|
|
// Extract park references from message
|
|
$singlespot = $this->enrich_spot_metadata($singlespot);
|
|
|
|
// Collect spots for batch processing
|
|
$spotsout[] = $singlespot;
|
|
}
|
|
|
|
|
|
// Batch process all spot statuses in a single optimized database query
|
|
if (!empty($spotsout)) {
|
|
$batch_statuses = $this->logbook_model->get_batch_spot_statuses(
|
|
$spotsout,
|
|
$logbooks_locations_array,
|
|
$band,
|
|
$mode
|
|
);
|
|
|
|
// Collect callsigns that need last_worked info (only those that are worked)
|
|
$worked_callsigns = [];
|
|
foreach ($spotsout as $spot) {
|
|
$callsign = $spot->spotted;
|
|
if (isset($batch_statuses[$callsign]) && $batch_statuses[$callsign]['worked_call']) {
|
|
$worked_callsigns[] = $callsign;
|
|
}
|
|
}
|
|
|
|
// Batch fetch last_worked info for all worked callsigns
|
|
$last_worked_batch = [];
|
|
if (!empty($worked_callsigns)) {
|
|
$last_worked_batch = $this->logbook_model->get_batch_last_worked(
|
|
$worked_callsigns,
|
|
$logbooks_locations_array,
|
|
$band
|
|
);
|
|
}
|
|
|
|
// Map batch results back to spots
|
|
foreach ($spotsout as $index => $spot) {
|
|
$callsign = $spot->spotted;
|
|
if (isset($batch_statuses[$callsign])) {
|
|
$status = $batch_statuses[$callsign];
|
|
$spot->worked_dxcc = $status['worked_dxcc'];
|
|
$spot->worked_call = $status['worked_call'];
|
|
$spot->cnfmd_dxcc = $status['cnfmd_dxcc'];
|
|
$spot->cnfmd_call = $status['cnfmd_call'];
|
|
$spot->cnfmd_continent = $status['cnfmd_continent'];
|
|
$spot->worked_continent = $status['worked_continent'];
|
|
|
|
// Use batch last_worked data
|
|
if ($spot->worked_call && isset($last_worked_batch[$callsign])) {
|
|
$spot->last_wked = $last_worked_batch[$callsign];
|
|
|
|
// Validate and convert date safely to prevent epoch date (1970) issues
|
|
if (!empty($spot->last_wked->LAST_QSO)) {
|
|
$timestamp = strtotime($spot->last_wked->LAST_QSO);
|
|
// Check if strtotime succeeded and timestamp is valid (> 0)
|
|
if ($timestamp !== false && $timestamp > 0) {
|
|
$spot->last_wked->LAST_QSO = date($custom_date_format, $timestamp);
|
|
} else {
|
|
// Invalid date - remove last_wked to prevent displaying incorrect date
|
|
unset($spot->last_wked);
|
|
}
|
|
} else {
|
|
// Empty date - remove last_wked
|
|
unset($spot->last_wked);
|
|
}
|
|
}
|
|
} else {
|
|
// Fallback for spots without status
|
|
$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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
return $spotsout;
|
|
}
|
|
|
|
// Determine mode with priority: POTA/SOTA mode > message keywords > frequency-based
|
|
function get_mode($spot) {
|
|
// 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;
|
|
|
|
if (!empty($potaMode)) {
|
|
return $this->mapToModeCategory($potaMode);
|
|
}
|
|
if (!empty($sotaMode)) {
|
|
return $this->mapToModeCategory($sotaMode);
|
|
}
|
|
|
|
// Priority 2: Message keywords (explicit mode in message text)
|
|
if (isset($spot->message)) {
|
|
$message = strtolower($spot->message);
|
|
|
|
// Check for CW first (simplest check)
|
|
if (strpos($message, 'cw') !== false) {
|
|
return 'cw';
|
|
}
|
|
|
|
// Check for digital modes using class property
|
|
foreach ($this->digitalModes as $digiMode) {
|
|
if (strpos($message, strtolower($digiMode)) !== false) {
|
|
return 'digi';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority 3: Frequency-based mode (from bandedges table)
|
|
// If frequency falls within a defined band edge, use that mode
|
|
$frequencyMode = $this->Frequency2Mode($spot->frequency);
|
|
if ($frequencyMode != '') {
|
|
return $frequencyMode;
|
|
}
|
|
|
|
// Default fallback: phone
|
|
return 'phone';
|
|
}
|
|
|
|
// Map specific mode names to mode categories (phone/cw/digi)
|
|
function mapToModeCategory($mode) {
|
|
$modeUpper = strtoupper($mode);
|
|
|
|
// CW modes
|
|
if ($modeUpper === 'CW') {
|
|
return 'cw';
|
|
}
|
|
|
|
// Digital modes - check against class property
|
|
foreach ($this->digitalModes as $digiMode) {
|
|
if ($modeUpper === $digiMode) {
|
|
return 'digi';
|
|
}
|
|
}
|
|
|
|
// Phone modes
|
|
if (in_array($modeUpper, ['SSB', 'LSB', 'USB', 'AM', 'FM', 'PHONE'])) {
|
|
return 'phone';
|
|
}
|
|
|
|
// Default to phone if unknown
|
|
return 'phone';
|
|
}
|
|
|
|
// Determine submode for more specific mode classification
|
|
function get_submode($spot) {
|
|
$mode = strtolower($spot->mode ?? '');
|
|
$frequency = floatval($spot->frequency);
|
|
|
|
// Check if we have specific mode from POTA/SOTA - use that as submode
|
|
$potaMode = $spot->pota_mode ?? $spot->dxcc_spotted->pota_mode ?? null;
|
|
$sotaMode = $spot->sota_mode ?? $spot->dxcc_spotted->sota_mode ?? null;
|
|
|
|
// If POTA/SOTA provides generic "SSB", refine it to LSB/USB based on frequency
|
|
if (!empty($potaMode) && strtoupper($potaMode) !== 'SSB') {
|
|
return strtoupper($potaMode);
|
|
}
|
|
if (!empty($sotaMode) && strtoupper($sotaMode) !== 'SSB') {
|
|
return strtoupper($sotaMode);
|
|
}
|
|
|
|
// For phone modes (including generic SSB from POTA/SOTA), determine LSB or USB based on frequency
|
|
if ($mode === 'phone' || $mode === 'ssb') {
|
|
// Below 10 MHz use LSB, above use USB
|
|
return $frequency < 10000 ? 'LSB' : 'USB';
|
|
}
|
|
|
|
// For CW, return CW
|
|
if ($mode === 'cw') {
|
|
return 'CW';
|
|
}
|
|
|
|
// For digital modes, try to get specific mode from message
|
|
if ($mode === 'digi') {
|
|
if (isset($spot->message)) {
|
|
$message = strtoupper($spot->message);
|
|
// Check for specific digital modes using class property
|
|
foreach ($this->digitalModes as $digiMode) {
|
|
if (strpos($message, $digiMode) !== false) {
|
|
return $digiMode;
|
|
}
|
|
}
|
|
}
|
|
return 'DIGI'; // Generic digital fallback
|
|
}
|
|
|
|
// Return uppercase version of mode as submode
|
|
return strtoupper($mode);
|
|
}
|
|
|
|
function modefilter($spot, $mode) {
|
|
$mode = strtolower($mode); // Normalize case
|
|
$spotMode = strtolower($spot->mode ?? ''); // Get already-determined mode
|
|
|
|
// Since get_mode() already determined the mode using priority logic
|
|
// (frequency > POTA/SOTA > message), we can directly compare
|
|
return $spotMode === $mode;
|
|
}
|
|
|
|
public function Frequency2Mode($frequency) {
|
|
// Ensure frequency is in Hz if input is in kHz
|
|
if ($frequency < 1_000_000) {
|
|
$frequency *= 1000;
|
|
}
|
|
|
|
foreach ($this->bandedges as $band) {
|
|
if ($frequency >= $band['frequencyfrom'] && $frequency < $band['frequencyto']) {
|
|
return $band['mode'];
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
public function isFrequencyInMode($frequency, $mode) {
|
|
// Ensure frequency is in Hz if input is in kHz
|
|
if ($frequency < 1_000_000) {
|
|
$frequency *= 1000;
|
|
}
|
|
|
|
foreach ($this->bandedges as $band) {
|
|
if (strtolower($band['mode']) === strtolower($mode)) {
|
|
if ($frequency >= $band['frequencyfrom'] && $frequency < $band['frequencyto']) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function dxc_qrg_lookup($qrg, $maxage = 120) {
|
|
$this->load->helper(array('psr4_autoloader'));
|
|
if (is_numeric($qrg)) {
|
|
|
|
$dxcache_url = ($this->optionslib->get_option('dxcache_url') == '' ? 'https://dxc.jo30.de/dxcache' : $this->optionslib->get_option('dxcache_url'));
|
|
|
|
$dxcache_url = $dxcache_url .'/spot/'.$qrg;
|
|
|
|
// CURL Functions
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $dxcache_url);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'Wavelog '.$this->optionslib->get_option('version').' DXLookup by QRG');
|
|
curl_setopt($ch, CURLOPT_HEADER, false);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
$jsonraw = curl_exec($ch);
|
|
curl_close($ch);
|
|
$json = json_decode($jsonraw);
|
|
|
|
$date = date('Ymd', time());
|
|
|
|
$dxccObj = new DXCC($date);
|
|
|
|
// Create JSON object
|
|
if (strlen($jsonraw)>20) {
|
|
$datetimecurrent = new DateTime("now", new DateTimeZone('UTC')); // Today's Date/Time
|
|
$datetimespot = new DateTime($json->when, new DateTimeZone('UTC'));
|
|
$spotage = $datetimecurrent->diff($datetimespot);
|
|
$minutes = $spotage->days * 24 * 60;
|
|
$minutes += $spotage->h * 60;
|
|
$minutes += $spotage->i;
|
|
$json->age=$minutes;
|
|
if ($minutes<=$maxage) {
|
|
$dxcc=$dxccObj->dxcc_lookup($json->spotter,date('Ymd', time()));
|
|
$json->dxcc_spotter=$dxcc;
|
|
return ($json);
|
|
} else {
|
|
return '';
|
|
}
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
|
|
function check_if_continent_worked_in_logbook($cont, $StationLocationsArray = null, $band = null, $mode = null) {
|
|
|
|
if ($StationLocationsArray == null) {
|
|
$this->load->model('logbooks_model');
|
|
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
|
} else {
|
|
$logbooks_locations_array = $StationLocationsArray;
|
|
}
|
|
|
|
$this->db->select('COL_CONT');
|
|
$this->db->where_in('station_id', $logbooks_locations_array);
|
|
$this->db->where('COL_CONT', $cont);
|
|
|
|
if (isset($mode)) {
|
|
$this->db->where(" COL_MODE in ".$this->Modes->get_modes_from_qrgmode($mode,true));
|
|
}
|
|
|
|
$band = ($band == 'All') ? null : $band;
|
|
if ($band != null && $band != 'SAT') {
|
|
$this->db->where('COL_BAND', $band);
|
|
} else if ($band == 'SAT') {
|
|
// Where col_sat_name is not empty
|
|
$this->db->where('COL_SAT_NAME !=', '');
|
|
}
|
|
$this->db->limit('2');
|
|
|
|
$query = $this->db->get($this->config->item('table_name'));
|
|
return $query->num_rows();
|
|
}
|
|
|
|
function check_if_continent_cnfmd_in_logbook($cont, $StationLocationsArray = null, $band = null, $mode = null) {
|
|
|
|
if ($StationLocationsArray == null) {
|
|
$this->load->model('logbooks_model');
|
|
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
|
} else {
|
|
$logbooks_locations_array = $StationLocationsArray;
|
|
}
|
|
|
|
$user_default_confirmation = $this->session->userdata('user_default_confirmation');
|
|
$extrawhere = '';
|
|
if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) {
|
|
$extrawhere = "COL_QSL_RCVD='Y'";
|
|
}
|
|
if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'L') !== false) {
|
|
if ($extrawhere != '') {
|
|
$extrawhere .= " OR";
|
|
}
|
|
$extrawhere .= " COL_LOTW_QSL_RCVD='Y'";
|
|
}
|
|
if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'E') !== false) {
|
|
if ($extrawhere != '') {
|
|
$extrawhere .= " OR";
|
|
}
|
|
$extrawhere .= " COL_EQSL_QSL_RCVD='Y'";
|
|
}
|
|
|
|
if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Z') !== false) {
|
|
if ($extrawhere != '') {
|
|
$extrawhere .= " OR";
|
|
}
|
|
$extrawhere .= " COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y'";
|
|
}
|
|
|
|
|
|
$this->db->select('COL_CONT');
|
|
$this->db->where_in('station_id', $logbooks_locations_array);
|
|
$this->db->where('COL_CONT', $cont);
|
|
|
|
if (isset($mode)) {
|
|
$this->db->where(" COL_MODE in ".$this->Modes->get_modes_from_qrgmode($mode,true));
|
|
}
|
|
|
|
$band = ($band == 'All') ? null : $band;
|
|
if ($band != null && $band != 'SAT') {
|
|
$this->db->where('COL_BAND', $band);
|
|
} else if ($band == 'SAT') {
|
|
// Where col_sat_name is not empty
|
|
$this->db->where('COL_SAT_NAME !=', '');
|
|
}
|
|
if ($extrawhere != '') {
|
|
$this->db->where('(' . $extrawhere . ')');
|
|
} else {
|
|
$this->db->where("1=0");
|
|
}
|
|
$this->db->limit('2');
|
|
|
|
$query = $this->db->get($this->config->item('table_name'));
|
|
|
|
return $query->num_rows();
|
|
}
|
|
|
|
/**
|
|
* Enrich spot metadata with park references and contest detection
|
|
* Extracts SOTA/POTA/IOTA/WWFF references and detects contest spots
|
|
* Only performs regex extraction if references are not already provided by DX cluster
|
|
* @param object $spot - Spot object with message and dxcc_spotted properties
|
|
* @return object - Spot object with enriched dxcc_spotted containing references and isContest flag
|
|
*/
|
|
function enrich_spot_metadata($spot) {
|
|
// Ensure dxcc_spotted object exists
|
|
if (!property_exists($spot, 'dxcc_spotted') || !is_object($spot->dxcc_spotted)) {
|
|
$spot->dxcc_spotted = (object)[];
|
|
}
|
|
|
|
// Initialize all properties at once using array merge
|
|
$defaults = [
|
|
'sota_ref' => '',
|
|
'pota_ref' => '',
|
|
'iota_ref' => '',
|
|
'wwff_ref' => '',
|
|
'isContest' => false,
|
|
'contestName' => null
|
|
];
|
|
|
|
foreach ($defaults as $prop => $defaultValue) {
|
|
if (!property_exists($spot->dxcc_spotted, $prop)) {
|
|
$spot->dxcc_spotted->$prop = $defaultValue;
|
|
}
|
|
} // Early exit if message is empty
|
|
$message = $spot->message ?? '';
|
|
if (empty($message)) {
|
|
return $spot;
|
|
}
|
|
|
|
$upperMessage = strtoupper($message);
|
|
|
|
// Check which references are missing to minimize regex executions
|
|
$needsSota = empty($spot->dxcc_spotted->sota_ref);
|
|
$needsPota = empty($spot->dxcc_spotted->pota_ref);
|
|
$needsIota = empty($spot->dxcc_spotted->iota_ref);
|
|
$needsWwff = empty($spot->dxcc_spotted->wwff_ref);
|
|
|
|
// Early exit if all references already populated
|
|
if (!$needsSota && !$needsPota && !$needsIota && !$needsWwff && $spot->dxcc_spotted->isContest) {
|
|
return $spot;
|
|
}
|
|
|
|
// Combined regex approach - execute all patterns in one pass if any are needed
|
|
if ($needsSota || $needsPota || $needsIota || $needsWwff) {
|
|
// SOTA format: XX/YY-### or XX/YY-#### (e.g., "G/LD-001", "W4G/NG-001", "DL/KW-044")
|
|
if ($needsSota && preg_match('/\b([A-Z0-9]{1,3}\/[A-Z]{2}-\d{3,4})\b/', $upperMessage, $sotaMatch)) {
|
|
$spot->dxcc_spotted->sota_ref = $sotaMatch[1];
|
|
}
|
|
|
|
// IOTA format: XX-### (e.g., "EU-005", "NA-001", "OC-123")
|
|
// Check IOTA before POTA as it's more specific
|
|
if ($needsIota && preg_match('/\b((?:AF|AN|AS|EU|NA|OC|SA)-\d{3})\b/', $upperMessage, $iotaMatch)) {
|
|
$spot->dxcc_spotted->iota_ref = $iotaMatch[1];
|
|
}
|
|
|
|
// WWFF format: XXFF-#### or KFF-#### (e.g., "GIFF-0001", "K1FF-0123", "ON4FF-0050", "KFF-6731")
|
|
// Check WWFF before POTA to avoid conflicts
|
|
if ($needsWwff && preg_match('/\b((?:[A-Z0-9]{2,4}FF|KFF)-\d{4})\b/', $upperMessage, $wwffMatch)) {
|
|
$spot->dxcc_spotted->wwff_ref = $wwffMatch[1];
|
|
}
|
|
|
|
// POTA format: XX-#### (e.g., "US-4306", "K-1234", "DE-0277")
|
|
// Must not match WWFF patterns (ending in FF) - checked last to avoid conflicts
|
|
if ($needsPota && preg_match('/\b([A-Z0-9]{1,5}-\d{4,5})\b/', $upperMessage, $potaMatch)) {
|
|
// Exclude WWFF patterns (contain FF-)
|
|
if (strpos($potaMatch[1], 'FF-') === false) {
|
|
$spot->dxcc_spotted->pota_ref = $potaMatch[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Contest detection - use class property instead of creating array each time
|
|
if (!$spot->dxcc_spotted->isContest) {
|
|
// More strict contest detection - require clear indicators
|
|
|
|
// Method 1: Explicit contest keywords with word boundaries
|
|
foreach ($this->contestIndicators as $indicator) {
|
|
// Use word boundary to avoid matching "CQ DX" in "CQ DX Americas" (which is just a CQ call)
|
|
if (preg_match('/\b' . preg_quote($indicator, '/') . '\b/', $upperMessage)) {
|
|
// Additional check: avoid false positives from generic "CQ" messages
|
|
if ($indicator === 'DX CONTEST' && preg_match('/^CQ\s+DX\s+[A-Z]+$/i', trim($message))) {
|
|
continue; // Skip "CQ DX <region>" patterns
|
|
}
|
|
$spot->dxcc_spotted->isContest = true;
|
|
$spot->dxcc_spotted->contestName = $indicator;
|
|
return $spot;
|
|
}
|
|
} // Method 2: Contest exchange pattern - must have RST AND serial AND no conversational words
|
|
// Exclude spots with conversational indicators (TU, TNX, 73, GL, etc.)
|
|
$conversational = '/\b(TU|TNX|THANKS|73|GL|HI|FB|CUL|HPE|PSE|DE)\b/';
|
|
|
|
if (!preg_match($conversational, $upperMessage)) {
|
|
// Look for typical contest exchange: RST + number (but not just any 599)
|
|
// Must be followed by more structured exchange (not just "ur 599")
|
|
if (preg_match('/\b(?:599|5NN)\s+(?:TU\s+)?[0-9]{2,4}\b/', $upperMessage) &&
|
|
!preg_match('/\bUR\s+599\b/', $upperMessage)) {
|
|
$spot->dxcc_spotted->isContest = true;
|
|
$spot->dxcc_spotted->contestName = '';
|
|
return $spot;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $spot;
|
|
}
|
|
}
|