mirror of
https://github.com/magicbug/Cloudlog
synced 2026-08-13 17:49:35 -04:00
Introduces the Personal Propagation Advisor, allowing users to analyze their own QSO data to determine the best times, bands, and modes for working specific DXCC entities. Adds a new controller, model methods, view, JavaScript, and language strings in multiple languages. Updates navigation and footer to support the new feature.
278 lines
9 KiB
PHP
278 lines
9 KiB
PHP
<?php if (!defined('BASEPATH')) exit('No direct script access allowed');
|
|
|
|
class Propagationadvisor extends CI_Controller
|
|
{
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->load->model('user_model');
|
|
if ($this->user_model->validate_session() == 0) {
|
|
redirect('user/login');
|
|
}
|
|
|
|
if (!$this->user_model->authorize(2)) {
|
|
$this->session->set_flashdata('notice', "You're not allowed to do that!");
|
|
redirect('dashboard');
|
|
}
|
|
|
|
$this->load->model('logbooks_model');
|
|
$this->load->model('logbook_model');
|
|
$this->load->model('bands');
|
|
$this->lang->load(array('statistics', 'general_words', 'menu'));
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$data['page_title'] = lang('propagation_title');
|
|
$data['dxcc_list'] = $this->logbook_model->fetchDxcc();
|
|
$bands = $this->bands->get_worked_bands();
|
|
$data['bands'] = array_values(array_filter($bands, function($band) {
|
|
return strtoupper($band) !== 'SAT';
|
|
}));
|
|
$data['modes'] = $this->logbook_model->get_modes()->result();
|
|
|
|
$this->load->view('interface_assets/header', $data);
|
|
$this->load->view('propagationadvisor/index', $data);
|
|
$this->load->view('interface_assets/footer');
|
|
}
|
|
|
|
public function data()
|
|
{
|
|
$filters = $this->build_filters();
|
|
if ($filters === null) {
|
|
return $this->respond_error(lang('propagation_required_filters'));
|
|
}
|
|
|
|
$stationIds = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
|
if (!$stationIds) {
|
|
return $this->respond_error(lang('error_no_active_station_profile'));
|
|
}
|
|
|
|
$hourlyRows = $this->logbook_model->get_propagation_hourly($filters, $stationIds);
|
|
$hourly = array_fill(0, 24, 0);
|
|
foreach ($hourlyRows as $row) {
|
|
$hour = (int)$row->hour_bucket;
|
|
if ($hour >= 0 && $hour <= 23) {
|
|
$hourly[$hour] = (int)$row->total;
|
|
}
|
|
}
|
|
|
|
$hourlyByBandRows = $this->logbook_model->get_propagation_hourly_by_band($filters, $stationIds);
|
|
$hourlyByBand = $this->format_hourly_by_band($hourlyByBandRows);
|
|
|
|
$bandBreakdown = $this->logbook_model->get_propagation_band_breakdown($filters, $stationIds);
|
|
$modeBreakdown = $this->logbook_model->get_propagation_mode_breakdown($filters, $stationIds);
|
|
$lastWorked = $this->logbook_model->get_propagation_last_worked($filters, $stationIds);
|
|
$trendRows = $this->logbook_model->get_propagation_activity_trend($filters, $stationIds, 90);
|
|
$trend = $this->format_trend($trendRows, 90);
|
|
|
|
$response = array(
|
|
'success' => true,
|
|
'filters' => $filters,
|
|
'hourly' => $hourly,
|
|
'hourly_by_band' => $hourlyByBand,
|
|
'total_qsos' => array_sum($hourly),
|
|
'best_window' => $this->compute_best_window($hourly),
|
|
'best_band' => $this->first_or_null($bandBreakdown, 'band'),
|
|
'best_mode' => $this->first_or_null($modeBreakdown, 'mode'),
|
|
'last_worked' => $lastWorked,
|
|
'band_breakdown' => $this->format_breakdown($bandBreakdown, 'band'),
|
|
'trend' => $trend,
|
|
);
|
|
|
|
return $this->output
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode($response));
|
|
}
|
|
|
|
public function export()
|
|
{
|
|
$filters = $this->build_filters();
|
|
if ($filters === null) {
|
|
show_error(lang('propagation_required_filters'), 400);
|
|
return;
|
|
}
|
|
|
|
$stationIds = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
|
|
if (!$stationIds) {
|
|
show_error(lang('error_no_active_station_profile'), 400);
|
|
return;
|
|
}
|
|
|
|
$hourlyRows = $this->logbook_model->get_propagation_hourly($filters, $stationIds);
|
|
$hourly = array_fill(0, 24, 0);
|
|
foreach ($hourlyRows as $row) {
|
|
$hour = (int)$row->hour_bucket;
|
|
if ($hour >= 0 && $hour <= 23) {
|
|
$hourly[$hour] = (int)$row->total;
|
|
}
|
|
}
|
|
|
|
$filename = 'propagation_advisor_' . date('Ymd_His') . '.csv';
|
|
header('Content-Description: File Transfer');
|
|
header('Content-Disposition: attachment; filename=' . $filename);
|
|
header('Content-Type: application/csv;charset=iso-8859-1');
|
|
|
|
$fp = fopen('php://output', 'w');
|
|
fputcsv($fp, array('Hour (UTC)', 'QSOs'));
|
|
foreach ($hourly as $hour => $count) {
|
|
fputcsv($fp, array(str_pad($hour, 2, '0', STR_PAD_LEFT), $count));
|
|
}
|
|
fclose($fp);
|
|
}
|
|
|
|
private function build_filters()
|
|
{
|
|
$dxcc = $this->input->get_post('dxcc', true);
|
|
if (empty($dxcc)) {
|
|
return null;
|
|
}
|
|
|
|
$band = $this->input->get_post('band', true);
|
|
$mode = $this->input->get_post('mode', true);
|
|
|
|
return array(
|
|
'dxcc' => (int) $dxcc,
|
|
'band' => $band ? trim($band) : '',
|
|
'mode' => $mode ? trim($mode) : '',
|
|
);
|
|
}
|
|
|
|
private function compute_best_window(array $hourly, $window = 3)
|
|
{
|
|
if (empty($hourly)) {
|
|
return null;
|
|
}
|
|
|
|
$bestStart = null;
|
|
$bestTotal = 0;
|
|
|
|
for ($start = 0; $start <= 23 - $window + 1; $start++) {
|
|
$sliceTotal = 0;
|
|
for ($i = 0; $i < $window; $i++) {
|
|
$sliceTotal += $hourly[$start + $i];
|
|
}
|
|
if ($sliceTotal > $bestTotal) {
|
|
$bestTotal = $sliceTotal;
|
|
$bestStart = $start;
|
|
}
|
|
}
|
|
|
|
if ($bestTotal === 0 || $bestStart === null) {
|
|
return null;
|
|
}
|
|
|
|
return array(
|
|
'start' => $bestStart,
|
|
'end' => $bestStart + $window - 1,
|
|
'total' => $bestTotal,
|
|
'label' => str_pad($bestStart, 2, '0', STR_PAD_LEFT) . '-' . str_pad($bestStart + $window - 1, 2, '0', STR_PAD_LEFT) . ' UTC',
|
|
);
|
|
}
|
|
|
|
private function first_or_null($collection, $key)
|
|
{
|
|
if (empty($collection)) {
|
|
return null;
|
|
}
|
|
|
|
$first = $collection[0];
|
|
if (!isset($first->$key)) {
|
|
return null;
|
|
}
|
|
|
|
return array(
|
|
'value' => $first->$key,
|
|
'total' => isset($first->total) ? (int)$first->total : null,
|
|
);
|
|
}
|
|
|
|
private function format_breakdown($collection, $key)
|
|
{
|
|
$output = array();
|
|
foreach ($collection as $item) {
|
|
if (isset($item->$key)) {
|
|
$output[] = array(
|
|
'label' => $item->$key,
|
|
'total' => (int)$item->total,
|
|
);
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
private function format_hourly_by_band($rows)
|
|
{
|
|
$result = array();
|
|
foreach ($rows as $row) {
|
|
$band = $row->band;
|
|
$hour = (int)$row->hour_bucket;
|
|
$total = (int)$row->total;
|
|
|
|
if (!isset($result[$band])) {
|
|
$result[$band] = array_fill(0, 24, 0);
|
|
}
|
|
|
|
if ($hour >= 0 && $hour <= 23) {
|
|
$result[$band][$hour] = $total;
|
|
}
|
|
}
|
|
|
|
// Order bands by frequency (reverse order so lowest frequency at top)
|
|
$this->load->model('bands');
|
|
$orderedBands = $this->bands->get_user_bands();
|
|
$orderedResult = array();
|
|
foreach ($orderedBands as $band) {
|
|
if (isset($result[$band])) {
|
|
$orderedResult[$band] = $result[$band];
|
|
}
|
|
}
|
|
|
|
return $orderedResult;
|
|
}
|
|
|
|
private function format_trend($rows, $days)
|
|
{
|
|
// Build date-indexed array for last $days days (inclusive of today)
|
|
$daily = array();
|
|
for ($i = $days - 1; $i >= 0; $i--) {
|
|
$dateKey = date('Y-m-d', strtotime("-$i days"));
|
|
$daily[$dateKey] = 0;
|
|
}
|
|
|
|
foreach ($rows as $row) {
|
|
$day = $row->day;
|
|
if (isset($daily[$day])) {
|
|
$daily[$day] = (int)$row->total;
|
|
}
|
|
}
|
|
|
|
$dates = array_keys($daily);
|
|
$counts = array_values($daily);
|
|
|
|
$last7 = array_sum(array_slice($counts, -7));
|
|
$prev7 = array_sum(array_slice($counts, -14, 7));
|
|
$last30 = array_sum(array_slice($counts, -30));
|
|
$prev30 = array_sum(array_slice($counts, -60, 30));
|
|
$last90 = array_sum($counts);
|
|
|
|
return array(
|
|
'dates' => $dates,
|
|
'counts' => $counts,
|
|
'last_7' => $last7,
|
|
'prev_7' => $prev7,
|
|
'last_30' => $last30,
|
|
'prev_30' => $prev30,
|
|
'last_90' => $last90,
|
|
);
|
|
}
|
|
|
|
private function respond_error($message)
|
|
{
|
|
return $this->output
|
|
->set_status_header(400)
|
|
->set_content_type('application/json')
|
|
->set_output(json_encode(array('success' => false, 'message' => $message)));
|
|
}
|
|
}
|