mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 18:27:16 +00:00
590 lines
19 KiB
PHP
590 lines
19 KiB
PHP
<?php
|
|
|
|
use Wavelog\Dxcc\Dxcc;
|
|
|
|
class Dxcluster_model extends CI_Model {
|
|
|
|
protected $bandedges = [];
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
public function dxc_spotlist($band = '20m', $maxage = 60, $de = '', $mode = 'All') {
|
|
$this->load->helper(array('psr4_autoloader'));
|
|
|
|
// Load cache driver once
|
|
$this->load->driver('cache', array('adapter' => 'file', 'backup' => 'file'));
|
|
|
|
// Check cache first for processed spot list
|
|
$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)
|
|
if ($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('logbooks_model'); lives in the autoloader
|
|
$this->load->model('logbook_model');
|
|
$logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
|
|
|
if (!$jsonraw = $this->cache->get('dxcache'.$band)) {
|
|
// 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 [];
|
|
}
|
|
|
|
$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;
|
|
}
|
|
$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);
|
|
|
|
// Apply mode filter early
|
|
if (($mode != 'All') && ($mode != $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);
|
|
|
|
// DXCC lookups with memoization to avoid duplicate lookups
|
|
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'] ?? '',
|
|
'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'] ?? '',
|
|
'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];
|
|
$spot->last_wked->LAST_QSO = date($custom_date_format, strtotime($spot->last_wked->LAST_QSO));
|
|
}
|
|
} 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)
|
|
if (!empty($spotsout)) {
|
|
$this->cache->save($cache_key, $spotsout, 59);
|
|
}
|
|
|
|
return $spotsout; }
|
|
|
|
// We need to build functions that check the frequency limit
|
|
// Right now this is just a proof of concept to determine mode
|
|
function get_mode($spot) {
|
|
if ($this->Frequency2Mode($spot->frequency) != '') {
|
|
return $this->Frequency2Mode($spot->frequency);
|
|
}
|
|
|
|
// Fallbacks using message keywords
|
|
if (isset($spot->message)) {
|
|
$message = strtolower($spot->message);
|
|
if (strpos($message, 'cw') !== false) {
|
|
return 'cw';;
|
|
}
|
|
if ((strpos($message, 'ft8') !== false || strpos($message, 'rtty') !== false || strpos($message, 'sstv') !== false)) {
|
|
return 'digi';;
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function modefilter($spot, $mode) {
|
|
$mode = strtolower($mode); // Normalize case
|
|
|
|
if ($this->isFrequencyInMode($spot->frequency, $mode)) {
|
|
return true;
|
|
}
|
|
|
|
// Fallbacks using message keywords
|
|
if (isset($spot->message)) {
|
|
$message = strtolower($spot->message);
|
|
if ($mode === 'cw' && strpos($message, 'cw') !== false) {
|
|
return true;
|
|
}
|
|
if ($mode === 'digi' && (strpos($message, 'ft8') !== false || strpos($message, 'rtty') !== false || strpos($message, 'sstv') !== false)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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 references and contest flag in dxcc_spotted if they don't exist
|
|
if (!property_exists($spot->dxcc_spotted, 'sota_ref')) {
|
|
$spot->dxcc_spotted->sota_ref = '';
|
|
}
|
|
if (!property_exists($spot->dxcc_spotted, 'pota_ref')) {
|
|
$spot->dxcc_spotted->pota_ref = '';
|
|
}
|
|
if (!property_exists($spot->dxcc_spotted, 'iota_ref')) {
|
|
$spot->dxcc_spotted->iota_ref = '';
|
|
}
|
|
if (!property_exists($spot->dxcc_spotted, 'wwff_ref')) {
|
|
$spot->dxcc_spotted->wwff_ref = '';
|
|
}
|
|
if (!property_exists($spot->dxcc_spotted, 'isContest')) {
|
|
$spot->dxcc_spotted->isContest = false;
|
|
}
|
|
|
|
// Process message only if we have missing references or need contest detection
|
|
$message = $spot->message ?? '';
|
|
if (!empty($message)) {
|
|
$upperMessage = strtoupper($message);
|
|
|
|
// Only extract park references if they're missing (not provided by DX cluster)
|
|
// This avoids unnecessary regex processing when data is already available
|
|
|
|
// SOTA format: XX/YY-### or XX/YY-#### (e.g., "G/LD-001", "W4G/NG-001", "DL/KW-044")
|
|
if (empty($spot->dxcc_spotted->sota_ref)) {
|
|
if (preg_match('/\b([A-Z0-9]{1,3}\/[A-Z]{2}-\d{3,4})\b/', $upperMessage, $sotaMatch)) {
|
|
$spot->dxcc_spotted->sota_ref = $sotaMatch[1];
|
|
}
|
|
}
|
|
|
|
// POTA format: XX-#### (e.g., "US-4306", "K-1234", "DE-0277")
|
|
// Must not match WWFF patterns (ending in FF)
|
|
if (empty($spot->dxcc_spotted->pota_ref)) {
|
|
if (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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// IOTA format: XX-### (e.g., "EU-005", "NA-001", "OC-123")
|
|
if (empty($spot->dxcc_spotted->iota_ref)) {
|
|
if (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")
|
|
if (empty($spot->dxcc_spotted->wwff_ref)) {
|
|
// Match both standard WWFF (XXFF-####) and USA format (KFF-####)
|
|
if (preg_match('/\b((?:[A-Z0-9]{2,4}FF|KFF)-\d{4})\b/', $upperMessage, $wwffMatch)) {
|
|
$spot->dxcc_spotted->wwff_ref = $wwffMatch[1];
|
|
}
|
|
}
|
|
|
|
// Detect contest spots - always perform this check
|
|
// Common contest indicators in spot comments
|
|
$contestIndicators = [
|
|
'CONTEST', // Generic contest mention
|
|
'CQ WW', // CQ World Wide
|
|
'CQ WPX', // CQ WPX
|
|
'ARRL', // ARRL contests
|
|
'IARU', // IARU HF Championship
|
|
'CQWW', // CQ WW (no space)
|
|
'CQWPX', // CQ WPX (no space)
|
|
'SWEEPSTAKES', // ARRL Sweepstakes
|
|
'FIELD DAY', // ARRL Field Day
|
|
'DX CONTEST', // Generic DX contest
|
|
'SSB CONTEST', // SSB contest
|
|
'CW CONTEST', // CW contest
|
|
'RTTY CONTEST', // RTTY contest
|
|
'VHF CONTEST', // VHF contest
|
|
'SPRINT', // Various sprints
|
|
'DXCC', // DXCC operations
|
|
'WAE', // Worked All Europe
|
|
'IOTA CONTEST', // IOTA contest
|
|
'NAQP', // North American QSO Party
|
|
'BARTG', // BARTG contests
|
|
'RSGB', // RSGB contests
|
|
'RUNDSPRUCH', // German contests
|
|
'JARTS', // JARTS contests
|
|
'CW OPEN', // CW Open
|
|
'SSB OPEN', // SSB Open
|
|
'EU CONTEST', // European contests
|
|
'NA CONTEST', // North American contests
|
|
'KING OF SPAIN', // King of Spain contest
|
|
'ALL ASIAN', // All Asian contest
|
|
];
|
|
|
|
// Check if message contains any contest indicators
|
|
foreach ($contestIndicators as $indicator) {
|
|
if (strpos($upperMessage, $indicator) !== false) {
|
|
$spot->dxcc_spotted->isContest = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Additional heuristic: Check for typical contest exchange patterns
|
|
// Example: "599 025" or "59 123" or "5NN K" (common contest exchanges)
|
|
if (!$spot->dxcc_spotted->isContest) {
|
|
// Match RST + serial number patterns
|
|
if (preg_match('/\b(599|59|5NN)\s+[0-9A-Z]{2,4}\b/', $upperMessage)) {
|
|
$spot->dxcc_spotted->isContest = true;
|
|
}
|
|
// Match zone/state exchanges like "CQ 14" or "CQ K"
|
|
if (preg_match('/\bCQ\s+[0-9A-Z]{1,3}\b/', $upperMessage)) {
|
|
$spot->dxcc_spotted->isContest = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $spot;
|
|
}
|
|
}
|