mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
[Zonechecker] Added an internal tool
This commit is contained in:
456
application/controllers/zonechecker.php
Normal file
456
application/controllers/zonechecker.php
Normal file
@@ -0,0 +1,456 @@
|
||||
<?php
|
||||
|
||||
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
class Zonechecker extends CI_Controller {
|
||||
private $geojsonFile = null;
|
||||
private $geojsonData = null;
|
||||
private $qralib;
|
||||
private $gridsquareCache = array(); // Cache gridsquare->zone lookups
|
||||
private $spatialIndex = null; // Spatial index for faster lookups
|
||||
private $featureBoundingBoxes = array(); // Pre-calculated bounding boxes
|
||||
|
||||
function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
$this->load->model('user_model');
|
||||
if(!$this->user_model->authorize(99)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
|
||||
}
|
||||
|
||||
public function index() {
|
||||
|
||||
$this->load->model('stations');
|
||||
|
||||
$data['station_profile'] = $this->stations->all_of_user();
|
||||
|
||||
$data['page_title'] = __("Gridsquare Zone finder");
|
||||
$this->load->view('interface_assets/header', $data);
|
||||
$this->load->view('zonechecker/index');
|
||||
$this->load->view('interface_assets/footer');
|
||||
}
|
||||
|
||||
function getQsos($station_id) {
|
||||
$sql = 'select distinct col_country, col_call, col_dxcc, col_time_on, station_profile.station_profile_name, col_primary_key, col_cqz, col_ituz, col_gridsquare
|
||||
from ' . $this->config->item('table_name') . '
|
||||
join station_profile on ' . $this->config->item('table_name') . '.station_id = station_profile.station_id
|
||||
where station_profile.user_id = ?
|
||||
and length(col_gridsquare) >= 6';
|
||||
$params[] = array($this->session->userdata('user_id'));
|
||||
|
||||
if ($station_id && is_numeric($station_id)) {
|
||||
$sql .= ' and ' . $this->config->item('table_name') . '.station_id = ?';
|
||||
$params[] = $station_id;
|
||||
}
|
||||
|
||||
$sql .= ' order by station_profile.station_profile_name asc, col_time_on desc';
|
||||
|
||||
$query = $this->db->query($sql, $params);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
function doWazCheck() {
|
||||
set_time_limit(3600);
|
||||
$de = $this->input->post('de', true);
|
||||
$zoneType = $this->input->post('zoneType', true) ?: 'cq'; // Default to CQ if not specified
|
||||
|
||||
$i = 0;
|
||||
$result = array();
|
||||
$this->gridsquareCache = array(); // Reset cache
|
||||
|
||||
$callarray = $this->getQsos($de)->result();
|
||||
|
||||
// Starting clock time in seconds
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Load appropriate GeoJSON file based on zone type
|
||||
if ($this->geojsonFile === null) {
|
||||
if ($zoneType === 'itu') {
|
||||
$this->geojsonFile = "assets/json/geojson/ituzones.geojson";
|
||||
} else {
|
||||
$this->geojsonFile = "assets/json/geojson/cqzones.geojson";
|
||||
}
|
||||
$this->geojsonData = $this->loadGeoJsonFile($this->geojsonFile);
|
||||
}
|
||||
|
||||
if ($this->geojsonData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hits = 0; // Track cache hits for performance metrics
|
||||
$misses = 0; // Track cache misses
|
||||
|
||||
foreach ($callarray as $qso) {
|
||||
$i++;
|
||||
$gridsquare = $qso->col_gridsquare;
|
||||
|
||||
// Check cache first - avoid redundant gridsquare->zone conversions
|
||||
if (!isset($this->gridsquareCache[$gridsquare])) {
|
||||
$zone = $this->findCqZoneFromGridsquare($gridsquare, $zoneType);
|
||||
$this->gridsquareCache[$gridsquare] = $zone;
|
||||
$misses++;
|
||||
} else {
|
||||
$zone = $this->gridsquareCache[$gridsquare];
|
||||
$hits++;
|
||||
}
|
||||
|
||||
// Check zone based on type
|
||||
if ($zoneType === 'itu') {
|
||||
if (!isset($zone['itu_zone_number'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($qso->col_ituz != $zone['itu_zone_number']) {
|
||||
$result[] = [
|
||||
'id' => $qso->col_primary_key,
|
||||
'qso_date' => $qso->col_time_on,
|
||||
'callsign' => $qso->col_call,
|
||||
'station_profile' => $qso->station_profile_name,
|
||||
'ituzone' => $qso->col_ituz,
|
||||
'gridsquare' => $qso->col_gridsquare,
|
||||
'itugeo' => $zone['itu_zone_number'],
|
||||
'zone_type' => 'ITU',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// CQ Zone (default)
|
||||
if (!isset($zone['cq_zone_number'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($qso->col_cqz != $zone['cq_zone_number']) {
|
||||
$result[] = [
|
||||
'id' => $qso->col_primary_key,
|
||||
'qso_date' => $qso->col_time_on,
|
||||
'callsign' => $qso->col_call,
|
||||
'station_profile' => $qso->station_profile_name,
|
||||
'cqzone' => $qso->col_cqz,
|
||||
'gridsquare' => $qso->col_gridsquare,
|
||||
'cqgeo' => $zone['cq_zone_number'],
|
||||
'zone_type' => 'CQ',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End clock time in seconds
|
||||
$end_time = microtime(true);
|
||||
|
||||
// Calculate script execution time
|
||||
$execution_time = ($end_time - $start_time);
|
||||
|
||||
$data['execution_time'] = $execution_time;
|
||||
$data['calls_tested'] = $i;
|
||||
$data['cache_hits'] = $hits;
|
||||
$data['cache_misses'] = $misses;
|
||||
$data['cache_hit_rate'] = $i > 0 ? round(($hits / $i) * 100, 2) : 0;
|
||||
$data['result'] = $result;
|
||||
$data['zone_type'] = $zoneType;
|
||||
|
||||
$this->loadView($data);
|
||||
}
|
||||
|
||||
public function loadGeoJsonFile($filepath) {
|
||||
$fullpath = FCPATH . $filepath;
|
||||
|
||||
if (!file_exists($fullpath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$geojsonData = file_get_contents($fullpath);
|
||||
|
||||
if ($geojsonData === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove BOM if present (UTF-8, UTF-16, UTF-32)
|
||||
$geojsonData = preg_replace('/^\xEF\xBB\xBF|\xFF\xFE|\xFE\xFF|\x00\x00\xFE\xFF|\xFF\xFE\x00\x00/', '', $geojsonData);
|
||||
|
||||
// Additional cleanup: trim whitespace
|
||||
$geojsonData = trim($geojsonData);
|
||||
|
||||
$data = json_decode($geojsonData, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pre-process: calculate bounding boxes and build spatial index
|
||||
$this->preProcessGeoJson($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-process GeoJSON to calculate bounding boxes and build spatial index
|
||||
* This dramatically speeds up point-in-polygon lookups
|
||||
*/
|
||||
private function preProcessGeoJson(&$geojsonData) {
|
||||
if (!isset($geojsonData['features']) || !is_array($geojsonData['features'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->featureBoundingBoxes = array();
|
||||
$this->spatialIndex = array(
|
||||
'minLng' => 180,
|
||||
'maxLng' => -180,
|
||||
'minLat' => 90,
|
||||
'maxLat' => -90,
|
||||
'grid' => array() // 10x10 degree grid for coarse filtering
|
||||
);
|
||||
|
||||
foreach ($geojsonData['features'] as $index => &$feature) {
|
||||
$bbox = $this->calculateFeatureBoundingBox($feature);
|
||||
|
||||
if ($bbox !== null) {
|
||||
// Store bbox for quick access
|
||||
$feature['bbox'] = $bbox;
|
||||
$this->featureBoundingBoxes[$index] = $bbox;
|
||||
|
||||
// Update global bounds
|
||||
$this->spatialIndex['minLng'] = min($this->spatialIndex['minLng'], $bbox[0]);
|
||||
$this->spatialIndex['maxLng'] = max($this->spatialIndex['maxLng'], $bbox[2]);
|
||||
$this->spatialIndex['minLat'] = min($this->spatialIndex['minLat'], $bbox[1]);
|
||||
$this->spatialIndex['maxLat'] = max($this->spatialIndex['maxLat'], $bbox[3]);
|
||||
|
||||
// Add to spatial grid (10x10 degree cells)
|
||||
$minGridLng = floor($bbox[0] / 10) * 10;
|
||||
$maxGridLng = floor($bbox[2] / 10) * 10;
|
||||
$minGridLat = floor($bbox[1] / 10) * 10;
|
||||
$maxGridLat = floor($bbox[3] / 10) * 10;
|
||||
|
||||
for ($lng = $minGridLng; $lng <= $maxGridLng; $lng += 10) {
|
||||
for ($lat = $minGridLat; $lat <= $maxGridLat; $lat += 10) {
|
||||
$key = $lng . ',' . $lat;
|
||||
if (!isset($this->spatialIndex['grid'][$key])) {
|
||||
$this->spatialIndex['grid'][$key] = array();
|
||||
}
|
||||
$this->spatialIndex['grid'][$key][] = $index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bounding box for a feature
|
||||
* @return array|null [minLng, minLat, maxLng, maxLat] or null
|
||||
*/
|
||||
private function calculateFeatureBoundingBox($feature) {
|
||||
if (!isset($feature['geometry']['coordinates']) || !isset($feature['geometry']['type'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$geometryType = $feature['geometry']['type'];
|
||||
$coordinates = $feature['geometry']['coordinates'];
|
||||
|
||||
$minLng = 180;
|
||||
$maxLng = -180;
|
||||
$minLat = 90;
|
||||
$maxLat = -90;
|
||||
|
||||
if ($geometryType === 'Polygon') {
|
||||
$this->updateBoundsFromCoords($coordinates[0], $minLng, $minLat, $maxLng, $maxLat);
|
||||
} elseif ($geometryType === 'MultiPolygon') {
|
||||
foreach ($coordinates as $polygon) {
|
||||
$this->updateBoundsFromCoords($polygon[0], $minLng, $minLat, $maxLng, $maxLat);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($minLng > $maxLng || $minLat > $maxLat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array($minLng, $minLat, $maxLng, $maxLat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update min/max bounds from coordinate array
|
||||
*/
|
||||
private function updateBoundsFromCoords($coords, &$minLng, &$minLat, &$maxLng, &$maxLat) {
|
||||
foreach ($coords as $point) {
|
||||
$lng = $point[0];
|
||||
$lat = $point[1];
|
||||
$minLng = min($minLng, $lng);
|
||||
$maxLng = max($maxLng, $lng);
|
||||
$minLat = min($minLat, $lat);
|
||||
$maxLat = max($maxLat, $lat);
|
||||
}
|
||||
}
|
||||
|
||||
public function findCqZoneFromGridsquare($gridsquare, $type) {
|
||||
$coords = $this->gridsquareToLatLng($gridsquare);
|
||||
|
||||
if ($coords === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findFeatureContainingPoint($coords['lat'], $coords['lng'], $this->geojsonData);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// GEOMETRIC ALGORITHMS - Point-in-polygon detection
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if a point (latitude, longitude) is inside a polygon
|
||||
* Uses optimized ray casting algorithm
|
||||
*
|
||||
* @param float $lat Latitude of the point
|
||||
* @param float $lng Longitude of the point
|
||||
* @param array $polygon GeoJSON polygon coordinates array [[[lng, lat], [lng, lat], ...]]
|
||||
* @return bool True if point is inside polygon, false otherwise
|
||||
*/
|
||||
public function isPointInPolygon($lat, $lng, $polygon) {
|
||||
if (!is_numeric($lat) || !is_numeric($lng) || !is_array($polygon) || empty($polygon)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$inside = false;
|
||||
$count = count($polygon);
|
||||
|
||||
// Ray casting algorithm - optimized with minimal variable assignments
|
||||
for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) {
|
||||
$xi = $polygon[$i][0];
|
||||
$yi = $polygon[$i][1];
|
||||
$xj = $polygon[$j][0];
|
||||
$yj = $polygon[$j][1];
|
||||
|
||||
$intersect = (($yi > $lat) != ($yj > $lat))
|
||||
&& ($lng < ($xj - $xi) * ($lat - $yi) / ($yj - $yi) + $xi);
|
||||
|
||||
$inside ^= $intersect;
|
||||
}
|
||||
|
||||
return $inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find which feature in a GeoJSON FeatureCollection contains a given point
|
||||
* Optimized with spatial indexing and bounding box pre-checks
|
||||
*
|
||||
* @param float $lat Latitude of the point
|
||||
* @param float $lng Longitude of the point
|
||||
* @param array $geojsonData Decoded GeoJSON FeatureCollection
|
||||
* @return array|null Feature properties if found, null otherwise
|
||||
*/
|
||||
public function findFeatureContainingPoint($lat, $lng, $geojsonData) {
|
||||
if (!isset($geojsonData['features']) || !is_array($geojsonData['features'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Early exit: check global bounds
|
||||
if ($this->spatialIndex !== null) {
|
||||
if ($lng < $this->spatialIndex['minLng'] || $lng > $this->spatialIndex['maxLng'] ||
|
||||
$lat < $this->spatialIndex['minLat'] || $lat > $this->spatialIndex['maxLat']) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use spatial index to get candidate features
|
||||
$candidateIndices = $this->getCandidateFeatures($lat, $lng);
|
||||
|
||||
// If no spatial index, fall back to checking all features
|
||||
if ($candidateIndices === null) {
|
||||
$candidateIndices = array_keys($geojsonData['features']);
|
||||
}
|
||||
|
||||
// Check only candidate features
|
||||
foreach ($candidateIndices as $index) {
|
||||
if (!isset($geojsonData['features'][$index])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$feature = $geojsonData['features'][$index];
|
||||
|
||||
if (!isset($feature['geometry']['coordinates']) || !isset($feature['geometry']['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fast bounding box check (always available now due to pre-processing)
|
||||
if (isset($feature['bbox'])) {
|
||||
$bbox = $feature['bbox'];
|
||||
if ($lng < $bbox[0] || $lng > $bbox[2] || $lat < $bbox[1] || $lat > $bbox[3]) {
|
||||
continue; // Point is outside bounding box, skip detailed check
|
||||
}
|
||||
}
|
||||
|
||||
$geometryType = $feature['geometry']['type'];
|
||||
$coordinates = $feature['geometry']['coordinates'];
|
||||
|
||||
// Handle Polygon geometry
|
||||
if ($geometryType === 'Polygon') {
|
||||
// For Polygon, coordinates[0] is the outer ring
|
||||
if ($this->isPointInPolygon($lat, $lng, $coordinates[0])) {
|
||||
return $feature['properties'];
|
||||
}
|
||||
}
|
||||
// Handle MultiPolygon geometry
|
||||
elseif ($geometryType === 'MultiPolygon') {
|
||||
foreach ($coordinates as $polygon) {
|
||||
// For MultiPolygon, each polygon is [[[lng,lat],...]]
|
||||
// We need to pass just the outer ring (first element)
|
||||
if ($this->isPointInPolygon($lat, $lng, $polygon[0])) {
|
||||
return $feature['properties'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get candidate features that might contain the point using spatial index
|
||||
* @return array|null Array of feature indices or null if index not available
|
||||
*/
|
||||
private function getCandidateFeatures($lat, $lng) {
|
||||
if ($this->spatialIndex === null || !isset($this->spatialIndex['grid'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find which grid cell the point falls into
|
||||
$gridLng = floor($lng / 10) * 10;
|
||||
$gridLat = floor($lat / 10) * 10;
|
||||
$key = $gridLng . ',' . $gridLat;
|
||||
|
||||
if (isset($this->spatialIndex['grid'][$key])) {
|
||||
return $this->spatialIndex['grid'][$key];
|
||||
}
|
||||
|
||||
// If not found in grid, return null (fallback to all features)
|
||||
return null;
|
||||
}
|
||||
|
||||
public function gridsquareToLatLng($gridsquare) {
|
||||
if (!$this->qralib) {
|
||||
$this->load->library('Qra');
|
||||
$this->qralib = $this->qra;
|
||||
}
|
||||
|
||||
if (!is_string($gridsquare) || strlen($gridsquare) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = $this->qralib->qra2latlong($gridsquare);
|
||||
|
||||
if ($result === false || !is_array($result) || count($result) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Qra library returns [lat, lng], we need to return associative array
|
||||
return [
|
||||
'lat' => $result[0],
|
||||
'lng' => $result[1]
|
||||
];
|
||||
}
|
||||
|
||||
function loadView($data) {
|
||||
$this->load->view('zonechecker/result', $data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -531,6 +531,18 @@
|
||||
|
||||
<li><a class="dropdown-item" href="<?php echo site_url('api'); ?>" title="Manage API keys"><i class="fas fa-key"></i> <?= __("API Keys"); ?></a></li>
|
||||
<li><a class="dropdown-item" href="<?php echo site_url('radio'); ?>" title="Interface with one or more radios"><i class="fas fa-broadcast-tower"></i> <?= __("Hardware Interfaces"); ?></a></li>
|
||||
|
||||
<?php if (($this->config->item('internal_tools') ?? false)) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<li><a class="dropdown-item dropdown-toggle dropdown-toggle-submenu" data-bs-toggle="dropdown"><i class="fas fa-sync"></i> <?= __("Internal tools"); ?></a>
|
||||
<ul class="submenu submenu-left dropdown-menu">
|
||||
<li><a class="dropdown-item" href="<?php echo site_url('calltester'); ?>" title="Callsign DXCC checker"><i class="fas fa-globe-europe"></i> <?= __("Callsign DXCC checker"); ?></a></li>
|
||||
<li><a class="dropdown-item" href="<?php echo site_url('map/qso_map'); ?>" title="GeoJSON QSO Map"><i class="fas fa-globe-europe"></i> <?= __("GeoJSON QSO Map"); ?></a></li>
|
||||
<li><a class="dropdown-item" href="<?php echo site_url('zonechecker'); ?>" title="Gridsquare Zone checker"><i class="fas fa-globe-europe"></i> <?= __("Gridsquare Zone checker"); ?></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<li><a class="dropdown-item" href="javascript:displayVersionDialog();" title="Version Information"><i class="fas fa-star"></i> <?= __("Version Info"); ?></a></li>
|
||||
<li><a class="dropdown-item" target="_blank" href="https://github.com/wavelog/wavelog/wiki" title="Help"><i class="fas fa-question"></i> <?= __("Help"); ?></a></li>
|
||||
|
||||
86
application/views/zonechecker/index.php
Normal file
86
application/views/zonechecker/index.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<div class="container">
|
||||
<br />
|
||||
<h5><?= __("Gridsquare Zone identification"); ?></h5>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<label class="me-2" for="de"><?= __("Station Location"); ?></label>
|
||||
<select class="form-select form-select-sm w-auto me-2" id="de" name="de">
|
||||
<option value="all">All</option>
|
||||
<?php foreach ($station_profile->result() as $station) { ?>
|
||||
<option value="<?php echo $station->station_id; ?>">
|
||||
<?= __("Callsign: ") . " " ?>
|
||||
<?php echo str_replace("0", "Ø", strtoupper($station->station_callsign)); ?> (<?php echo $station->station_profile_name; ?>)
|
||||
</option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<label class="me-2" for="zoneType"><?= __("Zone Type"); ?></label>
|
||||
<select class="form-select form-select-sm w-auto me-2" id="zoneType" name="zoneType">
|
||||
<option value="cq"><?= __("CQ Zone"); ?></option>
|
||||
<option value="itu"><?= __("ITU Zone"); ?></option>
|
||||
</select>
|
||||
<button id="startDxccCheck" class="btn btn-primary btn-sm"><?= __("Start Zone Check"); ?></button>
|
||||
</div>
|
||||
<div class='result'>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check if jQuery is loaded, if not wait for it
|
||||
if (typeof $ === 'undefined') {
|
||||
// jQuery not yet loaded, add event listener
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof $ === 'undefined') {
|
||||
// Wait for jQuery to load
|
||||
var checkJQuery = setInterval(function() {
|
||||
if (typeof $ !== 'undefined') {
|
||||
clearInterval(checkJQuery);
|
||||
$('#startDxccCheck').on('click', function() {
|
||||
let de = $('#de').val();
|
||||
let zoneType = $('#zoneType').val();
|
||||
$('.result').html('<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div> <?= __("Processing...") ?>');
|
||||
$.ajax({
|
||||
url: site_url + '/zonechecker/doWazCheck',
|
||||
type: "POST",
|
||||
data: {de: de,
|
||||
zoneType: zoneType
|
||||
},
|
||||
success: function(response) {
|
||||
$('.result').html(response);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$('.result').html('<div class="alert alert-danger" role="alert"><?= __("An error occurred while processing the request.") ?></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
$('#startDxccCheck').on('click', function() {
|
||||
let de = $('#de').val();
|
||||
let zoneType = $('#zoneType').val();
|
||||
$('.result').html('<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div> <?= __("Processing...") ?>');
|
||||
$.ajax({
|
||||
url: site_url + '/zonechecker/doWazCheck',
|
||||
type: "POST",
|
||||
data: {de: de,
|
||||
zoneType: zoneType
|
||||
},
|
||||
success: function(response) {
|
||||
$('.result').html(response);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$('.result').html('<div class="alert alert-danger" role="alert"><?= __("An error occurred while processing the request.") ?></div>');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// jQuery already loaded
|
||||
$(document).ready(function() {
|
||||
initMap();
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
84
application/views/zonechecker/result.php
Normal file
84
application/views/zonechecker/result.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
$i = 0;
|
||||
|
||||
// Determine zone type from data (default to CQ)
|
||||
$zone_type = isset($zone_type) ? $zone_type : 'cq';
|
||||
$is_itu = ($zone_type === 'itu');
|
||||
$zone_label = $is_itu ? 'ITU' : 'CQ';
|
||||
|
||||
// Calculate color for cache hit rate
|
||||
$cache_color = $cache_hit_rate >= 70 ? 'success' : ($cache_hit_rate >= 40 ? 'warning' : 'danger');
|
||||
|
||||
// Compact statistics row
|
||||
echo '<div class="row mb-3 g-2">';
|
||||
|
||||
$stats = [
|
||||
['label' => __("Callsigns Tested"), 'value' => $calls_tested, 'color' => 'primary'],
|
||||
['label' => __("Execution Time"), 'value' => round($execution_time, 2) . 's', 'color' => 'info'],
|
||||
['label' => __("Potential Wrong Zones"), 'value' => count($result), 'color' => 'warning'],
|
||||
['label' => __("Cache Hits"), 'value' => $cache_hits, 'color' => 'success'],
|
||||
['label' => __("Cache Misses"), 'value' => $cache_misses, 'color' => 'info'],
|
||||
['label' => __("Hit Rate"), 'value' => $cache_hit_rate . '%', 'color' => $cache_color],
|
||||
];
|
||||
|
||||
foreach ($stats as $stat) {
|
||||
echo '<div class="col-6 col-md-2">
|
||||
<div class="card border-' . $stat['color'] . ' text-center py-2">
|
||||
<div class="h5 mb-0 text-' . $stat['color'] . '">' . $stat['value'] . '</div>
|
||||
<small class="text-muted">' . $stat['label'] . '</small>
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
|
||||
// Get Date format
|
||||
if($this->session->userdata('user_date_format')) {
|
||||
// If Logged in and session exists
|
||||
$custom_date_format = $this->session->userdata('user_date_format');
|
||||
} else {
|
||||
// Get Default date format from /config/wavelog.php
|
||||
$custom_date_format = $this->config->item('qso_date_format');
|
||||
}
|
||||
|
||||
if ($result) { ?>
|
||||
<div class="table-responsive" style="max-height:70vh; overflow:auto;">
|
||||
<table class="table table-sm table-striped table-bordered table-condensed mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th><?= __("Callsign"); ?></th>
|
||||
<th><?= __("QSO Date"); ?></th>
|
||||
<th><?= __("Station Profile"); ?></th>
|
||||
<th><?= __("Gridsquare"); ?></th>
|
||||
<?php if ($is_itu): ?>
|
||||
<th><?= __("ITUz"); ?></th>
|
||||
<th><?= __("ITUz geojson"); ?></th>
|
||||
<?php else: ?>
|
||||
<th><?= __("CQz"); ?></th>
|
||||
<th><?= __("CQz geojson"); ?></th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($result as $qso): ?>
|
||||
<tr>
|
||||
<td><?php echo ++$i; ?></td>
|
||||
<td><?php echo '<a id="edit_qso" href="javascript:displayQso(' . $qso['id'] . ')">' . htmlspecialchars($qso['callsign']) . '</a>'; ?></td>
|
||||
<td><?php echo date($custom_date_format, strtotime($qso['qso_date'])); ?></td>
|
||||
<td><?php echo $qso['station_profile']; ?></td>
|
||||
<td><?php echo $qso['gridsquare']; ?></td>
|
||||
<?php if ($is_itu): ?>
|
||||
<td><?php echo $qso['ituzone']; ?></td>
|
||||
<td><?php echo $qso['itugeo']; ?></td>
|
||||
<?php else: ?>
|
||||
<td><?php echo $qso['cqzone']; ?></td>
|
||||
<td><?php echo $qso['cqgeo']; ?></td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php } ?>
|
||||
Reference in New Issue
Block a user