mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
251 lines
8.8 KiB
PHP
251 lines
8.8 KiB
PHP
<?php if (! defined('BASEPATH')) exit('No direct script access allowed');
|
|
|
|
/**
|
|
* Geojson Library
|
|
*
|
|
* This library provides GeoJSON-based geographic operations for Wavelog,
|
|
* used for determining states, provinces, and other administrative subdivisions
|
|
* from gridsquare locators using point-in-polygon detection.
|
|
*
|
|
* Main functionality:
|
|
* - Convert Maidenhead gridsquares to lat/lng coordinates
|
|
* - Determine state/province from coordinates using GeoJSON boundary data
|
|
* - Point-in-polygon detection for Polygon and MultiPolygon geometries
|
|
*
|
|
*/
|
|
class Geojson {
|
|
|
|
/**
|
|
* DXCC entities that support state/province subdivision lookups
|
|
*
|
|
* Key: DXCC number
|
|
* Value: Array with 'name' and 'enabled' flag
|
|
*/
|
|
const SUPPORTED_STATES = [
|
|
1 => ['name' => 'Canada', 'enabled' => true], // 13 provinces/territories
|
|
227 => ['name' => 'France', 'enabled' => true], // 96 departments
|
|
230 => ['name' => 'Germany', 'enabled' => true], // 16 federal states
|
|
248 => ['name' => 'Italy', 'enabled' => true], // 107 provinces
|
|
263 => ['name' => 'Netherlands', 'enabled' => true], // 12 provinces
|
|
269 => ['name' => 'Poland', 'enabled' => true], // 16 voivodeships
|
|
287 => ['name' => 'Switzerland', 'enabled' => true], // 26 cantons
|
|
291 => ['name' => 'USA', 'enabled' => true], // 52 states/territories
|
|
339 => ['name' => 'Japan', 'enabled' => true], // 47 prefectures
|
|
];
|
|
|
|
private $qra;
|
|
|
|
public function __construct() {
|
|
$CI =& get_instance();
|
|
$CI->load->library('qra');
|
|
$this->qra = $CI->qra;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PUBLIC API METHODS - Main entry points for state lookup
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Find state from grid square locator
|
|
*
|
|
* This is the main method used by the application to determine state/province
|
|
* from a Maidenhead gridsquare.
|
|
*
|
|
* @param string $gridsquare Maidenhead grid square (e.g., "FM18lw")
|
|
* @param int $dxcc DXCC entity number (e.g., 291 for USA)
|
|
* @return array|null State properties (including 'code' and 'name') or null if not found
|
|
*/
|
|
public function findStateFromGridsquare($gridsquare, $dxcc) {
|
|
$coords = $this->gridsquareToLatLng($gridsquare);
|
|
|
|
if ($coords === null) {
|
|
return null;
|
|
}
|
|
|
|
return $this->findStateByDxcc($coords['lat'], $coords['lng'], $dxcc);
|
|
}
|
|
|
|
/**
|
|
* Find state by DXCC entity number and coordinates
|
|
*
|
|
* This method loads the appropriate GeoJSON file for the DXCC entity
|
|
* and searches for the state/province containing the given coordinates.
|
|
*
|
|
* @param float $lat Latitude
|
|
* @param float $lng Longitude
|
|
* @param int $dxcc DXCC entity number (e.g., 291 for USA)
|
|
* @return array|null State properties or null if not found
|
|
*/
|
|
public function findStateByDxcc($lat, $lng, $dxcc) {
|
|
// Check if state lookup is supported for this DXCC
|
|
if (!$this->isStateSupported($dxcc)) {
|
|
return null;
|
|
}
|
|
|
|
$geojsonFile = "assets/json/geojson/states_{$dxcc}.geojson";
|
|
$geojsonData = $this->loadGeoJsonFile($geojsonFile);
|
|
|
|
if ($geojsonData === null) {
|
|
return null;
|
|
}
|
|
|
|
return $this->findFeatureContainingPoint($lat, $lng, $geojsonData);
|
|
}
|
|
|
|
/**
|
|
* Check if state lookup is supported for given DXCC entity
|
|
*
|
|
* @param int $dxcc DXCC entity number
|
|
* @return bool True if state lookup is supported and enabled
|
|
*/
|
|
public function isStateSupported($dxcc) {
|
|
return isset(self::SUPPORTED_STATES[$dxcc]) && self::SUPPORTED_STATES[$dxcc]['enabled'] === true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// COORDINATE CONVERSION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Convert Maidenhead grid square to latitude/longitude
|
|
*
|
|
* Uses the Qra library for gridsquare conversion.
|
|
* Supports 2, 4, 6, 8, and 10 character gridsquares.
|
|
* Also supports grid lines and grid corners (comma-separated).
|
|
*
|
|
* @param string $gridsquare Maidenhead grid square (e.g., "JO70va")
|
|
* @return array|null Array with 'lat' and 'lng' or null on error
|
|
*/
|
|
public function gridsquareToLatLng($gridsquare) {
|
|
if (!is_string($gridsquare) || strlen($gridsquare) < 2) {
|
|
return null;
|
|
}
|
|
|
|
$result = $this->qra->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]
|
|
];
|
|
}
|
|
|
|
// ============================================================================
|
|
// GEOJSON FILE OPERATIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Load and parse a GeoJSON file
|
|
*
|
|
* @param string $filepath Path to GeoJSON file (relative to FCPATH)
|
|
* @return array|null Decoded GeoJSON data or null on error
|
|
*/
|
|
public function loadGeoJsonFile($filepath) {
|
|
$fullpath = FCPATH . $filepath;
|
|
|
|
if (!file_exists($fullpath)) {
|
|
return null;
|
|
}
|
|
|
|
$geojsonData = file_get_contents($fullpath);
|
|
|
|
if ($geojsonData === false) {
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($geojsonData, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GEOMETRIC ALGORITHMS - Point-in-polygon detection
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if a point (latitude, longitude) is inside a polygon
|
|
* Uses 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
|
|
for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) {
|
|
$xi = $polygon[$i][0]; // longitude
|
|
$yi = $polygon[$i][1]; // latitude
|
|
$xj = $polygon[$j][0]; // longitude
|
|
$yj = $polygon[$j][1]; // latitude
|
|
|
|
$intersect = (($yi > $lat) !== ($yj > $lat))
|
|
&& ($lng < ($xj - $xi) * ($lat - $yi) / ($yj - $yi) + $xi);
|
|
|
|
if ($intersect) {
|
|
$inside = !$inside;
|
|
}
|
|
}
|
|
|
|
return $inside;
|
|
}
|
|
|
|
/**
|
|
* Find which feature in a GeoJSON FeatureCollection contains a given point
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
foreach ($geojsonData['features'] as $feature) {
|
|
if (!isset($feature['geometry']['coordinates']) || !isset($feature['geometry']['type'])) {
|
|
continue;
|
|
}
|
|
|
|
$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;
|
|
}
|
|
}
|