mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
738 lines
25 KiB
PHP
738 lines
25 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_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_band_enabled || $cache_worked_enabled) {
|
|
$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'file'));
|
|
|
|
// Garbage collection: 1% chance to clean expired cache files
|
|
// Only needed when worked cache is enabled (creates many per-callsign files)
|
|
if ($cache_worked_enabled) {
|
|
$this->load->library('DxclusterCache');
|
|
$this->dxclustercache->maybeRunGc();
|
|
}
|
|
}
|
|
|
|
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.wavelog.org/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'));
|
|
|
|
// Cache key for RAW cluster response (instance-wide, no worked status)
|
|
// Use DxclusterCache library for centralized key generation
|
|
$this->load->library('DxclusterCache');
|
|
$raw_cache_key = $this->dxclustercache->getRawCacheKey($maxage, $band);
|
|
|
|
// Check cache for raw processed spots (without worked status)
|
|
$spotsout = null;
|
|
if ($cache_band_enabled) {
|
|
$spotsout = $this->cache->get($raw_cache_key);
|
|
}
|
|
|
|
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');
|
|
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 [];
|
|
}
|
|
|
|
// 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();
|
|
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
|
|
|
|
// 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'
|
|
];
|
|
}
|
|
|
|
// Extract park references from message
|
|
$singlespot = $this->enrich_spot_metadata($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);
|
|
}
|
|
}
|
|
|
|
// Apply user-specific filters AFTER cache retrieval (mode & continent)
|
|
if (!empty($spotsout) && ($mode != 'All' || ($de != '' && $de != 'Any'))) {
|
|
$de_lower = strtolower($de);
|
|
$filter_continent = ($de != '' && $de != 'Any');
|
|
$spotsout = array_filter($spotsout, function($spot) use ($mode, $de_lower, $filter_continent) {
|
|
if ($mode != 'All' && !$this->modefilter($spot, $mode)) return false;
|
|
if ($filter_continent && ($de_lower != strtolower($spot->dxcc_spotter->cont ?? ''))) return false;
|
|
return true;
|
|
});
|
|
$spotsout = array_values($spotsout); // Re-index array
|
|
}
|
|
|
|
// NOW add worked status if enabled (user-specific)
|
|
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_spots = [];
|
|
foreach ($spotsout as $spot) {
|
|
$callsign = $spot->spotted;
|
|
if (isset($batch_statuses[$callsign]) && $batch_statuses[$callsign]['worked_call']) {
|
|
$worked_spots[] = $spot;
|
|
}
|
|
}
|
|
|
|
// Batch fetch last_worked info for all worked spots (with their specific bands)
|
|
$last_worked_batch = [];
|
|
if (!empty($worked_spots)) {
|
|
$last_worked_batch = $this->logbook_model->get_batch_last_worked(
|
|
$worked_spots,
|
|
$logbooks_locations_array
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
} 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
|
|
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;
|
|
|
|
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) {
|
|
// 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);
|
|
|
|
// 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.wavelog.org/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;
|
|
}
|
|
}
|