build_historical_caches($logbooks_locations_array, $start_date); $report = array( 'total_qsos' => $this->get_total_qsos($logbooks_locations_array, $start_date, $end_date), 'unique_callsigns' => $this->get_unique_callsigns($logbooks_locations_array, $start_date, $end_date), 'callsign_list' => $this->get_callsign_list($logbooks_locations_array, $start_date, $end_date), 'new_dxcc' => $this->get_new_dxcc($logbooks_locations_array, $start_date, $end_date, $prev_month_end), 'new_dxcc_by_band' => $this->get_new_dxcc_by_band($logbooks_locations_array, $start_date, $end_date, $prev_month_end), 'new_dxcc_satellite' => $this->get_new_dxcc_by_prop($logbooks_locations_array, $start_date, $end_date, $prev_month_end, 'SAT'), 'new_dxcc_eme' => $this->get_new_dxcc_by_prop($logbooks_locations_array, $start_date, $end_date, $prev_month_end, 'EME'), 'new_grids' => $this->get_new_gridsquares($logbooks_locations_array, $start_date, $end_date, $prev_month_end), 'new_grids_by_band' => $this->get_new_gridsquares_by_band($logbooks_locations_array, $start_date, $end_date, $prev_month_end), 'new_grids_satellite' => $this->get_new_gridsquares_by_prop($logbooks_locations_array, $start_date, $end_date, $prev_month_end, 'SAT'), 'new_grids_eme' => $this->get_new_gridsquares_by_prop($logbooks_locations_array, $start_date, $end_date, $prev_month_end, 'EME'), 'new_grids_hf' => $this->get_new_gridsquares_hf($logbooks_locations_array, $start_date, $end_date, $prev_month_end), 'countries_worked' => $this->get_countries_worked($logbooks_locations_array, $start_date, $end_date), 'modes' => $this->get_modes_breakdown($logbooks_locations_array, $start_date, $end_date), 'bands' => $this->get_bands_breakdown($logbooks_locations_array, $start_date, $end_date), 'continents' => $this->get_continents_breakdown($logbooks_locations_array, $start_date, $end_date), 'satellite_qsos' => $this->get_satellite_qsos($logbooks_locations_array, $start_date, $end_date), 'satellite_breakdown' => $this->get_satellite_breakdown($logbooks_locations_array, $start_date, $end_date), 'eme_qsos' => $this->get_eme_qsos($logbooks_locations_array, $start_date, $end_date), 'top_band' => '', 'top_mode' => '' ); // Calculate top band and mode if (!empty($report['bands'])) { arsort($report['bands']); $report['top_band'] = array_key_first($report['bands']); } if (!empty($report['modes'])) { arsort($report['modes']); $report['top_mode'] = array_key_first($report['modes']); } return $report; } // Cache variables for historical data lookups private $historical_grids = array(); private $historical_grids_by_prop = array(); private $historical_grids_hf = array(); /** * Build historical caches for fast "new" checks * This prevents hundreds of individual queries */ private function build_historical_caches($locations, $start_date) { // Get ALL historical gridsquares (before this month) $this->db->select('COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_PROP_MODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON <', $start_date); $this->db->where("(COL_GRIDSQUARE IS NOT NULL AND COL_GRIDSQUARE != '') OR (COL_VUCC_GRIDS IS NOT NULL AND COL_VUCC_GRIDS != '')"); $query = $this->db->get(); foreach ($query->result() as $row) { $prop_mode = !empty($row->COL_PROP_MODE) ? $row->COL_PROP_MODE : ''; // Process main gridsquare if (!empty($row->COL_GRIDSQUARE) && strlen($row->COL_GRIDSQUARE) >= 4) { $grid_4char = strtoupper(substr($row->COL_GRIDSQUARE, 0, 4)); $this->historical_grids[$grid_4char] = true; // Track by prop mode if ($prop_mode === 'SAT' || $prop_mode === 'EME') { $this->historical_grids_by_prop[$prop_mode][$grid_4char] = true; } else { $this->historical_grids_hf[$grid_4char] = true; } } // Process VUCC grids if (!empty($row->COL_VUCC_GRIDS)) { $vucc_grids = explode(',', $row->COL_VUCC_GRIDS); foreach ($vucc_grids as $grid) { $grid = trim($grid); if (strlen($grid) >= 4) { $grid_4char = strtoupper(substr($grid, 0, 4)); $this->historical_grids[$grid_4char] = true; // Track by prop mode if ($prop_mode === 'SAT' || $prop_mode === 'EME') { $this->historical_grids_by_prop[$prop_mode][$grid_4char] = true; } else { $this->historical_grids_hf[$grid_4char] = true; } } } } } } /** * Sort bands in amateur radio order (160m, 80m, 40m, 20m, etc.) */ private function sort_bands_array($bands_array) { // Define standard band order $band_order = array( '2190m' => 1, '630m' => 2, '560m' => 3, '160m' => 4, '80m' => 5, '60m' => 6, '40m' => 7, '30m' => 8, '20m' => 9, '17m' => 10, '15m' => 11, '12m' => 12, '10m' => 13, '8m' => 14, '6m' => 15, '5m' => 16, '4m' => 17, '2m' => 18, '1.25m' => 19, '70cm' => 20, '33cm' => 21, '23cm' => 22, '13cm' => 23, '9cm' => 24, '6cm' => 25, '3cm' => 26, '1.25cm' => 27, '6mm' => 28, '4mm' => 29, '2.5mm' => 30, '2mm' => 31, '1mm' => 32 ); uksort($bands_array, function($a, $b) use ($band_order) { $pos_a = isset($band_order[$a]) ? $band_order[$a] : 999; $pos_b = isset($band_order[$b]) ? $band_order[$b] : 999; if ($pos_a == $pos_b) { return strcmp($a, $b); } return $pos_a - $pos_b; }); return $bands_array; } /** * Get total QSO count for the period */ private function get_total_qsos($locations, $start_date, $end_date) { $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $query = $this->db->get(); $row = $query->row(); return $row ? (int)$row->count : 0; } /** * Get count of unique callsigns worked */ private function get_unique_callsigns($locations, $start_date, $end_date) { $this->db->select('COUNT(DISTINCT COL_CALL) as count', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $query = $this->db->get(); $row = $query->row(); return $row ? (int)$row->count : 0; } /** * Get list of unique callsigns worked with QSO counts */ private function get_callsign_list($locations, $start_date, $end_date) { $this->db->select('COL_CALL, COUNT(*) as qso_count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->group_by('COL_CALL'); $this->db->order_by('qso_count', 'DESC'); $query = $this->db->get(); $callsigns = array(); foreach ($query->result() as $row) { $callsigns[] = array( 'callsign' => $row->COL_CALL, 'qso_count' => (int)$row->qso_count ); } return $callsigns; } /** * Get new DXCC entities worked this month (first time ever) */ private function get_new_dxcc($locations, $start_date, $end_date, $prev_month_end) { $new_dxcc = array(); // Get all unique DXCC entities worked this month (group by DXCC) $this->db->select('COL_DXCC, MAX(COL_COUNTRY) as COL_COUNTRY', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_DXCC IS NOT NULL'); $this->db->where('COL_DXCC !=', ''); $this->db->group_by('COL_DXCC'); $query = $this->db->get(); foreach ($query->result() as $row) { // Check if this DXCC was worked before this month $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_DXCC', $row->COL_DXCC); $this->db->where('COL_TIME_ON <', $start_date); $prev_query = $this->db->get(); $prev_row = $prev_query->row(); // If not worked before, it's new this month if ($prev_row->count == 0) { $new_dxcc[] = array( 'dxcc_id' => $row->COL_DXCC, 'name' => $row->COL_COUNTRY ); } } return $new_dxcc; } /** * Get new DXCC entities by band */ private function get_new_dxcc_by_band($locations, $start_date, $end_date, $prev_month_end) { $new_dxcc_by_band = array(); // Get all bands worked this month, plus propagation mode to separate SAT/EME $this->db->distinct(); $this->db->select('COL_BAND, COL_PROP_MODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_BAND IS NOT NULL'); $this->db->where('COL_BAND !=', ''); $this->db->where('COL_DXCC IS NOT NULL'); $this->db->where('COL_DXCC !=', ''); $bands_query = $this->db->get(); foreach ($bands_query->result() as $band_row) { $band = $band_row->COL_BAND; $prop_mode = $band_row->COL_PROP_MODE; // Group SAT and EME separately, not by frequency band if ($prop_mode == 'SAT') { $band_key = 'Satellite'; } elseif ($prop_mode == 'EME') { $band_key = 'EME'; } else { $band_key = $band; } if (!isset($new_dxcc_by_band[$band_key])) { $new_dxcc_by_band[$band_key] = array(); } // Get all unique DXCC entities worked on this band/prop this month (group by DXCC, get earliest callsign) $this->db->select('COL_DXCC, MAX(COL_COUNTRY) as COL_COUNTRY, MIN(COL_TIME_ON) as FIRST_QSO', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_BAND', $band); $this->db->where('COL_DXCC IS NOT NULL'); $this->db->where('COL_DXCC !=', ''); // Filter by propagation mode if ($prop_mode) { $this->db->where('COL_PROP_MODE', $prop_mode); } else { // For terrestrial, exclude SAT and EME $this->db->where('(COL_PROP_MODE IS NULL OR (COL_PROP_MODE != "SAT" AND COL_PROP_MODE != "EME"))', NULL, FALSE); } $this->db->group_by('COL_DXCC'); $query = $this->db->get(); foreach ($query->result() as $row) { // Check if this DXCC was worked before this month on this band/prop $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_DXCC', $row->COL_DXCC); $this->db->where('COL_BAND', $band); if ($prop_mode) { $this->db->where('COL_PROP_MODE', $prop_mode); } else { $this->db->where('(COL_PROP_MODE IS NULL OR (COL_PROP_MODE != "SAT" AND COL_PROP_MODE != "EME"))', NULL, FALSE); } $this->db->where('COL_TIME_ON <', $start_date); $prev_query = $this->db->get(); $prev_row = $prev_query->row(); // If not worked before on this band/prop, it's new if ($prev_row->count == 0) { // Get the callsign and mode from the first QSO with this DXCC on this band/prop $this->db->select('COL_CALL, COL_MODE, COL_SUBMODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_DXCC', $row->COL_DXCC); $this->db->where('COL_BAND', $band); if ($prop_mode) { $this->db->where('COL_PROP_MODE', $prop_mode); } else { $this->db->where('(COL_PROP_MODE IS NULL OR (COL_PROP_MODE != "SAT" AND COL_PROP_MODE != "EME"))', NULL, FALSE); } $this->db->where('COL_TIME_ON', $row->FIRST_QSO); $this->db->limit(1); $call_query = $this->db->get(); $call_row = $call_query->row(); $mode = ''; if ($call_row) { $mode = !empty($call_row->COL_SUBMODE) ? $call_row->COL_SUBMODE : $call_row->COL_MODE; } // Avoid duplicates in the same band_key $already_added = false; foreach ($new_dxcc_by_band[$band_key] as $existing) { if ($existing['dxcc_id'] == $row->COL_DXCC) { $already_added = true; break; } } if (!$already_added) { $new_dxcc_by_band[$band_key][] = array( 'dxcc_id' => $row->COL_DXCC, 'name' => $row->COL_COUNTRY, 'callsign' => $call_row ? $call_row->COL_CALL : '', 'mode' => $mode ); } } } } // Remove bands with no new DXCC $new_dxcc_by_band = array_filter($new_dxcc_by_band, function($arr) { return !empty($arr); }); // Sort bands in proper amateur radio order $new_dxcc_by_band = $this->sort_bands_array($new_dxcc_by_band); return $new_dxcc_by_band; } /** * Get new DXCC entities by propagation mode (SAT or EME) */ private function get_new_dxcc_by_prop($locations, $start_date, $end_date, $prev_month_end, $prop_mode) { $new_dxcc = array(); // Get all unique DXCC entities worked this month via this prop mode (group by DXCC, get earliest time) $this->db->select('COL_DXCC, MAX(COL_COUNTRY) as COL_COUNTRY, MIN(COL_TIME_ON) as FIRST_QSO', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_PROP_MODE', $prop_mode); $this->db->where('COL_DXCC IS NOT NULL'); $this->db->where('COL_DXCC !=', ''); $this->db->group_by('COL_DXCC'); $query = $this->db->get(); foreach ($query->result() as $row) { // Check if this DXCC was worked before this month via this prop mode $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_DXCC', $row->COL_DXCC); $this->db->where('COL_PROP_MODE', $prop_mode); $this->db->where('COL_TIME_ON <', $start_date); $prev_query = $this->db->get(); $prev_row = $prev_query->row(); // If not worked before via this prop mode, it's new if ($prev_row->count == 0) { // Get the callsign and mode from the first QSO $this->db->select('COL_CALL, COL_MODE, COL_SUBMODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_DXCC', $row->COL_DXCC); $this->db->where('COL_PROP_MODE', $prop_mode); $this->db->where('COL_TIME_ON', $row->FIRST_QSO); $this->db->limit(1); $call_query = $this->db->get(); $call_row = $call_query->row(); $mode = ''; if ($call_row) { $mode = !empty($call_row->COL_SUBMODE) ? $call_row->COL_SUBMODE : $call_row->COL_MODE; } $new_dxcc[] = array( 'dxcc_id' => $row->COL_DXCC, 'name' => $row->COL_COUNTRY, 'callsign' => $call_row ? $call_row->COL_CALL : '', 'mode' => $mode ); } } return $new_dxcc; } /** * Get new gridsquares by band */ private function get_new_gridsquares_by_band($locations, $start_date, $end_date, $prev_month_end) { $new_grids_by_band = array(); // Get all bands worked this month, plus propagation mode to separate SAT/EME $this->db->distinct(); $this->db->select('COL_BAND, COL_PROP_MODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_BAND IS NOT NULL'); $this->db->where('COL_BAND !=', ''); $bands_query = $this->db->get(); foreach ($bands_query->result() as $band_row) { $band = $band_row->COL_BAND; $prop_mode = $band_row->COL_PROP_MODE; // Group SAT and EME separately, not by frequency band if ($prop_mode == 'SAT') { $band_key = 'Satellite'; } elseif ($prop_mode == 'EME') { $band_key = 'EME'; } else { $band_key = $band; } if (!isset($new_grids_by_band[$band_key])) { $new_grids_by_band[$band_key] = array(); } // Get all gridsquares worked on this band/prop this month $this->db->select('COL_CALL, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_TIME_ON, COL_MODE, COL_SUBMODE, COL_SAT_NAME'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_BAND', $band); // Filter by propagation mode if ($prop_mode) { $this->db->where('COL_PROP_MODE', $prop_mode); } else { // For terrestrial, exclude SAT and EME $this->db->where('(COL_PROP_MODE IS NULL OR (COL_PROP_MODE != "SAT" AND COL_PROP_MODE != "EME"))', NULL, FALSE); } $this->db->order_by('COL_TIME_ON', 'ASC'); $query = $this->db->get(); $grid_data = array(); foreach ($query->result() as $row) { $mode = !empty($row->COL_SUBMODE) ? $row->COL_SUBMODE : $row->COL_MODE; // Process main gridsquare (first 4 characters only) if (!empty($row->COL_GRIDSQUARE) && strlen($row->COL_GRIDSQUARE) >= 4) { $grid_4char = strtoupper(substr($row->COL_GRIDSQUARE, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'satellite' => !empty($row->COL_SAT_NAME) ? $row->COL_SAT_NAME : '', 'mode' => $mode, 'time' => $row->COL_TIME_ON ); } } // Process VUCC grids (first 4 characters only) if (!empty($row->COL_VUCC_GRIDS)) { $vucc_grids = explode(',', $row->COL_VUCC_GRIDS); foreach ($vucc_grids as $grid) { $grid = trim($grid); if (strlen($grid) >= 4) { $grid_4char = strtoupper(substr($grid, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'satellite' => !empty($row->COL_SAT_NAME) ? $row->COL_SAT_NAME : '', 'mode' => $mode, 'time' => $row->COL_TIME_ON ); } } } } } // Check each grid to see if it was worked before on this band/prop foreach ($grid_data as $grid => $data) { // Check if this grid was worked before this month on this band/prop $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_BAND', $band); if ($prop_mode) { $this->db->where('COL_PROP_MODE', $prop_mode); } else { $this->db->where('(COL_PROP_MODE IS NULL OR (COL_PROP_MODE != "SAT" AND COL_PROP_MODE != "EME"))', NULL, FALSE); } $this->db->where('COL_TIME_ON <', $start_date); $this->db->group_start(); $this->db->where('UPPER(SUBSTR(COL_GRIDSQUARE, 1, 4)) =', $grid); $this->db->or_like('COL_VUCC_GRIDS', $grid); $this->db->group_end(); $prev_query = $this->db->get(); $prev_row = $prev_query->row(); // If not worked before on this band/prop, it's new if ($prev_row->count == 0) { // Avoid duplicates in the same band_key $already_added = false; foreach ($new_grids_by_band[$band_key] as $existing) { if ($existing['grid'] == $grid) { $already_added = true; break; } } if (!$already_added) { $new_grids_by_band[$band_key][] = array( 'grid' => $grid, 'callsign' => $data['callsign'], 'satellite' => $data['satellite'], 'mode' => $data['mode'] ); } } } } // Remove bands with no new grids $new_grids_by_band = array_filter($new_grids_by_band, function($arr) { return !empty($arr); }); // Sort bands in proper amateur radio order $new_grids_by_band = $this->sort_bands_array($new_grids_by_band); return $new_grids_by_band; } /** * Get new gridsquares worked this month (first time ever) - All modes */ private function get_new_gridsquares($locations, $start_date, $end_date, $prev_month_end) { $new_grids = array(); // Get all gridsquares worked this month (including VUCC grids) $this->db->select('COL_CALL, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_TIME_ON, COL_MODE, COL_SUBMODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->order_by('COL_TIME_ON', 'ASC'); $query = $this->db->get(); $grid_data = array(); foreach ($query->result() as $row) { $mode = !empty($row->COL_SUBMODE) ? $row->COL_SUBMODE : $row->COL_MODE; // Process main gridsquare (first 4 characters only) if (!empty($row->COL_GRIDSQUARE) && strlen($row->COL_GRIDSQUARE) >= 4) { $grid_4char = strtoupper(substr($row->COL_GRIDSQUARE, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'mode' => $mode ); } } // Process VUCC grids (comma-separated, first 4 characters only) if (!empty($row->COL_VUCC_GRIDS)) { $vucc_grids = explode(',', $row->COL_VUCC_GRIDS); foreach ($vucc_grids as $grid) { $grid = trim($grid); if (strlen($grid) >= 4) { $grid_4char = strtoupper(substr($grid, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'mode' => $mode ); } } } } } // Check each grid to see if it's new foreach ($grid_data as $grid => $data) { if ($this->is_grid_new($locations, $grid, $start_date)) { $new_grids[] = array( 'grid' => $grid, 'callsign' => $data['callsign'], 'mode' => $data['mode'] ); } } // Sort alphabetically usort($new_grids, function($a, $b) { return strcmp($a['grid'], $b['grid']); }); return $new_grids; } /** * Get new gridsquares for specific propagation mode */ private function get_new_gridsquares_by_prop($locations, $start_date, $end_date, $prev_month_end, $prop_mode) { $new_grids = array(); // Get all gridsquares worked this month with this prop mode $this->db->select('COL_CALL, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_TIME_ON, COL_SAT_NAME, COL_MODE, COL_SUBMODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_PROP_MODE', $prop_mode); $this->db->order_by('COL_TIME_ON', 'ASC'); $query = $this->db->get(); $grid_data = array(); foreach ($query->result() as $row) { $mode = !empty($row->COL_SUBMODE) ? $row->COL_SUBMODE : $row->COL_MODE; // Process main gridsquare (first 4 characters only) if (!empty($row->COL_GRIDSQUARE) && strlen($row->COL_GRIDSQUARE) >= 4) { $grid_4char = strtoupper(substr($row->COL_GRIDSQUARE, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'satellite' => !empty($row->COL_SAT_NAME) ? $row->COL_SAT_NAME : '', 'mode' => $mode ); } } // Process VUCC grids (first 4 characters only) if (!empty($row->COL_VUCC_GRIDS)) { $vucc_grids = explode(',', $row->COL_VUCC_GRIDS); foreach ($vucc_grids as $grid) { $grid = trim($grid); if (strlen($grid) >= 4) { $grid_4char = strtoupper(substr($grid, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'satellite' => !empty($row->COL_SAT_NAME) ? $row->COL_SAT_NAME : '', 'mode' => $mode ); } } } } } // Check each grid to see if it's new for this prop mode foreach ($grid_data as $grid => $data) { if ($this->is_grid_new_for_prop($locations, $grid, $start_date, $prop_mode)) { $new_grids[] = array( 'grid' => $grid, 'callsign' => $data['callsign'], 'satellite' => $data['satellite'], 'mode' => $data['mode'] ); } } // Sort alphabetically usort($new_grids, function($a, $b) { return strcmp($a['grid'], $b['grid']); }); return $new_grids; } /** * Get new HF gridsquares (non-SAT, non-EME) */ private function get_new_gridsquares_hf($locations, $start_date, $end_date, $prev_month_end) { $new_grids = array(); // Get all gridsquares worked this month via HF $this->db->select('COL_CALL, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_TIME_ON, COL_MODE, COL_SUBMODE'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where("(COL_PROP_MODE IS NULL OR COL_PROP_MODE = '' OR COL_PROP_MODE NOT IN ('SAT','EME'))"); $this->db->order_by('COL_TIME_ON', 'ASC'); $query = $this->db->get(); $grid_data = array(); foreach ($query->result() as $row) { $mode = !empty($row->COL_SUBMODE) ? $row->COL_SUBMODE : $row->COL_MODE; // Process main gridsquare (first 4 characters only) if (!empty($row->COL_GRIDSQUARE) && strlen($row->COL_GRIDSQUARE) >= 4) { $grid_4char = strtoupper(substr($row->COL_GRIDSQUARE, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'mode' => $mode ); } } // Process VUCC grids (first 4 characters only) if (!empty($row->COL_VUCC_GRIDS)) { $vucc_grids = explode(',', $row->COL_VUCC_GRIDS); foreach ($vucc_grids as $grid) { $grid = trim($grid); if (strlen($grid) >= 4) { $grid_4char = strtoupper(substr($grid, 0, 4)); if (!isset($grid_data[$grid_4char])) { $grid_data[$grid_4char] = array( 'callsign' => $row->COL_CALL, 'mode' => $mode ); } } } } } // Check each grid to see if it's new for HF foreach ($grid_data as $grid => $data) { if ($this->is_grid_new_for_hf($locations, $grid, $start_date)) { $new_grids[] = array( 'grid' => $grid, 'callsign' => $data['callsign'], 'mode' => $data['mode'] ); } } // Sort alphabetically usort($new_grids, function($a, $b) { return strcmp($a['grid'], $b['grid']); }); return $new_grids; } /** * Check if a grid is new (never worked before) */ private function is_grid_new($locations, $grid, $start_date) { // Use pre-built cache for instant lookup return !isset($this->historical_grids[strtoupper($grid)]); } /** * Check if a grid is new for a specific propagation mode */ private function is_grid_new_for_prop($locations, $grid, $start_date, $prop_mode) { // Use pre-built cache for instant lookup return !isset($this->historical_grids_by_prop[$prop_mode][strtoupper($grid)]); } /** * Check if a grid is new for HF (non-SAT, non-EME) */ private function is_grid_new_for_hf($locations, $grid, $start_date) { // Use pre-built cache for instant lookup return !isset($this->historical_grids_hf[strtoupper($grid)]); } /** * Get all countries worked this month */ private function get_countries_worked($locations, $start_date, $end_date) { $countries = array(); $this->db->select('COL_DXCC, MAX(COL_COUNTRY) as COL_COUNTRY', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_DXCC IS NOT NULL'); $this->db->where('COL_DXCC !=', ''); $this->db->group_by('COL_DXCC'); $query = $this->db->get(); foreach ($query->result() as $row) { $countries[] = array( 'dxcc_id' => $row->COL_DXCC, 'name' => $row->COL_COUNTRY ); } return $countries; } /** * Get modes breakdown with QSO counts */ private function get_modes_breakdown($locations, $start_date, $end_date) { $modes = array(); // Use COALESCE to prefer submode over mode $this->db->select('COALESCE(NULLIF(COL_SUBMODE, ""), COL_MODE) as mode_used, COUNT(*) as count', FALSE); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->group_by('mode_used'); $this->db->order_by('count', 'DESC'); $query = $this->db->get(); foreach ($query->result() as $row) { $mode = $row->mode_used ? $row->mode_used : 'Unknown'; $modes[$mode] = (int)$row->count; } return $modes; } /** * Get bands breakdown with QSO counts */ private function get_bands_breakdown($locations, $start_date, $end_date) { $bands = array(); $this->db->select('COL_BAND, COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->group_by('COL_BAND'); $query = $this->db->get(); foreach ($query->result() as $row) { $band = $row->COL_BAND ? $row->COL_BAND : 'Unknown'; $bands[$band] = (int)$row->count; } // Sort bands in proper amateur radio order $bands = $this->sort_bands_array($bands); return $bands; } /** * Get continents breakdown with QSO counts */ private function get_continents_breakdown($locations, $start_date, $end_date) { $continents = array(); $this->db->select('COL_CONT, COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_CONT IS NOT NULL'); $this->db->where('COL_CONT !=', ''); $this->db->group_by('COL_CONT'); $this->db->order_by('count', 'DESC'); $query = $this->db->get(); foreach ($query->result() as $row) { $continents[$row->COL_CONT] = (int)$row->count; } return $continents; } /** * Get satellite QSO count (via SAT propagation mode) */ private function get_satellite_qsos($locations, $start_date, $end_date) { $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_PROP_MODE', 'SAT'); $query = $this->db->get(); $row = $query->row(); return $row ? (int)$row->count : 0; } /** * Get breakdown of QSOs per satellite */ private function get_satellite_breakdown($locations, $start_date, $end_date) { $satellites = array(); $this->db->select('COL_SAT_NAME, COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_PROP_MODE', 'SAT'); $this->db->where('COL_SAT_NAME IS NOT NULL'); $this->db->where('COL_SAT_NAME !=', ''); $this->db->group_by('COL_SAT_NAME'); $this->db->order_by('count', 'DESC'); $query = $this->db->get(); foreach ($query->result() as $row) { $satellites[$row->COL_SAT_NAME] = (int)$row->count; } return $satellites; } /** * Get EME (moonbounce) QSO count */ private function get_eme_qsos($locations, $start_date, $end_date) { $this->db->select('COUNT(*) as count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->where('COL_TIME_ON >=', $start_date); $this->db->where('COL_TIME_ON <=', $end_date); $this->db->where('COL_PROP_MODE', 'EME'); $query = $this->db->get(); $row = $query->row(); return $row ? (int)$row->count : 0; } /** * Extract prefix from callsign */ private function extract_prefix($callsign) { // Simple prefix extraction - take everything before first digit if (preg_match('/^([A-Z0-9]+?)\d/', $callsign, $matches)) { return $matches[1]; } return $callsign; } /** * Get list of months with QSO activity * Useful for populating month/year selectors */ public function get_active_months($locations) { $this->db->select('YEAR(COL_TIME_ON) as year, MONTH(COL_TIME_ON) as month, COUNT(*) as qso_count'); $this->db->from($this->config->item('table_name')); $this->db->where_in('station_id', $locations); $this->db->group_by('YEAR(COL_TIME_ON), MONTH(COL_TIME_ON)'); $this->db->order_by('year DESC, month DESC'); $query = $this->db->get(); $months = array(); foreach ($query->result() as $row) { $months[] = array( 'year' => $row->year, 'month' => $row->month, 'qso_count' => $row->qso_count ); } return $months; } }