initial implementation

This commit is contained in:
DB4SCW
2024-11-18 13:27:29 +00:00
parent 505ad499ac
commit 525f430ea9
5 changed files with 599 additions and 0 deletions

View File

@@ -139,4 +139,161 @@ class Cabrillo extends CI_Controller {
$this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard');
}
}
public function cbrimport(){
//load user stations
$this->load->model('stations');
$data['station_profile'] = $this->stations->all_of_user();
//set page title and target tab
$data['page_title'] = __("Cabrillo Import");
$data['tab'] = "cbr";
//configure upload
$config['upload_path'] = './uploads/';
$config['allowed_types'] = 'cbr|CBR';
//load upload library
$this->load->library('upload', $config);
//if upload fails, return with errors, reset upload filesize
if ( ! $this->upload->do_upload()) {
$data['error'] = $this->upload->display_errors();
$data['max_upload'] = ini_get('upload_max_filesize');
$this->load->view('interface_assets/header', $data);
$this->load->view('adif/import', $data);
$this->load->view('interface_assets/footer');
return;
}
//get data from upload
$data = array('upload_data' => $this->upload->data());
//set memory limit to allow big files
ini_set('memory_limit', '-1');
set_time_limit(0);
//load the logbook model
$this->load->model('logbook_model');
//load the Cabrillo parser
if (!$this->load->is_loaded('cbr_parser')) {
$this->load->library('cbr_parser');
}
//parse the uploaded file
$parsed_cbr = $this->cbr_parser->parse_from_file('./uploads/'.$data['upload_data']['file_name']);
//return with error, reset upload filesize
if(count($parsed_cbr["QSOS"]) < 1)
{
$data['error'] = __("Broken CBR file - no QSO data or incomplete header found.");
$data['max_upload'] = ini_get('upload_max_filesize');
$this->load->view('interface_assets/header', $data);
$this->load->view('adif/import', $data);
$this->load->view('interface_assets/footer');
return;
}
//get flag about the presence of the serial number
$serial_number_present = ($this->input('serial_number_present', true) == 1);
//get all station ids for the active user
$this->load->model('stations');
$station_ids = [];
foreach ($this->stations->all_of_user() as $station) {
array_push($station_ids, $station->station_id);
}
//create helper variables
$custom_errors = [];
$i = 1;
//process each contest qso
foreach ($parsed_cbr["QSOS"] as $qso) {
//get relevant data from header and qso line
$station_callsign = $parsed_cbr["HEADER"]["CALLSIGN"];
$contest_id = $parsed_cbr["HEADER"]["CONTEST"];
$callsign = $qso["RCVD_CALLSIGN"];
$band = $qso["BAND"];
$mode = $qso["MODE"];
$date = $qso["DATE"];
$time = $qso["TIME"];
//load QSO
$contest_qsos = $this->logbook_model->getContestQSO($station_ids, $station_callsign, $contest_id, $callsign, $band, $mode, $date, $time)->result();
//create error if more than 1 QSO is found and skip
if(count($contest_qsos) != 1){
array_push($custom_errors, "QSO " . $i . " not found or more that 1 QSOs found that match the criteria of the CBR file. Skipping as a safety measure.");
$i++;
continue;
}
//load the first and only row
$contest_qso = $contest_qsos[0];
//load unique primary key
$contest_qso_id = $contest_qso->COL_PRIMARY_KEY;
//get new serial numbers if required, otherwise default to null. If serial is not numeric, use 0
$stx = $serial_number_present ? (is_numeric($qso["SENT_SERIAL"]) ? (int)$qso["SENT_SERIAL"] : 0) : null;
$srx = $serial_number_present ? (is_numeric($qso["RCVD_SERIAL"]) ? (int)$qso["RCVD_SERIAL"] : 0) : null;
//get count of exchanges
$sent_exchange_count = $parsed_cbr["SENT_EXCHANGE_COUNT"];
$rcvd_exchange_count = $parsed_cbr["RCVD_EXCHANGE_COUNT"];
//default to empty exchange strings
$stxstring = null;
$srxstring = null;
//process all sent exchanges
for ($i=1; $i <= $sent_exchange_count; $i++) {
if($stxstring == null)
{
$stxstring = $qso["SENT_EXCH_" . $i];
}else{
$stxstring += ' ' . $qso["SENT_EXCH_" . $i];
}
}
//process all sent exchanges
for ($i=1; $i <= $rcvd_exchange_count; $i++) {
if($srxstring == null)
{
$srxstring = $qso["RCVD_EXCH_" . $i];
}else{
$srxstring += ' ' . $qso["RCVD_EXCH_" . $i];
}
}
//correct data on contest qso
$this->logbook_model->set_contest_fields($contest_qso_id, $stx, $stxstring, $srx, $srxstring);
//increment counter
$i++;
}
//delete uploaded file
unlink('./uploads/' . $data['upload_data']['file_name']);
//set data for view
$data['cbr_errors'] = $custom_errors;
$data['cbr_error_count'] = count($custom_errors);
$data['cbr_update_count'] = count($parsed_cbr["QSOS"]) - count($custom_errors);
$data['page_title'] = __("CBR Data Imported");
//get view to user
$this->load->view('interface_assets/header', $data);
$this->load->view('cabrillo/cbr_success');
$this->load->view('interface_assets/footer');
}
}

