cloudlog/application/models/Pota.php
Peter Goodhall 2f917c8bcf Add advanced POTA awards dashboard with filtering and map
Introduces a new POTA awards dashboard with HTMX-powered filtering, statistics, progress bars, and a Leaflet map of worked parks. Adds new controller endpoints, model methods for filtered queries and park metadata, and modular view components for table, stats, progress, and map. Updates language files for new stats labels and enhances the update process to cache the full POTA parks CSV for richer features.
2026-01-10 15:08:22 +00:00

232 lines
7.1 KiB
PHP

<?php
class Pota extends CI_Model {
// Existing method used by current simple table
function get_all() {
$CI =& get_instance();
$CI->load->model('logbooks_model');
$logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
if (!$logbooks_locations_array) {
return null;
}
$this->load->model('bands');
$bandslots = $this->bands->get_worked_bands('pota');
if(!$bandslots) return null;
$this->db->where_in("station_id", $logbooks_locations_array);
$this->db->where_in("col_band", $bandslots);
$this->db->order_by("COL_POTA_REF", "ASC");
$this->db->where('COL_POTA_REF !=', '');
return $this->db->get($this->config->item('table_name'));
}
// New: generic filtered fetch for HTMX table
function fetch_qsos($filters) {
$CI =& get_instance();
$CI->load->model('logbooks_model');
$logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
if (!$logbooks_locations_array) {
return [];
}
$this->db->from($this->config->item('table_name'));
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_POTA_REF !=', '');
$this->apply_filters($filters);
$this->db->order_by('COL_TIME_ON', 'DESC');
$query = $this->db->get();
return $query->result();
}
// New: first and last QSO timestamps/refs within filters
function get_first_last($filters) {
$CI =& get_instance();
$CI->load->model('logbooks_model');
$logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
if (!$logbooks_locations_array) {
return ['first'=>null,'last'=>null];
}
// first
$this->db->from($this->config->item('table_name'));
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_POTA_REF !=', '');
$this->apply_filters($filters);
$this->db->order_by('COL_TIME_ON', 'ASC');
$this->db->limit(1);
$first = $this->db->get()->row();
// last
$this->db->from($this->config->item('table_name'));
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_POTA_REF !=', '');
$this->apply_filters($filters);
$this->db->order_by('COL_TIME_ON', 'DESC');
$this->db->limit(1);
$last = $this->db->get()->row();
return ['first'=>$first, 'last'=>$last];
}
// New: unique parks and optionally by band/mode
function get_uniques($filters, $by = null) {
$CI =& get_instance();
$CI->load->model('logbooks_model');
$logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
if (!$logbooks_locations_array) {
return [];
}
$this->db->from($this->config->item('table_name'));
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_POTA_REF !=', '');
$this->apply_filters($filters);
if ($by === 'band') {
$this->db->select('COL_BAND as k, COUNT(DISTINCT COL_POTA_REF) as v');
$this->db->group_by('COL_BAND');
} elseif ($by === 'mode') {
$this->db->select('(CASE WHEN COL_SUBMODE IS NOT NULL AND COL_SUBMODE<>"" THEN COL_SUBMODE ELSE COL_MODE END) as k, COUNT(DISTINCT COL_POTA_REF) as v', false);
$this->db->group_by('k');
} else {
$this->db->select('COUNT(DISTINCT COL_POTA_REF) as v');
}
$query = $this->db->get();
if (!$by) {
$row = $query->row();
return $row ? (int)$row->v : 0;
}
$out = [];
foreach ($query->result() as $r) {
$out[$r->k] = (int)$r->v;
}
return $out;
}
// New: confirmation counts
function get_confirmations($filters) {
$filtersConfirmed = $filters;
$filtersConfirmed['confirmed'] = true;
return $this->get_uniques($filtersConfirmed);
}
// New: read assets/json/pota_parks.csv and map refs to metadata
function get_parks_meta($refs) {
$refs = array_values(array_unique(array_filter($refs)));
if (empty($refs)) return [];
// Read only the needed refs from CSV to keep memory low
$out = $this->load_parks_subset($refs);
// If still missing coords, we skip map markers quietly
return $out;
}
private function load_parks_subset($wantedRefs) {
$path = FCPATH . 'assets/json/pota_parks.csv';
if (!file_exists($path)) return [];
$wantedSet = array_fill_keys($wantedRefs, true);
$found = [];
$fh = fopen($path, 'r');
if ($fh === false) return [];
$header = fgetcsv($fh);
if ($header === false) { fclose($fh); return []; }
$idx = $this->map_header_indices($header);
while (($cols = fgetcsv($fh)) !== false) {
if (!isset($cols[$idx['ref']])) continue;
$ref = trim($cols[$idx['ref']]);
if ($ref === '' || !isset($wantedSet[$ref])) continue;
$found[$ref] = [
'name' => $idx['name'] !== null && isset($cols[$idx['name']]) ? $cols[$idx['name']] : null,
'entity' => $idx['entity'] !== null && isset($cols[$idx['entity']]) ? $cols[$idx['entity']] : null,
'country' => $idx['country'] !== null && isset($cols[$idx['country']]) ? $cols[$idx['country']] : null,
'lat' => $idx['lat'] !== null && isset($cols[$idx['lat']]) ? floatval($cols[$idx['lat']]) : null,
'lon' => $idx['lon'] !== null && isset($cols[$idx['lon']]) ? floatval($cols[$idx['lon']]) : null,
];
// Early exit if we got all requested refs
if (count($found) === count($wantedSet)) {
break;
}
}
fclose($fh);
return $found;
}
private function map_header_indices($header) {
$find = function($candidates) use ($header) {
foreach ($header as $i => $h) {
$hn = strtolower(trim($h));
foreach ($candidates as $cand) {
if ($hn === $cand) return $i;
if (strpos($hn, $cand) !== false) return $i;
}
}
return null;
};
// Try to detect columns by common names
$refIdx = $find(['ref','reference','park id','park','entity id']);
if ($refIdx === null) $refIdx = 0; // fallback to first column
return [
'ref' => $refIdx,
'name' => $find(['name','park name','designation']),
'entity' => $find(['entity','state','province','region']),
'country' => $find(['country','country code']),
'lat' => $find(['lat','latitude']),
'lon' => $find(['lon','longitude','lng']),
];
}
private function apply_filters($filters) {
// Date range
if (!empty($filters['from'])) {
$this->db->where('COL_TIME_ON >=', $filters['from'] . ' 00:00:00');
}
if (!empty($filters['to'])) {
$this->db->where('COL_TIME_ON <=', $filters['to'] . ' 23:59:59');
}
// Band / SAT
if (!empty($filters['band']) && $filters['band'] !== 'All') {
if ($filters['band'] === 'SAT') {
$this->db->where('COL_PROP_MODE', 'SAT');
} else {
$this->db->where('(COL_PROP_MODE IS NULL OR COL_PROP_MODE <> "SAT")', null, false);
$this->db->where('COL_BAND', $filters['band']);
}
}
// Mode
if (!empty($filters['mode']) && $filters['mode'] !== 'All') {
$this->db->group_start();
$this->db->where('COL_MODE', $filters['mode']);
$this->db->or_where('COL_SUBMODE', $filters['mode']);
$this->db->group_end();
}
// Confirmed only
if (!empty($filters['confirmed'])) {
$this->db->group_start();
$this->db->where('col_qsl_rcvd', 'Y');
$this->db->or_where('col_lotw_qsl_rcvd', 'Y');
$this->db->group_end();
}
}
}
?>