cloudlog/application/controllers/Callhistory.php

865 lines
30 KiB
PHP
Raw Permalink Normal View History

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Callhistory extends CI_Controller {
public function __construct()
{
parent::__construct();
$this->load->model('user_model');
if (!$this->user_model->authorize(2)) {
$this->session->set_flashdata('notice', 'You\'re not allowed to do that!');
redirect('dashboard');
}
$this->load->model('callhistory_model');
}
public function index()
{
$user_id = (int)$this->session->userdata('user_id');
$this->load->model('logbooks_model');
$data['page_title'] = 'Call History';
$data['files'] = $this->callhistory_model->get_all_for_user($user_id);
$data['logbooks'] = $this->logbooks_model->show_all()->result();
$this->load->view('interface_assets/header', $data);
$this->load->view('callhistory/index', $data);
$this->load->view('interface_assets/footer');
}
public function upload()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$upload_dir = FCPATH . 'uploads/callhistory/' . $user_id . '/';
if (!is_dir($upload_dir)) {
if (!@mkdir($upload_dir, 0755, TRUE)) {
$this->session->set_flashdata('notice', 'Unable to create upload directory.');
redirect('callhistory');
return;
}
}
if (!isset($_FILES['history_file']) || empty($_FILES['history_file']['name'])) {
$this->session->set_flashdata('notice', 'Please select a call history file to upload.');
redirect('callhistory');
return;
}
$config = array(
'upload_path' => $upload_dir,
'allowed_types' => 'txt|csv|TXT|CSV',
'max_size' => 5120,
'encrypt_name' => TRUE,
'detect_mime' => TRUE,
'mod_mime_fix' => TRUE,
);
$this->load->library('upload');
$this->upload->initialize($config);
if (!$this->upload->do_upload('history_file')) {
$this->session->set_flashdata('notice', trim(strip_tags($this->upload->display_errors('', ''))));
redirect('callhistory');
return;
}
$upload_data = $this->upload->data();
$original_filename = $upload_data['client_name'] !== '' ? $upload_data['client_name'] : $upload_data['orig_name'];
$organization_label = strtoupper(trim((string)$this->input->post('organization_label', TRUE)));
if ($organization_label === '') {
@unlink($upload_data['full_path']);
$this->session->set_flashdata('notice', 'Organization label is required for call history uploads.');
redirect('callhistory');
return;
}
$data = array(
'user_id' => $user_id,
'file_label' => trim((string)$this->input->post('file_label', TRUE)),
'organization_label' => $organization_label,
'stored_filename' => $upload_data['file_name'],
'original_filename' => $original_filename,
'mime_type' => isset($upload_data['file_type']) ? $upload_data['file_type'] : null,
'file_size' => isset($upload_data['file_size']) ? (int)round($upload_data['file_size'] * 1024) : null,
'checksum' => @hash_file('sha256', $upload_data['full_path']),
'is_active' => 1,
'priority' => (int)$this->input->post('priority', TRUE),
'parser_status' => 'ready',
);
$this->callhistory_model->create_file($data);
$this->session->set_flashdata('notice', 'Call history file uploaded successfully.');
redirect('callhistory');
}
public function set_active()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$id = (int)$this->input->post('id', TRUE);
$is_active = $this->input->post('is_active', TRUE) ? 1 : 0;
$file = $this->callhistory_model->get_for_user_by_id($user_id, $id);
if (!$file) {
$this->session->set_flashdata('notice', 'Call history file not found.');
redirect('callhistory');
return;
}
$this->callhistory_model->update_active($user_id, $id, $is_active);
redirect('callhistory');
}
public function set_priority()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$id = (int)$this->input->post('id', TRUE);
$priority = (int)$this->input->post('priority', TRUE);
$file = $this->callhistory_model->get_for_user_by_id($user_id, $id);
if (!$file) {
$this->session->set_flashdata('notice', 'Call history file not found.');
redirect('callhistory');
return;
}
$this->callhistory_model->update_priority($user_id, $id, $priority);
redirect('callhistory');
}
public function delete()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$id = (int)$this->input->post('id', TRUE);
$file = $this->callhistory_model->get_for_user_by_id($user_id, $id);
if (!$file) {
$this->session->set_flashdata('notice', 'Call history file not found.');
redirect('callhistory');
return;
}
$path = FCPATH . 'uploads/callhistory/' . $user_id . '/' . $file->stored_filename;
if (is_file($path)) {
@unlink($path);
}
$this->callhistory_model->delete_file($user_id, $id);
$this->session->set_flashdata('notice', 'Call history file deleted.');
redirect('callhistory');
}
public function scan_preview()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$file_id = (int)$this->input->post('file_id', TRUE);
$logbook_id = $this->input->post('logbook_id', TRUE);
$logbook_id = ($logbook_id !== '' && $logbook_id !== FALSE) ? (int)$logbook_id : NULL;
$start_date = trim((string)$this->input->post('start_date', TRUE));
if ($start_date !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
$this->session->set_flashdata('notice', 'Start date must be in YYYY-MM-DD format.');
redirect('callhistory');
return;
}
$file = $this->callhistory_model->get_for_user_by_id($user_id, $file_id);
if (!$file) {
$this->session->set_flashdata('notice', 'Call history file not found.');
redirect('callhistory');
return;
}
if (trim((string)$file->organization_label) === '') {
$this->session->set_flashdata('notice', 'Bulk SIG updates require an organization label on the selected call history file.');
redirect('callhistory');
return;
}
$path = FCPATH . 'uploads/callhistory/' . $user_id . '/' . $file->stored_filename;
if (!is_file($path) || !is_readable($path)) {
$this->session->set_flashdata('notice', 'Uploaded file is not readable.');
redirect('callhistory');
return;
}
// Parse every callsign in the file
$all_callsigns = $this->get_all_callsigns_in_file($path);
if (empty($all_callsigns)) {
$this->session->set_flashdata('notice', 'No callsigns found in the selected file.');
redirect('callhistory');
return;
}
// Look up matching QSOs for all those callsigns
$qsos = $this->callhistory_model->get_qsos_for_callsigns($user_id, array_keys($all_callsigns), $logbook_id, $start_date !== '' ? $start_date : NULL);
$preview = array();
foreach ($qsos as $qso) {
$normalized_call = $this->normalize_callsign($qso->COL_CALL);
// Fall back to base call (strips /P prefix, country prefix etc.) for index lookup
if (isset($all_callsigns[$normalized_call])) {
$lookup_key = $normalized_call;
} elseif (isset($all_callsigns[$this->extract_base_callsign($normalized_call)])) {
$lookup_key = $this->extract_base_callsign($normalized_call);
} else {
continue;
}
$call_data = $all_callsigns[$lookup_key];
$proposed_sig = $file->organization_label;
$proposed_sig_info = trim((string)($call_data['exch1'] ?? ''));
$current_sig = trim((string)($qso->COL_SIG ?? ''));
$current_sig_info = trim((string)($qso->COL_SIG_INFO ?? ''));
// Only propose changes where both SIG fields are currently blank
if ($current_sig !== '' || $current_sig_info !== '') {
continue;
}
if ($proposed_sig === '') {
continue;
}
// Do not propose SIG updates without a membership number.
if ($proposed_sig_info === '') {
continue;
}
if (!$this->is_valid_sig_info_for_organization($proposed_sig, $proposed_sig_info)) {
continue;
}
$preview[] = array(
'qso_id' => (int)$qso->COL_PRIMARY_KEY,
'station_id' => (int)$qso->station_id,
'callsign' => $qso->COL_CALL,
'time_on' => $qso->COL_TIME_ON,
'band' => $qso->COL_BAND,
'mode' => $qso->COL_SUBMODE !== '' ? $qso->COL_SUBMODE : $qso->COL_MODE,
'station_location' => $qso->station_profile_name . ' (' . $qso->station_callsign . ')',
'current_sig' => $current_sig,
'current_sig_info' => $current_sig_info,
'new_sig' => $proposed_sig,
'new_sig_info' => $proposed_sig_info,
);
}
$this->load->model('logbooks_model');
$data['page_title'] = 'Call History - Scan Preview';
$data['files'] = $this->callhistory_model->get_all_for_user($user_id);
$data['preview'] = $preview;
$data['scan_file'] = $file;
$data['logbooks'] = $this->logbooks_model->show_all()->result();
$data['selected_logbook_id'] = $logbook_id;
$data['selected_start_date'] = $start_date;
$this->load->view('interface_assets/header', $data);
$this->load->view('callhistory/index', $data);
$this->load->view('interface_assets/footer');
}
public function scan_apply()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$raw_changes_json = (string)$this->input->post('changes_json', FALSE);
$raw_changes = array();
if ($raw_changes_json !== '') {
$decoded_changes = json_decode($raw_changes_json, TRUE);
if (is_array($decoded_changes)) {
$raw_changes = $decoded_changes;
}
}
if (empty($raw_changes)) {
$raw_changes = $this->input->post('changes', TRUE);
}
$file_id = (int)$this->input->post('file_id', TRUE);
$file = $this->callhistory_model->get_for_user_by_id($user_id, $file_id);
if (!$file || trim((string)$file->organization_label) === '') {
$this->session->set_flashdata('notice', 'Bulk SIG updates require an organization label on the selected call history file.');
redirect('callhistory');
return;
}
if (empty($raw_changes) || !is_array($raw_changes)) {
$this->session->set_flashdata('notice', 'No changes submitted.');
redirect('callhistory');
return;
}
// Validate that every station_id belongs to this user before applying
$station_ids = $this->callhistory_model->get_station_ids_for_logbook($user_id);
$station_ids_set = array_flip($station_ids);
$safe_changes = array();
foreach ($raw_changes as $change) {
$qso_id = isset($change['qso_id']) ? (int)$change['qso_id'] : 0;
$station_id = isset($change['station_id']) ? (int)$change['station_id'] : 0;
if ($qso_id <= 0 || $station_id <= 0) {
continue;
}
if (!isset($station_ids_set[$station_id])) {
continue;
}
$safe_changes[] = array(
'qso_id' => $qso_id,
'station_id' => $station_id,
'new_sig' => $this->security->xss_clean((string)($change['new_sig'] ?? '')),
'new_sig_info' => trim($this->security->xss_clean((string)($change['new_sig_info'] ?? ''))),
);
}
// Membership number is required for batch SIG updates.
$safe_changes = array_values(array_filter($safe_changes, function($change) {
return trim((string)$change['new_sig']) !== '' && trim((string)$change['new_sig_info']) !== '';
}));
// Enforce per-organization SIG info format rules (e.g. CWOPS requires numeric membership values).
$safe_changes = array_values(array_filter($safe_changes, function($change) use ($file) {
return $this->is_valid_sig_info_for_organization($file->organization_label, $change['new_sig_info']);
}));
if (empty($safe_changes)) {
$this->session->set_flashdata('notice', 'No valid changes to apply.');
redirect('callhistory');
return;
}
$selected_count = count($safe_changes);
$applied = $this->callhistory_model->apply_sig_backfill($safe_changes);
$this->session->set_flashdata('notice', $selected_count . ' QSO(s) selected, ' . $applied . ' QSO(s) updated with SIG data.');
redirect('callhistory');
}
public function scan_remove()
{
if (strtolower($this->input->method()) !== 'post') {
redirect('callhistory');
return;
}
$user_id = (int)$this->session->userdata('user_id');
$file_id = (int)$this->input->post('file_id', TRUE);
$logbook_id = $this->input->post('logbook_id', TRUE);
$logbook_id = ($logbook_id !== '' && $logbook_id !== FALSE) ? (int)$logbook_id : NULL;
$start_date = trim((string)$this->input->post('start_date', TRUE));
if ($start_date !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $start_date)) {
$this->session->set_flashdata('notice', 'Start date must be in YYYY-MM-DD format.');
redirect('callhistory');
return;
}
$file = $this->callhistory_model->get_for_user_by_id($user_id, $file_id);
if (!$file) {
$this->session->set_flashdata('notice', 'Call history file not found.');
redirect('callhistory');
return;
}
$organization_label = strtoupper(trim((string)$file->organization_label));
if ($organization_label === '') {
$this->session->set_flashdata('notice', 'Reverse SIG updates require an organization label on the selected call history file.');
redirect('callhistory');
return;
}
$path = FCPATH . 'uploads/callhistory/' . $user_id . '/' . $file->stored_filename;
if (!is_file($path) || !is_readable($path)) {
$this->session->set_flashdata('notice', 'Uploaded file is not readable.');
redirect('callhistory');
return;
}
$all_callsigns = $this->get_all_callsigns_in_file($path);
if (empty($all_callsigns)) {
$this->session->set_flashdata('notice', 'No callsigns found in the selected file.');
redirect('callhistory');
return;
}
$qsos = $this->callhistory_model->get_qsos_for_callsigns($user_id, array_keys($all_callsigns), $logbook_id, $start_date !== '' ? $start_date : NULL);
if (empty($qsos)) {
$this->session->set_flashdata('notice', 'No matching QSOs found for reverse update scope.');
redirect('callhistory');
return;
}
$changes = array();
foreach ($qsos as $qso) {
$current_sig = strtoupper(trim((string)($qso->COL_SIG ?? '')));
if ($current_sig !== $organization_label) {
continue;
}
$changes[] = array(
'qso_id' => (int)$qso->COL_PRIMARY_KEY,
'station_id' => (int)$qso->station_id,
'new_sig' => '',
'new_sig_info' => '',
);
}
if (empty($changes)) {
$this->session->set_flashdata('notice', 'No QSOs found with SIG ' . $organization_label . ' in the selected scope.');
redirect('callhistory');
return;
}
$applied = $this->callhistory_model->apply_sig_backfill($changes);
$this->session->set_flashdata('notice', count($changes) . ' QSO(s) matched SIG ' . $organization_label . ', ' . $applied . ' QSO(s) cleared.');
redirect('callhistory');
}
public function lookup()
{
if (strtolower($this->input->method()) !== 'post') {
show_404();
return;
}
$callsign = strtoupper(trim((string)$this->input->post('callsign', TRUE)));
$normalized_callsign = $this->normalize_callsign($callsign);
$response = array(
'status' => 'ok',
'callsign' => $normalized_callsign,
'matches' => array(),
'count' => 0,
);
if (strlen($normalized_callsign) < 3) {
header('Content-Type: application/json');
echo json_encode($response);
return;
}
$user_id = (int)$this->session->userdata('user_id');
$files = $this->callhistory_model->get_active_for_user($user_id);
$all_matches = array();
foreach ($files as $file) {
$path = FCPATH . 'uploads/callhistory/' . $user_id . '/' . $file->stored_filename;
if (!is_file($path) || !is_readable($path)) {
continue;
}
$matches = $this->find_matches_in_file($path, $normalized_callsign, $file);
if (!empty($matches)) {
$all_matches = array_merge($all_matches, $matches);
}
}
$response['matches'] = $all_matches;
$response['count'] = count($all_matches);
header('Content-Type: application/json');
echo json_encode($response);
}
private function get_all_callsigns_in_file($file_path)
{
$callsigns = array();
$header_map = null;
if (($handle = fopen($file_path, 'r')) === FALSE) {
return $callsigns;
}
while (($row = fgetcsv($handle, 0, ',')) !== FALSE) {
if (empty($row)) {
continue;
}
$row = array_map('trim', $row);
if (count($row) === 1 && $row[0] === '') {
continue;
}
if ($this->is_comment_row($row)) {
continue;
}
if ($this->looks_like_header($row)) {
$header_map = $this->build_header_map($row);
continue;
}
$row = $this->align_row_to_header($row, $header_map);
$row_callsign = $this->extract_by_map_or_index($row, $header_map, array('call', 'callsign', 'callsigns'), 0);
$row_callsign = $this->normalize_callsign($row_callsign);
if ($row_callsign === '') {
continue;
}
// Strip portable/alternative suffixes to get base call for matching
$base_callsign = $row_callsign;
if (strpos($base_callsign, '/') !== FALSE) {
$parts = explode('/', $base_callsign);
// Use the longest segment as the base callsign
usort($parts, function($a, $b) { return strlen($b) - strlen($a); });
$base_callsign = $parts[0];
}
$name = $this->extract_name($row, $header_map);
$exch1 = $this->extract_exch1($row, $header_map);
$callsigns[$base_callsign] = array(
'name' => $name,
'exch1' => $exch1,
);
// Also index by original callsign if it differs (e.g. contains /)
if ($row_callsign !== $base_callsign) {
$callsigns[$row_callsign] = array(
'name' => $name,
'exch1' => $exch1,
);
}
}
fclose($handle);
return $callsigns;
}
private function find_matches_in_file($file_path, $callsign, $file)
{
$matches = array();
$header_map = null;
if (($handle = fopen($file_path, 'r')) === FALSE) {
return $matches;
}
while (($row = fgetcsv($handle, 0, ',')) !== FALSE) {
if (empty($row)) {
continue;
}
$row = array_map('trim', $row);
if (count($row) === 1 && $row[0] === '') {
continue;
}
if ($this->is_comment_row($row)) {
continue;
}
if ($this->looks_like_header($row)) {
$header_map = $this->build_header_map($row);
continue;
}
$row = $this->align_row_to_header($row, $header_map);
$row_callsign = $this->extract_by_map_or_index($row, $header_map, array('call', 'callsign', 'callsigns'), 0);
$row_callsign = $this->normalize_callsign($row_callsign);
if ($row_callsign === '') {
continue;
}
// Match against the full input callsign OR its base call (strips prefix/suffix like F/ or /P)
if ($row_callsign !== $callsign && $row_callsign !== $this->extract_base_callsign($callsign)) {
continue;
}
$name = $this->extract_name($row, $header_map);
$exch1 = $this->extract_exch1($row, $header_map);
$matches[] = array(
'file_id' => (int)$file->id,
'file_label' => $file->file_label,
'organization_label' => $file->organization_label,
'name' => $name,
'exch1' => $exch1,
'sig' => $this->is_valid_sig_info_for_organization($file->organization_label, $exch1) ? $file->organization_label : '',
'sig_info' => $exch1,
);
}
fclose($handle);
return $matches;
}
private function is_comment_row($row)
{
foreach ($row as $column) {
$value = trim((string)$column);
if ($value === '') {
continue;
}
return strpos(ltrim($value, "\xEF\xBB\xBF"), '#') === 0;
}
return FALSE;
}
private function looks_like_header($row)
{
foreach ($row as $column) {
$normalized = $this->normalize_header_key($column);
if (in_array($normalized, array('call', 'callsign', 'name', 'exch1', 'exchange1'), TRUE)) {
return TRUE;
}
}
return FALSE;
}
private function build_header_map($row)
{
$map = array();
foreach ($row as $index => $column) {
$key = $this->normalize_header_key($column);
if ($key !== '') {
$map[$key] = $index;
}
}
return $map;
}
private function normalize_header_key($value)
{
$value = ltrim((string)$value, "\xEF\xBB\xBF");
$value = strtolower($value);
return preg_replace('/[^a-z0-9]/', '', $value);
}
private function align_row_to_header($row, $header_map)
{
if (!is_array($header_map)) {
return $row;
}
if (!array_key_exists('order', $header_map)) {
return $row;
}
$call_index = null;
if (array_key_exists('call', $header_map)) {
$call_index = (int)$header_map['call'];
} elseif (array_key_exists('callsign', $header_map)) {
$call_index = (int)$header_map['callsign'];
}
if ($call_index !== 1 || (int)$header_map['order'] !== 0) {
return $row;
}
// Some N1MM/CWOPS exports include an Order header column but omit that value in data rows.
if (count($row) < count($header_map)) {
$first = isset($row[0]) ? $this->normalize_callsign($row[0]) : '';
$second = isset($row[1]) ? $this->normalize_callsign($row[1]) : '';
if ($this->looks_like_callsign($first) && !$this->looks_like_callsign($second)) {
array_unshift($row, '');
}
}
return $row;
}
private function extract_by_map_or_index($row, $header_map, $candidate_keys, $default_index)
{
if (is_array($header_map)) {
foreach ($candidate_keys as $key) {
if (array_key_exists($key, $header_map)) {
$idx = $header_map[$key];
return isset($row[$idx]) ? trim((string)$row[$idx]) : '';
}
}
}
return isset($row[$default_index]) ? trim((string)$row[$default_index]) : '';
}
private function extract_exch1($row, $header_map)
{
// N1MM-style files: trust the declared header order (!!Order!!,Call,Name,Exch1,UserText,...).
if ($this->has_n1mm_order_header($header_map)) {
return $this->extract_by_map_or_index($row, $header_map, array('exch1'), 3);
}
$exch1 = $this->extract_by_map_or_index($row, $header_map, array('exch1', 'exchange1', 'exchange', 'member', 'membership'), 8);
if ($exch1 === '' && is_null($header_map)) {
$exch1 = $this->guess_exch1_without_header($row);
}
return $exch1;
}
private function has_n1mm_order_header($header_map)
{
if (!is_array($header_map)) {
return FALSE;
}
return array_key_exists('order', $header_map)
&& (array_key_exists('call', $header_map) || array_key_exists('callsign', $header_map))
&& array_key_exists('name', $header_map)
&& array_key_exists('exch1', $header_map);
}
private function extract_name($row, $header_map)
{
$name = $this->extract_by_map_or_index($row, $header_map, array('name', 'operator', 'opname'), 1);
if ($name !== '' || is_array($header_map)) {
return $name;
}
if (isset($row[1], $row[2])) {
$second_column = trim((string)$row[1]);
$third_column = trim((string)$row[2]);
if ($this->looks_like_exchange_value($second_column) && $this->looks_like_name_value($third_column)) {
return $third_column;
}
}
return $name;
}
private function guess_exch1_without_header($row)
{
if (isset($row[1], $row[2])) {
$second_column = trim((string)$row[1]);
$third_column = trim((string)$row[2]);
if ($this->looks_like_exchange_value($second_column) && $this->looks_like_name_value($third_column)) {
return $second_column;
}
if ($this->looks_like_name_value($second_column) && $this->looks_like_exchange_value($third_column)) {
return $third_column;
}
}
$indexes = array(8, 7, 6, 5, 4, 3, 2);
foreach ($indexes as $index) {
if (isset($row[$index]) && trim((string)$row[$index]) !== '') {
return trim((string)$row[$index]);
}
}
return '';
}
private function looks_like_name_value($value)
{
$value = trim((string)$value);
if ($value === '' || preg_match('/\d/', $value)) {
return FALSE;
}
if (preg_match('/[a-z]/', $value)) {
return TRUE;
}
return strlen($value) > 4;
}
private function looks_like_exchange_value($value)
{
$value = trim((string)$value);
if ($value === '') {
return FALSE;
}
return !$this->looks_like_name_value($value);
}
private function looks_like_callsign($callsign)
{
$callsign = strtoupper(trim((string)$callsign));
if ($callsign === '' || !preg_match('/^[A-Z0-9\/]{3,15}$/', $callsign)) {
return FALSE;
}
// Basic sanity: callsigns should contain at least one letter and one digit.
return preg_match('/[A-Z]/', $callsign) && preg_match('/\d/', $callsign);
}
private function is_valid_sig_info_for_organization($organization_label, $sig_info)
{
$organization_label = strtoupper(trim((string)$organization_label));
$sig_info = trim((string)$sig_info);
if ($sig_info === '') {
return FALSE;
}
// CWOPS membership numbers are numeric; skip textual values such as "CWA".
if ($organization_label === 'CWOPS') {
return ctype_digit($sig_info);
}
return TRUE;
}
private function normalize_callsign($callsign)
{
$callsign = strtoupper(trim((string)$callsign));
$callsign = str_replace('Ø', '0', $callsign);
return $callsign;
}
/**
* Extract the base callsign from a compound callsign like F/MM9SQL/P or MM9SQL/P.
* Returns the longest segment when split by '/', which is typically the base call.
*/
private function extract_base_callsign($callsign)
{
if (strpos($callsign, '/') === FALSE) {
return $callsign;
}
$parts = explode('/', $callsign);
usort($parts, function ($a, $b) { return strlen($b) - strlen($a); });
return $parts[0];
}
}