View File

@@ -1,6 +1,304 @@
<?php
class CBR_Parser
{
public function parse_from_file($filename) : array
{
//load file, call parser
return $this->parse(mb_convert_encoding(file_get_contents($filename), "UTF-8"));
}
public function parse(string $input, $serial_number_present = false) : array
{
//split the input into lines
$lines = explode("\n", trim($input));
//initialize the result array
$qso_lines_raw = [];
$header = [];
//helper variable to determine common 59 element indices in QSO lines
$common_59_indices = null;
//flag to indicate processing mode
$qso_mode = false;
//helper variable to determine the maximum number of qso data fields
$max_qso_fields = 0;
//loop through each line
foreach ($lines as $line) {
//if we encounter "QSO" or "X-QSO" switch processing mode to QSO mode
if (strpos($line, 'QSO:') === 0 or strpos($line, 'X-QSO:') === 0) {
$qso_mode = true;
}else {
$qso_mode = false;
}
//if we encounter "END-OF-LOG", stop processing lines
if (strpos($line, 'END-OF-LOG') === 0) {
break;
}
//process and collect header lines if qso mode is not set
if (!$qso_mode) {
//split the line into an array using ': ' as the delimiter
$parts = explode(': ', $line, 2);
//collect header information
$header[$parts[0]] = $parts[1];
//skip to next line
continue;
}
//process and collect QSO lines if qso mode is set
if ($qso_mode) {
//split the line into the elements
$qso_elements = preg_split('/\s+/', trim($line));
//determine maximum qso field size
$max_qso_fields = max($max_qso_fields, count($qso_elements));
//add qso elements to qso line array
array_push($qso_lines_raw, $qso_elements);
//find all occurrences of "59"
$indices_of_59 = [];
foreach ($qso_elements as $index => $value) {
if ($value === "59" or $value === "599") {
$indices_of_59[] = $index;
}
}
//find common indices position
if ($common_59_indices === null) {
//initialize common indices on the first iteration
$common_59_indices = $indices_of_59;
} else {
//intersect with current indices, preserving only common indices
$common_59_indices = array_intersect($common_59_indices, $indices_of_59);
}
//skip to next line
continue;
}
}
//abort further processing if no qso lines were found, return header only
if(count($qso_lines_raw) < 1)
{
$result = [];
$result["HEADER"] = $header;
$result["QSOS"] = [];
$result["SENT_59_POS"] = 0;
$result["RCVD_59_POS"] = 0;
$result["SENT_EXCHANGE_COUNT"] = 0;
$result["RCVD_EXCHANGE_COUNT"] = 0;
//return result
return $result;
}
//abort if basic things (Callsign and Contest ID) are not included in the header
$header_fields = array_keys($header);
if(!in_array('CALLSIGN', $header_fields) or !in_array('CONTEST', $header)){
$result = [];
$result["HEADER"] = $header;
$result["QSOS"] = [];
$result["SENT_59_POS"] = 0;
$result["RCVD_59_POS"] = 0;
$result["SENT_EXCHANGE_COUNT"] = 0;
$result["RCVD_EXCHANGE_COUNT"] = 0;
//return blank result
return $result;
}
//get positions of 59s inside QSO lines
$sent_59_pos = min($common_59_indices);
$rcvd_59_pos = max($common_59_indices);
//get codeigniter instance
$CI = &get_instance();
//load Frequency library
if(!$CI->load->is_loaded('Frequency')) {
$CI->load->library('Frequency');
}
//using 59 positions, remake qso_lines
$qso_lines = [];
//change all QSOs into associative arrays with meaningful keys
foreach ($qso_lines_raw as $line) {
$qso_line = [];
//get well defined fields
$qso_line["QSO_MARKER"] = $line[0];
$qso_line["FREQ"] = $line[1];
$qso_line["CBR_MODE"] = $line[2];
$qso_line["DATE"] = $line[3];
$qso_line["TIME"] = $line[4];
$qso_line["OWN_CALLSIGN"] = $line[5];
$qso_line["SENT_59"] = $line[$sent_59_pos];
//set serial if requested
if($serial_number_present)
{
$qso_line["SENT_SERIAL"] = $line[$sent_59_pos + 1];
}
//get all remaining sent exchanges
$exchange_nr = 1;
$startindex = ($sent_59_pos + ($serial_number_present ? 2 : 1));
$endindex = ($rcvd_59_pos - 1);
for ($i = $startindex; $i < $endindex; $i++) {
$qso_line["SENT_EXCH_" . $exchange_nr] = $line[$i];
$exchange_nr++;
}
//get rest of the well defined fields
$qso_line["RCVD_CALLSIGN"] = $line[$rcvd_59_pos - 1];
$qso_line["RCVD_59"] = $line[$rcvd_59_pos];
//set serial if requested
if($serial_number_present)
{
$qso_line["RCVD_SERIAL"] = $line[$rcvd_59_pos + 1];
}
//get all remaining received exchanges
$exchange_nr = 1;
$startindex = ($rcvd_59_pos + ($serial_number_present ? 2 : 1));
$endindex = (count($line));
for ($i = $startindex; $i < $endindex; $i++) {
$qso_line["RCVD_EXCH_" . $exchange_nr] = $line[$i];
$exchange_nr++;
}
//end of data in CQR format
//enhance QSO data with additional fields
$band = "";
//convert frequency to integer if possible
if(is_numeric($qso_line["FREQ"])) {
$frequency = (int)$qso_line["FREQ"];
}else{
$frequency = null;
}
//convert CBR values to band where no real frequency is given.
//if frequency is given, consult the frequency library
switch ($qso_line["FREQ"]) {
case '50':
$band = '6m';
break;
case '70':
$band = '4m';
break;
case '144':
$band = '2m';
break;
case '222':
$band = '1.25m';
break;
case '432':
$band = '70cm';
break;
case '902':
$band = '33cm';
break;
case '1.2G':
$band = '23cm';
break;
case '2.3G':
$band = '13cm';
break;
case '3.4G':
$band = '9cm';
break;
case '5.7G':
$band = '6cm';
break;
case '10G':
$band = '3cm';
break;
case '24G':
$band = '1.25cm';
break;
case '47G':
$band = 'SAT';
break;
case '75G':
$band = 'SAT';
break;
case '122G':
$band = 'SAT';
break;
case '134G':
$band = 'SAT';
break;
case '241G':
$band = 'SAT';
break;
case 'LIGHT':
$band = 'SAT';
break;
default:
$band = $CI->frequency->GetBand($frequency * 1000);
break;
}
//set band data for QSO
$qso_line["BAND"] = $band;
//get Wavelog mode
$mode = "";
switch ($qso_line["CBR_MODE"]) {
case 'CW':
$mode = 'CW';
break;
case 'PH':
$mode = 'SSB';
break;
case 'FM':
$mode = 'FM';
break;
case 'RY':
$mode = 'RTTY';
break;
case 'DG':
//indeterminate Digimode
$mode = '';
break;
default:
//something is wrong with the CBR file
$mode = '';
break;
}
//set mode data for QSO
$qso_line["MODE"] = $mode;
//collect new associative array
array_push($qso_lines, $qso_line);
}
//construct result, including positions of 59s for further processing down the line
$result = [];
$result["HEADER"] = $header;
$result["QSOS"] = $qso_lines;
$result["SENT_59_POS"] = $sent_59_pos;
$result["RCVD_59_POS"] = $rcvd_59_pos;
$result["SENT_EXCHANGE_COUNT"] = $rcvd_59_pos - $sent_59_pos - ($serial_number_present ? 3 : 2);
$result["RCVD_EXCHANGE_COUNT"] = $max_qso_fields - 1 - $rcvd_59_pos - ($serial_number_present ? 1 : 0);
//return result
return $result;
}
}
?>

