diff --git a/application/controllers/Cabrillo.php b/application/controllers/Cabrillo.php index d713b2e9b..faeb2a869 100644 --- a/application/controllers/Cabrillo.php +++ b/application/controllers/Cabrillo.php @@ -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'); + } } diff --git a/application/libraries/Cbr_parser.php b/application/libraries/Cbr_parser.php index a843df658..ce56b06a8 100755 --- a/application/libraries/Cbr_parser.php +++ b/application/libraries/Cbr_parser.php @@ -1,6 +1,304 @@ 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; + } } ?> diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index a9e24387e..1448660b1 100644 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -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; } + diff --git a/application/views/adif/import.php b/application/views/adif/import.php index 0415314b7..c44df4357 100644 --- a/application/views/adif/import.php +++ b/application/views/adif/import.php @@ -35,6 +35,15 @@ echo 'false'; } ?>"> + @@ -285,6 +294,35 @@ +
+ + + + +

Information

+

Important

+
+ +
+
+
+ + +
+
+
+
+ + +
+
+ diff --git a/application/views/cabrillo/cbr_success.php b/application/views/cabrillo/cbr_success.php new file mode 100755 index 000000000..3fcc68881 --- /dev/null +++ b/application/views/cabrillo/cbr_success.php @@ -0,0 +1,45 @@ +
+
+ session->flashdata('message')) { ?> + +
+

session->flashdata('message'); ?>

+
+ + +
+
+ +
+
+ 0) { ?> +

Yay, its updated!

+

+ +

+ + + +

+

+ + + + + + + + + + + + +
+ +
+
+ + +