View File

@@ -5427,9 +5427,70 @@ class Logbook_model extends CI_Model {
}
return '';
}
function getContestQSO(array $station_ids, string $station_callsign, string $contest_id, string $callsign, string $band, string $mode, string $date, string $time)
{
//load QSO table
$this->db->select('*');
$this->db->from($this->config->item('table_name'));
//load only for given station_ids
$this->db->where_in('station_id', $station_ids);
//load only for the station_callsign given
$this->db->where('COL_STATION_CALLSIGN', xss_clean($station_callsign));
//load only for the given contest id
$this->db->where('COL_CONTEST_ID', xss_clean($contest_id));
//load only for this qso partners callsign
$this->db->where('COL_CALL', xss_clean($callsign));
//load only for given band (no cleaning necessary because provided by wavelog itself)
$this->db->where('COL_BAND', $band);
//load only for specific mode if the mode is determinate. If not, omit it. In most cases, that should be fine. Also provided by wavelog itself, so no cleaning.
if($mode != '')
{
$this->db->where('COL_MODE', $mode);
}
//prepare datetime from format '2099-12-31 13:47' to be usable in a performant query
$datetime_raw = $date . ' ' . substr($time, 0, 2) . ':' . substr($time, 2, 2);
$datetime = new DateTime($datetime_raw,new DateTimeZone('UTC'));
$synthetic_endtime = $datetime->add(new DateInterval('PT1M'));
//load only for specific date and time. Since
$this->db->where('COL_TIME_ON >=', $datetime->format('Y-m-d H:i:s'));
$this->db->where('COL_TIME_ON <', $synthetic_endtime->format('Y-m-d H:i:s'));
//return whatever is left
return $this->db->get();
}
function set_contest_fields($qso_primary_key, ?int $stx, ?string $stxstring, ?int $srx, ?string $srxstring) {
//assemble data fields from input
$data = $data = array(
'COL_STX' => $stx,
'COL_STX_STRING' => $stxstring == null ? null : substr($stxstring, 0, 32),
'COL_SRX' => $srx,
'COL_SRX_STRING' => $srxstring == null ? null : substr($srxstring, 0, 32)
);
//narrow db operation down to 1 QSO
$this->db->where(array('COL_PRIMARY_KEY' => $qso_primary_key));
//update data and return
$this->db->update($this->config->item('table_name'), $data);
return;
}
}
function validateADIFDate($date, $format = 'Ymd') {
$d = DateTime::createFromFormat($format, $date);
return $d && $d->format($format) == $date;
}

View File

@@ -35,6 +35,15 @@
echo 'false';
} ?>"><?= __("DARC DCL") ?></a>
</li>
<li class="nav-item">
<a class="nav-link <?php if ($showtab == 'cbr') {
echo 'active';
} ?>" id="cbr-tab" data-bs-toggle="tab" href="#cbr" role="tab" aria-controls="cbr" aria-selected="<?php if ($showtab == 'cbr') {
echo 'true';
} else {
echo 'false';
} ?>"><?= __("CBR Import") ?></a>
</li>
</ul>
</div>
@@ -285,6 +294,35 @@
</form>
</div>
</div>
<div class="tab-pane <?php if ($showtab == 'cbr') {
echo 'active';
} else {
echo 'fade';
} ?>" id="cbr" role="tabpanel" aria-labelledby="home-tab">
<?php if (isset($error) && $showtab == 'cbr') { ?>
<div class="alert alert-danger" role="alert">
<?php echo $error; ?>
</div>
<?php } ?>
<p class="card-text"><span class="badge text-bg-info">Information</span> <?= __("If you imported an ADIF file of a contest, provided by another logging software, sometimes, depending on that software, your exchanges will not be imported properly from that softwares ADIF. If you like to correct that, you can provide the Cabrillo file that this software also provides to rewrite that data in Wavelog.") ?></p>
<p class="card-text"><span class="badge text-bg-warning">Important</span> <?= __("Please use this function before changing anything about the QSOs in Wavelog, as this function uses the Contest ID, as well as date and time information from both your already imported ADIF file, as well as the CBR file you are about to upload to match the QSOs and only correct relevant data.") ?></p>
<form class="form" action="<?php echo site_url('cabrillo/cbrimport'); ?>" method="post" enctype="multipart/form-data">
<div class="mb-3 row">
<div class="col-md-10">
<div class="form-check-inline">
<input class="form-check-input" type="checkbox" name="serial_number_present" value="0" id="serial_number_present" unchecked>
<label class="form-check-label" for="serial_number_present"><?= __("A Serial Number is part of the Exchange of this contest.") ?></label>
</div>
<div class="small form-text text-muted"><?= __("If unchecked, this will erase the default serial number that (for example) N1MM+ produces. If checked, it will correct the serial number if necessary.") ?></div>
</div>
</div>
<input class="form-control w-auto mb-2 me-sm-2" type="file" name="userfile" size="20" />
<button type="submit" class="btn btn-sm btn-primary mb-2" value="Upload"><?= __("Upload") ?></button>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,45 @@
<div class="container">
<br>
<?php if($this->session->flashdata('message')) { ?>
<!-- Display Message -->
<div class="alert-message error">
<p><?php echo $this->session->flashdata('message'); ?></p>
</div>
<?php } ?>
<div class="card">
<div class="card-header">
<?= __("Results of CBR Contest Data Update");?>
</div>
<div class="card-body">
<?php if($dcl_error_count[0] > 0) { ?>
<h3 class="card-title">Yay, its updated!</h3>
<p class="card-text"><?= __("Your contest QSOs have been updated using the values of your Cabrillo file.")?></p>
<?php } else { ?>
<h3 class="card-title"><?= __("No QSOs found which could be updated.")?></h3>
<?php } ?>
<div class="alert alert-info" role="alert">
<?= __("QSOs updated")?>: <?php echo $dcl_error_count[0] ?> / <?= __("QSOs ignored")?>: <?php echo $dcl_error_count[1] ?> / <?= __("QSOs unmatched")?>: <?php echo $dcl_error_count[2] ?>
</div>
<?php if($dcl_errors) { ?>
<h3><?= __("DOK Errors")?></h3>
<p><?= __("There is different data for DOK in your log compared to DCL")?></p>
<table width="100%">
<tr class="titles">
<td><?= __("Date"); ?></td>
<td><?= __("Time"); ?></td>
<td><?= __("Call"); ?></td>
<td><?= __("Band"); ?></td>
<td><?= __("Mode"); ?></td>
<td><?= __("DOK in Log"); ?></td>
<td><?= __("DOK in DCL"); ?></td>
<td><?= __("DCL QSL Status"); ?></td>
</tr>
<?php echo $dcl_errors; ?>
</table>
<?php } ?>
</div>
</div>
</div>