mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
Static Map API (#1098)
Added new Static Map Feature --------- Co-authored-by: phl0 <github@florian-wolters.de>
This commit is contained in:
@@ -184,7 +184,7 @@ class adif extends CI_Controller {
|
|||||||
$this->load->view('adif/import', $data);
|
$this->load->view('adif/import', $data);
|
||||||
$this->load->view('interface_assets/footer');
|
$this->load->view('interface_assets/footer');
|
||||||
} else {
|
} else {
|
||||||
if ($this->stations->check_station_is_accessible($this->input->post('station_profile'))) {
|
if ($this->stations->check_station_is_accessible($this->input->post('station_profile', TRUE))) {
|
||||||
$contest=$this->security->xss_clean($this->input->post('contest')) ?? '';
|
$contest=$this->security->xss_clean($this->input->post('contest')) ?? '';
|
||||||
$stopnow=false;
|
$stopnow=false;
|
||||||
$fdata = array('upload_data' => $this->upload->data());
|
$fdata = array('upload_data' => $this->upload->data());
|
||||||
@@ -236,7 +236,7 @@ class adif extends CI_Controller {
|
|||||||
};
|
};
|
||||||
$record=''; // free memory
|
$record=''; // free memory
|
||||||
try {
|
try {
|
||||||
$custom_errors = $this->logbook_model->import_bulk($alladif, $this->input->post('station_profile'), $this->input->post('skipDuplicate'), $this->input->post('markClublog'),$this->input->post('markLotw'), $this->input->post('dxccAdif'), $this->input->post('markQrz'), $this->input->post('markEqsl'), $this->input->post('markHrd'), true, $this->input->post('operatorName'), false, $this->input->post('skipStationCheck'));
|
$custom_errors = $this->logbook_model->import_bulk($alladif, $this->input->post('station_profile', TRUE), $this->input->post('skipDuplicate'), $this->input->post('markClublog'),$this->input->post('markLotw'), $this->input->post('dxccAdif'), $this->input->post('markQrz'), $this->input->post('markEqsl'), $this->input->post('markHrd'), true, $this->input->post('operatorName'), false, $this->input->post('skipStationCheck'));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
log_message('error', 'Import error: '.$e->getMessage());
|
log_message('error', 'Import error: '.$e->getMessage());
|
||||||
$data['page_title'] = __("ADIF Import failed!");
|
$data['page_title'] = __("ADIF Import failed!");
|
||||||
@@ -256,6 +256,12 @@ class adif extends CI_Controller {
|
|||||||
$custom_errors=__("Station Profile not valid for User");
|
$custom_errors=__("Station Profile not valid for User");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lets clean up static maps cache for this station
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image($this->input->post('station_profile', TRUE));
|
||||||
|
|
||||||
log_message("Error","ADIF End");
|
log_message("Error","ADIF End");
|
||||||
$data['adif_errors'] = $custom_errors;
|
$data['adif_errors'] = $custom_errors;
|
||||||
$data['skip_dupes'] = $this->input->post('skipDuplicate');
|
$data['skip_dupes'] = $this->input->post('skipDuplicate');
|
||||||
|
|||||||
165
application/controllers/Staticmap.php
Normal file
165
application/controllers/Staticmap.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||||
|
|
||||||
|
|
||||||
|
class Staticmap extends CI_Controller {
|
||||||
|
|
||||||
|
public function render($slug = '') {
|
||||||
|
|
||||||
|
// set to true to remove cached imaged for debugging pruposes
|
||||||
|
$debugging = false;
|
||||||
|
if (ENVIRONMENT == 'development') {
|
||||||
|
$debugging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
$this->load->model('stationsetup_model');
|
||||||
|
$this->load->model('visitor_model');
|
||||||
|
|
||||||
|
$slug = $this->security->xss_clean($slug);
|
||||||
|
if ($slug == '') {
|
||||||
|
show_404(__("Unknown Public Page."));
|
||||||
|
}
|
||||||
|
// check if the public slug exists
|
||||||
|
$logbook_id = $this->stationsetup_model->public_slug_exists_logbook_id($slug);
|
||||||
|
if ($logbook_id == false) {
|
||||||
|
show_404(__("Unknown Public Page."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional override-parameters
|
||||||
|
$band = $this->input->get('band', TRUE) ?? 'nbf';
|
||||||
|
$continent = $this->input->get('continent', TRUE) ?? 'nC';
|
||||||
|
$thememode = $this->input->get('theme', TRUE) ?? null;
|
||||||
|
$hide_home = $this->input->get('hide_home', TRUE) == 1 ? true : false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on Export Settings -> Overlays and QSO Count
|
||||||
|
*/
|
||||||
|
// qsocount
|
||||||
|
$qsocount = $this->input->get('qsocount', TRUE) ?? '';
|
||||||
|
// if the qso count is not a number, set it to the user option or 250 per default (same as used in stationsetup)
|
||||||
|
$uid = $this->stationsetup_model->getContainer($logbook_id, false)->row()->user_id;
|
||||||
|
if ($qsocount == 0 || !is_numeric($qsocount)) {
|
||||||
|
$qsocount = $this->user_options_model->get_options('ExportMapOptions', array('option_name' => 'qsocount', 'option_key' => $slug), $uid)->row()->option_value ?? 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Night shadow
|
||||||
|
$night_shadow = $this->input->get('ns', TRUE) ?? '';
|
||||||
|
if ($night_shadow == '' || ($night_shadow != 1 && $night_shadow != 0)) {
|
||||||
|
$r = $this->user_options_model->get_options('ExportMapOptions', array('option_name' => 'nightshadow_layer', 'option_key' => $slug), $uid)->row()->option_value ?? '';
|
||||||
|
$night_shadow = $r == 'true' ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathlines
|
||||||
|
$pathlines = $this->input->get('pl', TRUE) ?? '';
|
||||||
|
if ($pathlines == '' || ($pathlines != 1 && $pathlines != 0)) {
|
||||||
|
$r = $this->user_options_model->get_options('ExportMapOptions', array('option_name' => 'path_lines', 'option_key' => $slug), $uid)->row()->option_value ?? '';
|
||||||
|
$pathlines = $r == 'true' ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CQ Zones
|
||||||
|
$cqzones = $this->input->get('cqz', TRUE) ?? '';
|
||||||
|
if ($cqzones == '' || ($cqzones != 1 && $cqzones != 0)) {
|
||||||
|
$r = $this->user_options_model->get_options('ExportMapOptions', array('option_name' => 'cqzones_layer', 'option_key' => $slug), $uid)->row()->option_value ?? '';
|
||||||
|
$cqzones = $r == 'true' ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ITU Zones
|
||||||
|
$ituzones = $this->input->get('ituz', TRUE) ?? '';
|
||||||
|
if ($ituzones == '' || ($ituzones != 1 && $ituzones != 0)) {
|
||||||
|
$r = $this->user_options_model->get_options('ExportMapOptions', array('option_name' => 'ituzones_layer', 'option_key' => $slug), $uid)->row()->option_value ?? '';
|
||||||
|
$ituzones = $r == 'true' ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handling the theme mode
|
||||||
|
$this->load->model('themes_model');
|
||||||
|
if ($thememode == null || $thememode == '' || ($thememode != 'dark' && $thememode != 'light')) {
|
||||||
|
$r = $this->themes_model->get_theme_mode($this->optionslib->get_option('option_theme'));
|
||||||
|
$thememode = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare the cache directory
|
||||||
|
$cachepath = $this->config->item('cache_path') == '' ? APPPATH . 'cache/' : $this->config->item('cache_path');
|
||||||
|
$cacheDir = $cachepath . "staticmap_images/";
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need the realpath later for validation
|
||||||
|
$cacheDir = realpath($cachepath . "staticmap_images/");
|
||||||
|
|
||||||
|
// create a unique filename for the cache
|
||||||
|
$filenameRaw = $uid . $logbook_id . $qsocount . $band . $thememode . $continent . $hide_home . ($night_shadow == false ? 0 : 1) . ($pathlines == false ? 0 : 1) . ($cqzones == false ? 0 : 1) . ($ituzones == false ? 0 : 1);
|
||||||
|
$filename = crc32('staticmap_' . $slug) . '_' . substr(md5($filenameRaw), 0, 12) . '.png';
|
||||||
|
$filepath = $cacheDir . '/' . $filename;
|
||||||
|
|
||||||
|
// Set the cache time to 7 days
|
||||||
|
$maxAge = 3600 * 24 * 7;
|
||||||
|
|
||||||
|
// remove the cached image for debugging purposes
|
||||||
|
if ($debugging) {
|
||||||
|
if (is_file($filepath)) {
|
||||||
|
unlink($filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->staticmap_model->validate_cached_image($filepath, $cacheDir, $maxAge, $slug)) {
|
||||||
|
log_message('debug', 'Static map image found in cache: ' . $filename);
|
||||||
|
header('Content-Type: image/png');
|
||||||
|
readfile($filepath);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (in_array('gd', get_loaded_extensions())) {
|
||||||
|
|
||||||
|
if ($logbook_id != false) {
|
||||||
|
// Get associated station locations for mysql queries
|
||||||
|
$logbooks_locations_array = $this->stationsetup_model->get_container_relations($logbook_id);
|
||||||
|
|
||||||
|
if (!$logbooks_locations_array) {
|
||||||
|
show_404(__("Empty Logbook"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message('error', $slug . ' has no associated station locations');
|
||||||
|
show_404(__("Unknown Public Page."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to get an array of all coordinates of the stations
|
||||||
|
if (!$this->load->is_loaded('logbook_model')) {
|
||||||
|
$this->load->model('logbook_model');
|
||||||
|
}
|
||||||
|
$grids = [];
|
||||||
|
foreach ($logbooks_locations_array as $location) {
|
||||||
|
$station_info = $this->logbook_model->check_station($location);
|
||||||
|
if ($station_info) {
|
||||||
|
$grids[] = $station_info['station_gridsquare'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$this->load->is_loaded('Qra')) {
|
||||||
|
$this->load->library('Qra');
|
||||||
|
}
|
||||||
|
$coordinates = [];
|
||||||
|
foreach ($grids as $grid) {
|
||||||
|
$coordinates[] = $this->qra->qra2latlong($grid);
|
||||||
|
}
|
||||||
|
$centerMap = $this->qra->getCenterLatLng($coordinates);
|
||||||
|
|
||||||
|
$qsos = $this->visitor_model->get_qsos($qsocount, $logbooks_locations_array, $band == 'nbf' ? '' : $band, $continent == 'nC' ? '' : $continent); // TODO: Allow 'all' option
|
||||||
|
|
||||||
|
$image = $this->staticmap_model->render_static_map($qsos, $uid, $centerMap, $coordinates, $filepath, $continent, $thememode, $hide_home, $night_shadow, $pathlines, $cqzones, $ituzones);
|
||||||
|
|
||||||
|
header('Content-Type: image/png');
|
||||||
|
|
||||||
|
if ($image == false) {
|
||||||
|
$msg = "Can't create static map image. Something went wrong.";
|
||||||
|
log_message('error', $msg);
|
||||||
|
show_404($msg);
|
||||||
|
} else {
|
||||||
|
readfile($filepath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$msg = "Can't create static map image. Extention 'php-gd' is not installed. Install it and restart the webserver.";
|
||||||
|
log_message('error', $msg);
|
||||||
|
echo $msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,8 +44,8 @@ class Station extends CI_Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit($id)
|
public function edit($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
if ($this->stations->check_station_is_accessible($id)) {
|
if ($this->stations->check_station_is_accessible($id)) {
|
||||||
$data = $this->load_station_for_editing($id);
|
$data = $this->load_station_for_editing($id);
|
||||||
@@ -60,6 +60,11 @@ class Station extends CI_Controller
|
|||||||
if ($this->stations->edit()) {
|
if ($this->stations->edit()) {
|
||||||
$data['notice'] = __("Station Location") . $this->security->xss_clean($this->input->post('station_profile_name', true)) . " Updated";
|
$data['notice'] = __("Station Location") . $this->security->xss_clean($this->input->post('station_profile_name', true)) . " Updated";
|
||||||
}
|
}
|
||||||
|
// Also clean up static map images first
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image($id);
|
||||||
redirect('stationsetup');
|
redirect('stationsetup');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -67,8 +72,8 @@ class Station extends CI_Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function copy($id)
|
public function copy($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
if ($this->stations->check_station_is_accessible($id)) {
|
if ($this->stations->check_station_is_accessible($id)) {
|
||||||
$data = $this->load_station_for_editing($id);
|
$data = $this->load_station_for_editing($id);
|
||||||
@@ -93,16 +98,16 @@ class Station extends CI_Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit_favourite($id)
|
public function edit_favourite($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
$this->stations->edit_favourite($id);
|
$this->stations->edit_favourite($id);
|
||||||
|
|
||||||
redirect('stationsetup');
|
redirect('stationsetup');
|
||||||
}
|
}
|
||||||
|
|
||||||
function load_station_for_editing($id): array
|
function load_station_for_editing($id): array {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->library('form_validation');
|
$this->load->library('form_validation');
|
||||||
|
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
@@ -123,8 +128,8 @@ class Station extends CI_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function reassign_profile($id)
|
function reassign_profile($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
// $id is the profile that needs reassigned to QSOs // ONLY Admin can do that!
|
// $id is the profile that needs reassigned to QSOs // ONLY Admin can do that!
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
if ($this->user_model->authorize(99)) {
|
if ($this->user_model->authorize(99)) {
|
||||||
@@ -135,8 +140,9 @@ class Station extends CI_Controller
|
|||||||
redirect('stationsetup');
|
redirect('stationsetup');
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_active($current, $new, $is_ajax = null)
|
function set_active($current, $new, $is_ajax = null) {
|
||||||
{
|
$current = $this->security->xss_clean($current);
|
||||||
|
$new = $this->security->xss_clean($new);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
$this->stations->set_active($current, $new);
|
$this->stations->set_active($current, $new);
|
||||||
|
|
||||||
@@ -147,8 +153,8 @@ class Station extends CI_Controller
|
|||||||
redirect('stationsetup');
|
redirect('stationsetup');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete($id)
|
public function delete($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
if ($this->stations->check_station_is_accessible($id)) {
|
if ($this->stations->check_station_is_accessible($id)) {
|
||||||
$this->stations->delete($id);
|
$this->stations->delete($id);
|
||||||
@@ -156,8 +162,8 @@ class Station extends CI_Controller
|
|||||||
redirect('stationsetup');
|
redirect('stationsetup');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deletelog($id)
|
public function deletelog($id) {
|
||||||
{
|
$id = $this->security->xss_clean($id);
|
||||||
$this->load->model('stations');
|
$this->load->model('stations');
|
||||||
if ($this->stations->check_station_is_accessible($id)) {
|
if ($this->stations->check_station_is_accessible($id)) {
|
||||||
$this->stations->deletelog($id);
|
$this->stations->deletelog($id);
|
||||||
|
|||||||
@@ -459,8 +459,7 @@ class Visitor extends CI_Controller {
|
|||||||
|
|
||||||
$this->load->model('stationsetup_model');
|
$this->load->model('stationsetup_model');
|
||||||
$logbook_id = $this->stationsetup_model->public_slug_exists_logbook_id($slug);
|
$logbook_id = $this->stationsetup_model->public_slug_exists_logbook_id($slug);
|
||||||
if ($logbook_id != false)
|
if ($logbook_id != false) {
|
||||||
{
|
|
||||||
// Get associated station locations for mysql queries
|
// Get associated station locations for mysql queries
|
||||||
$logbooks_locations_array = $this->stationsetup_model->get_container_relations($logbook_id);
|
$logbooks_locations_array = $this->stationsetup_model->get_container_relations($logbook_id);
|
||||||
|
|
||||||
@@ -474,7 +473,7 @@ class Visitor extends CI_Controller {
|
|||||||
|
|
||||||
$qsos = $this->visitor_model->get_qsos($qsocount, $logbooks_locations_array, $band);
|
$qsos = $this->visitor_model->get_qsos($qsocount, $logbooks_locations_array, $band);
|
||||||
$userid = $this->stationsetup_model->public_slug_exists_userid($slug);
|
$userid = $this->stationsetup_model->public_slug_exists_userid($slug);
|
||||||
$user_default_confirmation = $this->get_user_default_confirmation($userid);
|
$user_default_confirmation = $this->visitor_model->get_user_default_confirmation($userid);
|
||||||
|
|
||||||
$mappedcoordinates = array();
|
$mappedcoordinates = array();
|
||||||
foreach ($qsos->result('array') as $qso) {
|
foreach ($qsos->result('array') as $qso) {
|
||||||
@@ -498,6 +497,7 @@ class Visitor extends CI_Controller {
|
|||||||
$this->load->library('Qra');
|
$this->load->library('Qra');
|
||||||
}
|
}
|
||||||
$this->load->model('logbook_model');
|
$this->load->model('logbook_model');
|
||||||
|
$this->load->model('visitor_model');
|
||||||
|
|
||||||
$latlng1 = $this->qra->qra2latlong($locator1);
|
$latlng1 = $this->qra->qra2latlong($locator1);
|
||||||
$latlng2 = $this->qra->qra2latlong($locator2);
|
$latlng2 = $this->qra->qra2latlong($locator2);
|
||||||
@@ -508,7 +508,7 @@ class Visitor extends CI_Controller {
|
|||||||
|
|
||||||
$data['latlng1'] = $latlng1;
|
$data['latlng1'] = $latlng1;
|
||||||
$data['latlng2'] = $latlng2;
|
$data['latlng2'] = $latlng2;
|
||||||
$data['confirmed'] = ($this->qso_is_confirmed($qso, $user_default_confirmation)==true) ? true : false;
|
$data['confirmed'] = ($this->visitor_model->qso_is_confirmed($qso, $user_default_confirmation)==true) ? true : false;
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
@@ -518,6 +518,7 @@ class Visitor extends CI_Controller {
|
|||||||
$this->load->library('Qra');
|
$this->load->library('Qra');
|
||||||
}
|
}
|
||||||
$this->load->model('logbook_model');
|
$this->load->model('logbook_model');
|
||||||
|
$this->load->model('visitor_model');
|
||||||
|
|
||||||
$latlng1 = $this->qra->qra2latlong($mygrid);
|
$latlng1 = $this->qra->qra2latlong($mygrid);
|
||||||
$latlng2[0] = $lat;
|
$latlng2[0] = $lat;
|
||||||
@@ -529,39 +530,8 @@ class Visitor extends CI_Controller {
|
|||||||
|
|
||||||
$data['latlng1'] = $latlng1;
|
$data['latlng1'] = $latlng1;
|
||||||
$data['latlng2'] = $latlng2;
|
$data['latlng2'] = $latlng2;
|
||||||
$data['confirmed'] = ($this->qso_is_confirmed($qso, $user_default_confirmation)==true) ? true : false;
|
$data['confirmed'] = ($this->visitor_model->qso_is_confirmed($qso, $user_default_confirmation)==true) ? true : false;
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function qso_is_confirmed($qso, $user_default_confirmation) {
|
|
||||||
$confirmed = false;
|
|
||||||
$qso = (array) $qso;
|
|
||||||
if (strpos($user_default_confirmation, 'Q') !== false) { // QSL
|
|
||||||
if ($qso['COL_QSL_RCVD']=='Y') { $confirmed = true; }
|
|
||||||
}
|
|
||||||
if (strpos($user_default_confirmation, 'L') !== false) { // LoTW
|
|
||||||
if ($qso['COL_LOTW_QSL_RCVD']=='Y') { $confirmed = true; }
|
|
||||||
}
|
|
||||||
if (strpos($user_default_confirmation, 'E') !== false) { // eQsl
|
|
||||||
if ($qso['COL_EQSL_QSL_RCVD']=='Y') { $confirmed = true; }
|
|
||||||
}
|
|
||||||
if (strpos($user_default_confirmation, 'Z') !== false) { // QRZ
|
|
||||||
if ($qso['COL_QRZCOM_QSO_DOWNLOAD_STATUS']=='Y') { $confirmed = true; }
|
|
||||||
}
|
|
||||||
return $confirmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_user_default_confirmation($userid) {
|
|
||||||
$this->db->where('user_id', $userid);
|
|
||||||
$query = $this->db->get('users');
|
|
||||||
|
|
||||||
if ($query->num_rows() > 0){
|
|
||||||
foreach ($query->result() as $row) {
|
|
||||||
return $row->user_default_confirmation;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,4 +134,125 @@ class Genfunctions
|
|||||||
return $flag;
|
return $flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to convert a FontAwesome icon to a PNG image and returns filename if successful (or already exists)
|
||||||
|
* Source: https://github.com/sperelson/Awesome2PNG
|
||||||
|
* Modified by HB9HIL
|
||||||
|
*
|
||||||
|
* @param string $unicode Unicode of the FontAwesome icon (e.g. f0c8) - required
|
||||||
|
* @param string $color Hexadecimal color of the icon (default: ffffff)
|
||||||
|
* @param string $dest_file Destination file path (optional)
|
||||||
|
* @param int $pixelshigh Height of the icon in pixels (default: 32)
|
||||||
|
* @param int $alpha Alpha channel of the icon (default: 0)
|
||||||
|
* @param array $padding Padding of the icon (default: array(3, 3, 3, 3))
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function fas2png($unicode, $color='ffffff', $dest_file = null, $pixelshigh=32, $alpha=0, $padding=array(3, 3, 3, 3)) {
|
||||||
|
try {
|
||||||
|
// Set the target path
|
||||||
|
if ($dest_file != null) {
|
||||||
|
$icon = $dest_file;
|
||||||
|
} else {
|
||||||
|
$CI = &get_instance();
|
||||||
|
$cachepath = $CI->config->item('cache_path') == '' ? APPPATH . 'cache/' : $CI->config->item('cache_path');
|
||||||
|
$cacheDir = $cachepath . "fas_icons_cache/";
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
$icon = $cacheDir . 'icon_' . $unicode . '_' . $color . '_' . $pixelshigh . '_a' . $alpha . '_p' . implode('-', $padding) . '.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the icon already exists
|
||||||
|
if (file_exists($icon)) {
|
||||||
|
return $icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the font file exists
|
||||||
|
$font = realpath(APPPATH . '../') . '/assets/fontawesome/webfonts/fa-solid-900.ttf';
|
||||||
|
if (!file_exists($font)) {
|
||||||
|
throw new Exception('Font file not found: ' . $font);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables for brute-forcing the correct point height
|
||||||
|
$ratio = 96 / 72;
|
||||||
|
$ratioadd = 0.0001;
|
||||||
|
$heightalright = false;
|
||||||
|
$count = 0;
|
||||||
|
$maxcount = 20000;
|
||||||
|
|
||||||
|
$text = json_decode('"&#x'.$unicode.';"');
|
||||||
|
if ($text === null) {
|
||||||
|
throw new Exception('Failed to decode unicode: &#x'.$unicode.';');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brute-force point height
|
||||||
|
while (!$heightalright && $count < $maxcount) {
|
||||||
|
$x = $pixelshigh / $ratio;
|
||||||
|
$count++;
|
||||||
|
$bounds = imagettfbbox($x, 0, $font, $text);
|
||||||
|
|
||||||
|
if ($bounds === false) {
|
||||||
|
throw new Exception('Failed to calculate bounding box with imagettfbbox.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$height = abs($bounds[7] - abs($bounds[1]));
|
||||||
|
|
||||||
|
if ($height == $pixelshigh) {
|
||||||
|
$heightalright = true;
|
||||||
|
} else {
|
||||||
|
if ($height < $pixelshigh) {
|
||||||
|
$ratio -= $ratioadd;
|
||||||
|
} else {
|
||||||
|
$ratio += $ratioadd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$heightalright) {
|
||||||
|
throw new Exception('Could not calculate the correct height for the icon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = abs($bounds[4]) + abs($bounds[0]);
|
||||||
|
|
||||||
|
// Create the image
|
||||||
|
$im = imagecreatetruecolor($width + $padding[2] + $padding[3], $pixelshigh + $padding[0] + $padding[1]);
|
||||||
|
if ($im === false) {
|
||||||
|
throw new Exception('Failed to create image resource.');
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesavealpha($im, true);
|
||||||
|
$trans = imagecolorallocatealpha($im, 0, 0, 0, 127);
|
||||||
|
imagefill($im, 0, 0, $trans);
|
||||||
|
imagealphablending($im, true);
|
||||||
|
|
||||||
|
// Prepare font color
|
||||||
|
$fontcolor = $alpha << 24 | hexdec($color);
|
||||||
|
|
||||||
|
// Add the icon
|
||||||
|
if (imagettftext($im, $x, 0, 1 + $padding[2], $height - abs($bounds[1]) - 1 + $padding[0], $fontcolor, $font, $text) === false) {
|
||||||
|
throw new Exception('Failed to render icon with imagettftext.');
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesavealpha($im, true);
|
||||||
|
|
||||||
|
// Save the image
|
||||||
|
if (imagepng($im, $icon) === false) {
|
||||||
|
throw new Exception('Failed to save PNG image.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep to make sure the file is available in the next run.
|
||||||
|
usleep(100000); // 100ms
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
imagedestroy($im);
|
||||||
|
|
||||||
|
return $icon;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
log_message('error', $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,39 @@ class Qra {
|
|||||||
//return findings
|
//return findings
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to calculate the center of a bunch of coordinates
|
||||||
|
* Source: https://www.phpsnippet.com/snippet/center-point-of-multiple-gps-coordinates-with-php
|
||||||
|
*
|
||||||
|
* @param array $coordinates Array of coordinates [latitude, longitude]
|
||||||
|
* Example:
|
||||||
|
* $coordinates = [
|
||||||
|
* [37.7797, -122.41924],
|
||||||
|
* [37.77323, -122.41114],
|
||||||
|
* [37.79203, -122.40864],
|
||||||
|
* [37.7952, -122.4222]
|
||||||
|
* ];
|
||||||
|
*
|
||||||
|
* @return array [latitude, longitude] Center of the coordinates
|
||||||
|
*/
|
||||||
|
|
||||||
|
function getCenterLatLng($coordinates) {
|
||||||
|
$x = $y = $z = 0;
|
||||||
|
$n = count($coordinates);
|
||||||
|
foreach ($coordinates as $point)
|
||||||
|
{
|
||||||
|
$lt = $point[0] * pi() / 180;
|
||||||
|
$lg = $point[1] * pi() / 180;
|
||||||
|
$x += cos($lt) * cos($lg);
|
||||||
|
$y += cos($lt) * sin($lg);
|
||||||
|
$z += sin($lt);
|
||||||
|
}
|
||||||
|
$x /= $n;
|
||||||
|
$y /= $n;
|
||||||
|
|
||||||
|
return [atan2(($z / $n), sqrt($x * $x + $y * $y)) * 180 / pi(), atan2($y, $x) * 180 / pi()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -382,6 +382,12 @@ class Logbook_model extends CI_Model {
|
|||||||
$data['COL_MY_CNTY'] = strtoupper(trim($station['station_cnty']));
|
$data['COL_MY_CNTY'] = strtoupper(trim($station['station_cnty']));
|
||||||
$data['COL_MY_CQ_ZONE'] = strtoupper(trim($station['station_cq']));
|
$data['COL_MY_CQ_ZONE'] = strtoupper(trim($station['station_cq']));
|
||||||
$data['COL_MY_ITU_ZONE'] = strtoupper(trim($station['station_itu']));
|
$data['COL_MY_ITU_ZONE'] = strtoupper(trim($station['station_itu']));
|
||||||
|
|
||||||
|
// if there are any static map images for this station, remove them so they can be regenerated
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image($station_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decide whether its single gridsquare or a multi which makes it vucc_grids
|
// Decide whether its single gridsquare or a multi which makes it vucc_grids
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ class Logbooks_model extends CI_Model {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also clean up static map images first
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image(null, $clean_id);
|
||||||
|
|
||||||
// Delete logbook
|
// Delete logbook
|
||||||
$this->db->where('user_id', $this->session->userdata('user_id'));
|
$this->db->where('user_id', $this->session->userdata('user_id'));
|
||||||
$this->db->where('logbook_id', $clean_id);
|
$this->db->where('logbook_id', $clean_id);
|
||||||
|
|||||||
765
application/models/Staticmap_model.php
Normal file
765
application/models/Staticmap_model.php
Normal file
@@ -0,0 +1,765 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Staticmap_model extends CI_Model {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a static map image
|
||||||
|
*
|
||||||
|
* @param $qsos Amount of QSOs to render
|
||||||
|
* @param $uid User ID
|
||||||
|
* @param $centerMap Center of the map
|
||||||
|
* @param $station_coordinates Coordinates of the station
|
||||||
|
* @param $filepath Path to save the image to
|
||||||
|
* @param $continent Continent to display
|
||||||
|
* @param $thememode Theme mode ('light' or 'dark')
|
||||||
|
* @param $hide_home Whether to hide the home station
|
||||||
|
* @param $night_shadow Whether to display the night shadow
|
||||||
|
* @param $pathlines Whether to display pathlines
|
||||||
|
* @param $cqzones Whether to display CQ zones
|
||||||
|
* @param $ituzones Whether to display ITU zones
|
||||||
|
*
|
||||||
|
* @return bool True if the image was rendered successfully, false if not
|
||||||
|
*/
|
||||||
|
|
||||||
|
function render_static_map($qsos, $uid, $centerMap, $station_coordinates, $filepath, $continent = null, $thememode = null, $hide_home = false, $night_shadow = false, $pathlines = false, $cqzones = false, $ituzones = false) {
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//=============================================== PREPARE AND LOAD DEPENDENCIES =================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
$this->load->model('Stations');
|
||||||
|
$this->load->model('user_model');
|
||||||
|
$this->load->model('stationsetup_model');
|
||||||
|
|
||||||
|
if (!$this->load->is_loaded('Qra')) {
|
||||||
|
$this->load->library('Qra');
|
||||||
|
}
|
||||||
|
if (!$this->load->is_loaded('genfunctions')) {
|
||||||
|
$this->load->library('genfunctions');
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredClasses = [
|
||||||
|
'./src/StaticMap/src/OpenStreetMap.php',
|
||||||
|
'./src/StaticMap/src/LatLng.php',
|
||||||
|
'./src/StaticMap/src/TileLayer.php',
|
||||||
|
'./src/StaticMap/src/Markers.php',
|
||||||
|
'./src/StaticMap/src/MapData.php',
|
||||||
|
'./src/StaticMap/src/XY.php',
|
||||||
|
'./src/StaticMap/src/Image.php',
|
||||||
|
'./src/StaticMap/src/Utils/Terminator.php',
|
||||||
|
'./src/StaticMap/src/Line.php',
|
||||||
|
'./src/StaticMap/src/Polygon.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($requiredClasses as $class) {
|
||||||
|
require_once($class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fontPath = 'src/StaticMap/src/resources/font.ttf';
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//===================================================== CONFIGURE GRAPHICS ======================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Map data and default values
|
||||||
|
$centerMapLat = 25; // Needs to be a fix value as we can't wrap Latitude. Latitude of 25 is a good value to display all necessary places from north to south
|
||||||
|
$centerMapLng = $centerMap[1];
|
||||||
|
$centerMap = $centerMapLat . $centerMapLng; // used for cached tiles
|
||||||
|
$zoom = 3;
|
||||||
|
$width = 2048;
|
||||||
|
$height = round(($width * 3.3) / 4);
|
||||||
|
$line_pxsize = 1;
|
||||||
|
$fontSize = 20;
|
||||||
|
$fontPosX = $height - 20;
|
||||||
|
$fontPosY = 300;
|
||||||
|
$contFontPosX = $width - ($width - 50);
|
||||||
|
$contFontPosY = $height - ($height - 30);
|
||||||
|
$watermark_size_mutiplier = 1.5;
|
||||||
|
$watermarkPosX = DantSu\PHPImageEditor\Image::ALIGN_CENTER;
|
||||||
|
$watermarkPosY = DantSu\PHPImageEditor\Image::ALIGN_MIDDLE;
|
||||||
|
$continentEnabled = false;
|
||||||
|
$cqz_color = '195619'; // Green
|
||||||
|
$ituz_color = '2c3e5f'; // Blue
|
||||||
|
|
||||||
|
// Continent Option
|
||||||
|
if ($continent != 'nC' || $continent != null || $continent != '') {
|
||||||
|
if ($continent == 'AF') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'Africa';
|
||||||
|
$centerMapLat = 2;
|
||||||
|
$centerMapLng = 20;
|
||||||
|
$zoom = 5;
|
||||||
|
$height = round(($width * 4) / 4);
|
||||||
|
$fontPosX = $height - 20;
|
||||||
|
} elseif ($continent == 'AS') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'Asia';
|
||||||
|
$centerMapLat = 45;
|
||||||
|
$centerMapLng = 100;
|
||||||
|
$zoom = 4;
|
||||||
|
$contFontPosX = $width - ($width - 50);
|
||||||
|
} elseif ($continent == 'EU') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'Europe';
|
||||||
|
$centerMapLat = 65;
|
||||||
|
$centerMapLng = 15;
|
||||||
|
$height = round(($width * 5) / 4);
|
||||||
|
$zoom = 5;
|
||||||
|
$fontPosX = $height - 20;
|
||||||
|
$contFontPosX = $width - ($width - 50);
|
||||||
|
} elseif ($continent == 'NA') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'North America';
|
||||||
|
$centerMapLat = 55;
|
||||||
|
$centerMapLng = -100;
|
||||||
|
$zoom = 4;
|
||||||
|
$contFontPosX = $width - ($width - 110);
|
||||||
|
} elseif ($continent == 'OC') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'Oceania';
|
||||||
|
$centerMapLat = -25;
|
||||||
|
$centerMapLng = 140;
|
||||||
|
$zoom = 5;
|
||||||
|
$contFontPosX = $width - ($width - 70);
|
||||||
|
} elseif ($continent == 'SA') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'South America';
|
||||||
|
$centerMapLat = -26;
|
||||||
|
$centerMapLng = -60;
|
||||||
|
$zoom = 5;
|
||||||
|
$width = 1570;
|
||||||
|
$height = round(($width * 5) / 4);
|
||||||
|
$fontPosX = $height - 20;
|
||||||
|
$contFontPosX = $width - ($width - 110);
|
||||||
|
} elseif ($continent == 'AN') {
|
||||||
|
$continentEnabled = true;
|
||||||
|
$continentText = 'Antarctica';
|
||||||
|
$centerMapLat = -73;
|
||||||
|
$centerMapLng = 0;
|
||||||
|
$zoom = 3;
|
||||||
|
$watermark_size_mutiplier = 1;
|
||||||
|
$height = round(($width * 1.5) / 4);
|
||||||
|
$fontPosX = $height - 20;
|
||||||
|
$contFontPosX = $width - ($width - 90);
|
||||||
|
} else {
|
||||||
|
// we don't want to change the default values in this case
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($zoom == 3) {
|
||||||
|
$marker_size = 18;
|
||||||
|
} elseif ($zoom == 4) {
|
||||||
|
$marker_size = 24;
|
||||||
|
} elseif ($zoom == 5) {
|
||||||
|
$marker_size = 28;
|
||||||
|
} else {
|
||||||
|
$marker_size = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//================================================ CREATE AN INSTANCE OF THE MAP ================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Set the tile layer
|
||||||
|
if ($thememode != null) {
|
||||||
|
$attribution = $this->optionslib->get_option('option_map_tile_server_copyright') ?? 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a>';
|
||||||
|
if ($thememode == 'light') {
|
||||||
|
$server_url = $this->optionslib->get_option('option_map_tile_server') ?? '';
|
||||||
|
if ($server_url == '') {
|
||||||
|
$server_url = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||||
|
$this->optionslib->update('map_tile_server', $server_url, 'yes');
|
||||||
|
}
|
||||||
|
$tileLayer = new \Wavelog\StaticMapImage\TileLayer($server_url, $attribution, $thememode);
|
||||||
|
} elseif ($thememode == 'dark') {
|
||||||
|
$server_url = $this->optionslib->get_option('option_map_tile_server_dark') ?? '';
|
||||||
|
if ($server_url == '') {
|
||||||
|
$server_url = 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png';
|
||||||
|
$this->optionslib->update('map_tile_server_dark', $server_url, 'yes');
|
||||||
|
}
|
||||||
|
$tileLayer = new \Wavelog\StaticMapImage\TileLayer($server_url, $attribution, $thememode);
|
||||||
|
} else {
|
||||||
|
$tileLayer = \Wavelog\StaticMapImage\TileLayer::defaultTileLayer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$tileLayer = \Wavelog\StaticMapImage\TileLayer::defaultTileLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the map
|
||||||
|
$map = new \Wavelog\StaticMapImage\OpenStreetMap(new \Wavelog\StaticMapImage\LatLng($centerMapLat, $centerMapLng), $zoom, $width, $height, $tileLayer);
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//====================================================== RENDER THE ICONS =======================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Get user defined markers
|
||||||
|
$options_object = $this->user_options_model->get_options('map_custom', null, $uid)->result();
|
||||||
|
$user_icondata = array();
|
||||||
|
if (count($options_object) > 0) {
|
||||||
|
foreach ($options_object as $row) {
|
||||||
|
if ($row->option_name == 'icon') {
|
||||||
|
$option_value = json_decode($row->option_value, true);
|
||||||
|
foreach ($option_value as $ktype => $vtype) {
|
||||||
|
if ($this->input->post('user_map_' . $row->option_key . '_icon')) {
|
||||||
|
$user_icondata['user_map_' . $row->option_key . '_' . $ktype] = $this->input->post('user_map_' . $row->option_key . '_' . $ktype, true);
|
||||||
|
} else {
|
||||||
|
$user_icondata['user_map_' . $row->option_key . '_' . $ktype] = $vtype;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$user_icondata['user_map_' . $row->option_name . '_' . $row->option_key] = $row->option_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default values
|
||||||
|
$user_icondata['user_map_qso_icon'] = "fas fa-dot-circle";
|
||||||
|
$user_icondata['user_map_qso_color'] = "#FF0000";
|
||||||
|
$user_icondata['user_map_station_icon'] = "fas fa-home";
|
||||||
|
$user_icondata['user_map_station_color'] = "#0000FF";
|
||||||
|
$user_icondata['user_map_qsoconfirm_icon'] = "fas fa-check-circle";
|
||||||
|
$user_icondata['user_map_qsoconfirm_color'] = "#00AA00";
|
||||||
|
$user_icondata['user_map_gridsquare_show'] = "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map all available icons to the unicode
|
||||||
|
$unicode_map = array(
|
||||||
|
'0' => 'f192', // dot-circle is default
|
||||||
|
'fas fa-home' => 'f015',
|
||||||
|
'fas fa-broadcast-tower' => 'f519',
|
||||||
|
'fas fa-user' => 'f007',
|
||||||
|
'fas fa-dot-circle' => 'f192',
|
||||||
|
'fas fa-check-circle' => 'f058',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Home Icon
|
||||||
|
if (!$home_icon = $this->genfunctions->fas2png($unicode_map[$user_icondata['user_map_station_icon']], substr($user_icondata['user_map_station_color'], 1))) {
|
||||||
|
log_message('error', "Failed to generate map icon. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// QSO Icon
|
||||||
|
if (!$qso_icon = $this->genfunctions->fas2png($unicode_map[$user_icondata['user_map_qso_icon']], substr($user_icondata['user_map_qso_color'], 1))) {
|
||||||
|
log_message('error', "Failed to generate map icon. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// QSO Confirm Icon
|
||||||
|
if (!$qso_cfnm_icon = $this->genfunctions->fas2png($unicode_map[$user_icondata['user_map_qsoconfirm_icon']], substr($user_icondata['user_map_qsoconfirm_color'], 1))) {
|
||||||
|
log_message('error', "Failed to generate map icon. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//========================================= PROCESS THE QSOs AND PREPARE THE PATHLINES ==========================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Get all QSOs with gridsquares and set markers for confirmed and unconfirmed QSOs
|
||||||
|
// We also prepare the PATHLINES here ($paths and $paths_cnfd)
|
||||||
|
|
||||||
|
$markerQsos = [];
|
||||||
|
$markerQsosConfirmed = [];
|
||||||
|
$paths = [];
|
||||||
|
$paths_cnfd = [];
|
||||||
|
$user_default_confirmation = $this->visitor_model->get_user_default_confirmation($uid);
|
||||||
|
foreach ($qsos->result('array') as $qso) {
|
||||||
|
if (!empty($qso['COL_GRIDSQUARE'])) {
|
||||||
|
$latlng = $this->qra->qra2latlong($qso['COL_GRIDSQUARE']);
|
||||||
|
$lat = $latlng[0];
|
||||||
|
$lng = $latlng[1];
|
||||||
|
} else if (!empty($qso['COL_VUCC_GRIDS'])) {
|
||||||
|
$latlng = $this->qra->qra2latlong($qso['COL_VUCC_GRIDS']);
|
||||||
|
$lat = $latlng[0];
|
||||||
|
$lng = $latlng[1];
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for continents
|
||||||
|
if ($continentEnabled) {
|
||||||
|
if ($qso['COL_CONT'] != $continent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->visitor_model->qso_is_confirmed($qso, $user_default_confirmation) == true) {
|
||||||
|
if ($pathlines) {
|
||||||
|
$station_grid = $this->stations->profile($qso['station_id'])->row()->station_gridsquare;
|
||||||
|
$station_latlng = $this->qra->qra2latlong($station_grid);
|
||||||
|
$paths_cnfd[] = $this->draw_pathline($station_latlng, $latlng, $continentEnabled, '04A902', $line_pxsize); // Green
|
||||||
|
}
|
||||||
|
$markerQsosConfirmed[] = new \Wavelog\StaticMapImage\LatLng($lat, $lng);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if ($pathlines) {
|
||||||
|
$station_grid = $this->stations->profile($qso['station_id'])->row()->station_gridsquare;
|
||||||
|
$station_latlng = $this->qra->qra2latlong($station_grid);
|
||||||
|
$paths[] = $this->draw_pathline($station_latlng, $latlng, $continentEnabled, 'ff0000', $line_pxsize); // Red
|
||||||
|
}
|
||||||
|
$markerQsos[] = new \Wavelog\StaticMapImage\LatLng($lat, $lng);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//==================================================== PREPARE THE MARKERS ======================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
// Set the markers for the station
|
||||||
|
if (!$hide_home) {
|
||||||
|
$wrapping = !$continentEnabled;
|
||||||
|
$markersStation = new \Wavelog\StaticMapImage\Markers($home_icon, $wrapping);
|
||||||
|
$markersStation->resizeMarker($marker_size, $marker_size);
|
||||||
|
$markersStation->setAnchor(\Wavelog\StaticMapImage\Markers::ANCHOR_CENTER, \Wavelog\StaticMapImage\Markers::ANCHOR_BOTTOM);
|
||||||
|
foreach ($station_coordinates as $station) {
|
||||||
|
$markersStation->addMarker(new \Wavelog\StaticMapImage\LatLng($station[0], $station[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the markers for unconfirmed QSOs
|
||||||
|
$markers = new \Wavelog\StaticMapImage\Markers($qso_icon, true);
|
||||||
|
$markers->resizeMarker($marker_size, $marker_size);
|
||||||
|
$markers->setAnchor(\Wavelog\StaticMapImage\Markers::ANCHOR_CENTER, \Wavelog\StaticMapImage\Markers::ANCHOR_BOTTOM);
|
||||||
|
|
||||||
|
foreach ($markerQsos as $position) {
|
||||||
|
$markers->addMarker($position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the markers for confirmed QSOs
|
||||||
|
$markersConfirmed = new \Wavelog\StaticMapImage\Markers($qso_cfnm_icon, true);
|
||||||
|
$markersConfirmed->resizeMarker($marker_size, $marker_size);
|
||||||
|
$markersConfirmed->setAnchor(\Wavelog\StaticMapImage\Markers::ANCHOR_CENTER, \Wavelog\StaticMapImage\Markers::ANCHOR_BOTTOM);
|
||||||
|
|
||||||
|
foreach ($markerQsosConfirmed as $position) {
|
||||||
|
$markersConfirmed->addMarker($position);
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//================================================== PREPARE THE NIGHTSHADOW ====================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
if ($night_shadow) {
|
||||||
|
$terminator = new Terminator();
|
||||||
|
$terminatorLine = $terminator->getTerminatorCoordinates();
|
||||||
|
|
||||||
|
$lcolor = '000000'; // 000000 = black but we set lweight to 0, so the line won't be drawn anyway
|
||||||
|
$lweight = 0;
|
||||||
|
$pcolor = '000000AA'; // 000000 = black, AA = 66% opacity as hex
|
||||||
|
|
||||||
|
$night_shadow_polygon = new Wavelog\StaticMapImage\Polygon($lcolor, $lweight, $pcolor);
|
||||||
|
|
||||||
|
foreach ($terminatorLine as $coordinate) {
|
||||||
|
$night_shadow_polygon->addPoint(new Wavelog\StaticMapImage\LatLng($coordinate[0], $coordinate[1]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//============================================ PREPARE THE CQ ZONES OVERLAY =====================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due to the long rendering times we cache the CQ Zones overlay and just paste it on the map
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ($cqzones) {
|
||||||
|
|
||||||
|
$cqz_cachedir = dirname($filepath) . '/cqz_overlays';
|
||||||
|
if (!is_dir($cqz_cachedir)) {
|
||||||
|
mkdir($cqz_cachedir, 0777, true);
|
||||||
|
}
|
||||||
|
$cqz_filename = crc32($centerMap . $continent . $zoom . $width . $height . $cqz_color) . '.png';
|
||||||
|
|
||||||
|
if (!file_exists($cqz_cachedir . '/' . $cqz_filename)) {
|
||||||
|
|
||||||
|
log_message('info', "No cached CQ Zone Overlay found. Creating new CQ Zones overlay...");
|
||||||
|
|
||||||
|
$geojsonFile = 'assets/json/geojson/cqzones.geojson';
|
||||||
|
$geojsonData = file_get_contents($geojsonFile);
|
||||||
|
|
||||||
|
$data = json_decode($geojsonData, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
log_message("error", "Failed to read geojson data for cqzones" . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
$lcolor = $cqz_color;
|
||||||
|
$lweight = 1;
|
||||||
|
$pcolor = $cqz_color . 'FF'; // FF = 100% opacity as hex
|
||||||
|
|
||||||
|
if (isset($data['features'])) {
|
||||||
|
$cqzones_polygon_array = [];
|
||||||
|
foreach ($data['features'] as $feature) {
|
||||||
|
$one_cqzpolygon = new Wavelog\StaticMapImage\Polygon($lcolor, $lweight, $pcolor, !$continentEnabled);
|
||||||
|
$coordinates = $feature['geometry']['coordinates'];
|
||||||
|
|
||||||
|
foreach ($coordinates as $zone) {
|
||||||
|
foreach ($zone as $point) {
|
||||||
|
$one_cqzpolygon->addPoint(new Wavelog\StaticMapImage\LatLng($point[1], $point[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone_number = $feature['properties']['cq_zone_number'];
|
||||||
|
$zone_name_loc = $feature['properties']['cq_zone_name_loc'];
|
||||||
|
$cqzones_polygon_array[$zone_number]['polygon'] = $one_cqzpolygon;
|
||||||
|
$cqzones_polygon_array[$zone_number]['number'] = $zone_number;
|
||||||
|
$cqzones_polygon_array[$zone_number]['name_loc'] = $zone_name_loc;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message("error", "Failed to read geojson data for cqzones. No features found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$cqz_tl = \Wavelog\StaticMapImage\TileLayer::defaultTileLayer();
|
||||||
|
$cqz_tl = $cqz_tl->setOpacity(0);
|
||||||
|
$cqz_map = new \Wavelog\StaticMapImage\OpenStreetMap(new \Wavelog\StaticMapImage\LatLng($centerMapLat, $centerMapLng), $zoom, $width, $height, $cqz_tl);
|
||||||
|
$cqz_image = $cqz_map->getImage($centerMap);
|
||||||
|
|
||||||
|
foreach ($cqzones_polygon_array as $cqzones_polygon) {
|
||||||
|
$polygon = $cqzones_polygon['polygon'];
|
||||||
|
$polygon->draw($cqz_image, $cqz_map->getMapData());
|
||||||
|
|
||||||
|
$zone_number = $cqzones_polygon['number'];
|
||||||
|
$cqz_fontsize = 33;
|
||||||
|
$position = new \Wavelog\StaticMapImage\LatLng($cqzones_polygon['name_loc'][0], $cqzones_polygon['name_loc'][1]);
|
||||||
|
$positionXY = $cqz_map->getMapData()->convertLatLngToPxPosition($position);
|
||||||
|
$cqz_image->writeText($zone_number, $fontPath, $cqz_fontsize, $lcolor, $positionXY->getX(), $positionXY->getY(), $cqz_image::ALIGN_CENTER, $cqz_image::ALIGN_MIDDLE, 0, 0, !$continentEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cqz_image->savePNG($cqz_cachedir . '/' . $cqz_filename);
|
||||||
|
} else {
|
||||||
|
log_message('info', "Found cached CQ Zone Overlay. Using cached overlay...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//============================================ PREPARE THE ITU ZONES OVERLAY ====================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
if ($ituzones) {
|
||||||
|
|
||||||
|
$ituz_cachedir = dirname($filepath) . '/ituz_overlays';
|
||||||
|
if (!is_dir($ituz_cachedir)) {
|
||||||
|
mkdir($ituz_cachedir, 0777, true);
|
||||||
|
}
|
||||||
|
$ituz_filename = crc32($centerMap . $continent . $zoom . $width . $height . $ituz_color) . '.png';
|
||||||
|
|
||||||
|
if (!file_exists($ituz_cachedir . '/' . $ituz_filename)) {
|
||||||
|
|
||||||
|
log_message('info', "No cached ITU Zone Overlay found. Creating new ITU Zones overlay...");
|
||||||
|
|
||||||
|
$geojsonFile = 'assets/json/geojson/ituzones.geojson';
|
||||||
|
$geojsonData = file_get_contents($geojsonFile);
|
||||||
|
|
||||||
|
$data = json_decode($geojsonData, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
log_message("error", "Failed to read geojson data for ituzones" . json_last_error_msg());
|
||||||
|
}
|
||||||
|
|
||||||
|
$lcolor = $ituz_color;
|
||||||
|
$lweight = 1;
|
||||||
|
$pcolor = $ituz_color . 'FF'; // FF = 100% opacity as hex
|
||||||
|
|
||||||
|
if (isset($data['features'])) {
|
||||||
|
$ituzones_polygon_array = [];
|
||||||
|
foreach ($data['features'] as $feature) { // one zone
|
||||||
|
$one_ituzpolygon = new Wavelog\StaticMapImage\Polygon($lcolor, $lweight, $pcolor, !$continentEnabled);
|
||||||
|
$coordinates = $feature['geometry']['coordinates'];
|
||||||
|
|
||||||
|
foreach ($coordinates as $zone) {
|
||||||
|
$ituz_points = [];
|
||||||
|
foreach ($zone as $point) {
|
||||||
|
$ituz_points[] = [$point[1], $point[0]];
|
||||||
|
$one_ituzpolygon->addPoint(new Wavelog\StaticMapImage\LatLng($point[1], $point[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone_number = $feature['properties']['itu_zone_number'];
|
||||||
|
$zone_name_loc = $feature['properties']['itu_zone_name_loc'];
|
||||||
|
$ituzones_polygon_array[$zone_number]['polygon'] = $one_ituzpolygon;
|
||||||
|
$ituzones_polygon_array[$zone_number]['number'] = $zone_number;
|
||||||
|
$ituzones_polygon_array[$zone_number]['name_loc'] = $zone_name_loc;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message("error", "Failed to read geojson data for ituzones. No features found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ituz_tl = \Wavelog\StaticMapImage\TileLayer::defaultTileLayer();
|
||||||
|
$ituz_tl = $ituz_tl->setOpacity(0);
|
||||||
|
$ituz_map = new \Wavelog\StaticMapImage\OpenStreetMap(new \Wavelog\StaticMapImage\LatLng($centerMapLat, $centerMapLng), $zoom, $width, $height, $ituz_tl);
|
||||||
|
$ituz_image = $ituz_map->getImage($centerMap);
|
||||||
|
|
||||||
|
foreach ($ituzones_polygon_array as $ituzones_polygon) {
|
||||||
|
$polygon = $ituzones_polygon['polygon'];
|
||||||
|
$polygon->draw($ituz_image, $ituz_map->getMapData());
|
||||||
|
|
||||||
|
$zone_number = $ituzones_polygon['number'];
|
||||||
|
$ituz_fontsize = 33;
|
||||||
|
$position = new \Wavelog\StaticMapImage\LatLng($ituzones_polygon['name_loc'][0], $ituzones_polygon['name_loc'][1]);
|
||||||
|
$positionXY = $ituz_map->getMapData()->convertLatLngToPxPosition($position);
|
||||||
|
$ituz_image->writeText($zone_number, $fontPath, $ituz_fontsize, $ituz_color, $positionXY->getX(), $positionXY->getY(), $ituz_image::ALIGN_CENTER, $ituz_image::ALIGN_MIDDLE, 0, 0, !$continentEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ituz_image->savePNG($ituz_cachedir . '/' . $ituz_filename);
|
||||||
|
} else {
|
||||||
|
log_message('info', "Found cached ITU Zone Overlay. Using cached overlay...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//===============================================================================================================================
|
||||||
|
//==================================================== CREATE THE IMAGE =========================================================
|
||||||
|
//===============================================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finally we can create the image and add the elements
|
||||||
|
*/
|
||||||
|
|
||||||
|
$image = $map->getImage($centerMap);
|
||||||
|
|
||||||
|
// Add night shadow
|
||||||
|
if ($night_shadow) {
|
||||||
|
$night_shadow_polygon->draw($image, $map->getMapData());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathlines
|
||||||
|
if ($pathlines) {
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
$path->draw($image, $map->getMapData());
|
||||||
|
}
|
||||||
|
foreach ($paths_cnfd as $path) {
|
||||||
|
$path->draw($image, $map->getMapData());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CQ Zones
|
||||||
|
if ($cqzones) {
|
||||||
|
$cqz_image = DantSu\PHPImageEditor\Image::fromPath($cqz_cachedir . '/' . $cqz_filename);
|
||||||
|
$image->pasteOn($cqz_image, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ITU Zones
|
||||||
|
if ($ituzones) {
|
||||||
|
$ituz_image = DantSu\PHPImageEditor\Image::fromPath($ituz_cachedir . '/' . $ituz_filename);
|
||||||
|
$image->pasteOn($ituz_image, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add markers
|
||||||
|
if (!$hide_home) {
|
||||||
|
$markersStation->draw($image, $map->getMapData());
|
||||||
|
}
|
||||||
|
$markers->draw($image, $map->getMapData());
|
||||||
|
$markersConfirmed->draw($image, $map->getMapData());
|
||||||
|
|
||||||
|
// Add Wavelog watermark
|
||||||
|
$watermark = DantSu\PHPImageEditor\Image::fromPath('src/StaticMap/src/resources/watermark_static_map.png');
|
||||||
|
$watermark->resize(round($width * $watermark_size_mutiplier), round((($width * 3) / 4) * $watermark_size_mutiplier));
|
||||||
|
$image->pasteOn($watermark, $watermarkPosX, $watermarkPosY);
|
||||||
|
|
||||||
|
// Add "Created with Wavelog" text
|
||||||
|
$user = $this->user_model->get_by_id($uid)->row();
|
||||||
|
$custom_date_format = $user->user_date_format;
|
||||||
|
$dateTime = date($custom_date_format . ' - H:i');
|
||||||
|
$text = "Created with Wavelog on " . $dateTime . " UTC";
|
||||||
|
$color = 'ff0000'; // Red
|
||||||
|
$image->writeText($text, $fontPath, $fontSize, $color, $fontPosY, $fontPosX);
|
||||||
|
|
||||||
|
// Add continent text
|
||||||
|
if ($continentEnabled) {
|
||||||
|
$fontPath = 'src/StaticMap/src/resources/font.ttf';
|
||||||
|
$color = 'ff0000'; // Red
|
||||||
|
$image->writeText($continentText, $fontPath, $fontSize, $color, $contFontPosX, $contFontPosY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the image
|
||||||
|
if ($image->savePNG($filepath)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a geodesic pathline between two points
|
||||||
|
*
|
||||||
|
* @param $start Start point
|
||||||
|
* @param $end End point
|
||||||
|
* @param $continent If continent view is enabled (affects wrapping)
|
||||||
|
* @param $color Color of the pathline
|
||||||
|
* @param $weight Weight of the pathline (px)
|
||||||
|
*
|
||||||
|
* @return Line An instance of Wavelog\StaticMapImage\Line
|
||||||
|
*/
|
||||||
|
|
||||||
|
function draw_pathline($start, $end, $continent, $color = 'ffffff', $weight = 1) {
|
||||||
|
// Start in Berlin
|
||||||
|
$start = new \Wavelog\StaticMapImage\LatLng($start[0], $start[1]);
|
||||||
|
|
||||||
|
// End in honkong
|
||||||
|
$end = new \Wavelog\StaticMapImage\LatLng($end[0], $end[1]);
|
||||||
|
|
||||||
|
$path = new \Wavelog\StaticMapImage\Line($color, $weight, !$continent);
|
||||||
|
$points = $path->geodesicPoints($start, $end);
|
||||||
|
|
||||||
|
foreach ($points as $point) {
|
||||||
|
$path->addPoint($point);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove outdated static map images from the cache directory
|
||||||
|
* Based on station_id because is handled and used during qso creation
|
||||||
|
*
|
||||||
|
* @param int $station_id The station ID to remove the static map image for
|
||||||
|
* @param int $logbook_id The logbook ID to remove the static map image for
|
||||||
|
*
|
||||||
|
* @return bool True if the image was removed successfully, false if not
|
||||||
|
*/
|
||||||
|
|
||||||
|
function remove_static_map_image($station_id = null, $logbook_id = null) {
|
||||||
|
|
||||||
|
$this->load->model('stationsetup_model');
|
||||||
|
|
||||||
|
if ($station_id == null && $logbook_id == null) {
|
||||||
|
log_message('error', "Can't remove static map image cache. Neither a station ID nor a logbook ID was provided. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$cachepath = $this->config->item('cache_path') == '' ? APPPATH . 'cache/' : $this->config->item('cache_path');
|
||||||
|
$cacheDir = $cachepath . "staticmap_images/";
|
||||||
|
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
log_message('debug', "Cache directory '" . $cacheDir . "' does not exist. Therefore no static map images to remove...");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($station_id != null) {
|
||||||
|
if (!is_numeric($station_id) || $station_id == '' || $station_id == null) {
|
||||||
|
log_message('error', "Station ID is not valid. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$linked_logbooks = $this->stationsetup_model->get_container_relations($station_id, true); // true means we do a reverse search
|
||||||
|
|
||||||
|
if (!$linked_logbooks) {
|
||||||
|
log_message('error', "No linked logbooks found for station ID " . $station_id . ". Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach ($linked_logbooks as $logbook_id) {
|
||||||
|
$slug = $this->stationsetup_model->get_slug($logbook_id);
|
||||||
|
if ($slug == false) {
|
||||||
|
log_message('debug', "No slug found for logbook ID " . $logbook_id . ". Continue...");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = crc32('staticmap_' . $slug);
|
||||||
|
$files = glob($cacheDir . $prefix . '*');
|
||||||
|
|
||||||
|
if (!empty($files)) {
|
||||||
|
foreach ($files as $file) {
|
||||||
|
log_message('debug', "Found a outdated static map image: " . basename($file) . ". Deleting...");
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message('info', "Found no files with the prefix '" . $prefix . "' in the cache directory.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Success
|
||||||
|
}
|
||||||
|
if ($logbook_id != null) {
|
||||||
|
|
||||||
|
if (!is_numeric($logbook_id) || $logbook_id == '' || $logbook_id == null) {
|
||||||
|
log_message('error', "Logbook ID is not valid. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $this->stationsetup_model->get_slug($logbook_id);
|
||||||
|
if ($slug == false) {
|
||||||
|
log_message('debug', "No slug found for logbook ID " . $logbook_id . ". Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = crc32('staticmap_' . $slug);
|
||||||
|
$files = glob($cacheDir . $prefix . '*');
|
||||||
|
|
||||||
|
if (!empty($files)) {
|
||||||
|
foreach ($files as $file) {
|
||||||
|
log_message('debug', "Found a outdated static map image: " . basename($file) . ". Deleting...");
|
||||||
|
unlink($file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_message('info', "Found no files with the prefix '" . $prefix . "' in the cache directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a cached static map image
|
||||||
|
*
|
||||||
|
* @param $file File to validate (realpath)
|
||||||
|
* @param $cacheDir Cache directory itself
|
||||||
|
* @param $maxAge Maximum age of the file in seconds
|
||||||
|
*
|
||||||
|
* @return bool True if the file is valid, false if not
|
||||||
|
*/
|
||||||
|
|
||||||
|
function validate_cached_image($file, $cacheDir, $maxAge, $slug) {
|
||||||
|
|
||||||
|
$realPath = realpath($file);
|
||||||
|
$filename = basename($file);
|
||||||
|
|
||||||
|
// get the slug
|
||||||
|
$parts = explode('_', $filename);
|
||||||
|
$validation_hash = $parts[0];
|
||||||
|
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
log_message('debug', "Cached static map image file does not exist. Creating a new one...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($validation_hash !== (string) crc32('staticmap_' . $slug)) {
|
||||||
|
log_message('error', "Static_map: Invalid validation hash. Deleting the file and exiting...");
|
||||||
|
if (!unlink($file)) {
|
||||||
|
log_message('error', "Failed to delete invalid cached static map image file: " . $file);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($realPath === false || strpos($realPath, $cacheDir) !== 0) {
|
||||||
|
log_message('error', "Invalid Filepath. Possible traversal attack detected. Deleting the file and exiting...");
|
||||||
|
if (file_exists($file)) {
|
||||||
|
if (!unlink($file)) {
|
||||||
|
log_message('error', "Failed to delete invalid cached static map image file: " . $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesize($file) < 1024) { // 1 kB
|
||||||
|
log_message('error', "Cached static map image file is unusually small, possible corruption detected. Deleting the file and exiting...");
|
||||||
|
if (!unlink($file)) {
|
||||||
|
log_message('error', "Failed to delete invalid cached static map image file: " . $file);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mime_content_type($file) !== 'image/png') {
|
||||||
|
log_message('error', "Cached static map image file is no PNG. Deleting the file and exiting...");
|
||||||
|
if (!unlink($file)) {
|
||||||
|
log_message('error', "Failed to delete invalid cached static map image file: " . $file);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time() - filemtime($file) > $maxAge) {
|
||||||
|
log_message('debug', "Cached static map image has expired. Deleting old cache file...");
|
||||||
|
if (!unlink($file)) {
|
||||||
|
log_message('error', "Failed to delete invalid cached static map image file: " . $file);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -262,6 +262,12 @@ class Stations extends CI_Model {
|
|||||||
}
|
}
|
||||||
// Delete QSOs
|
// Delete QSOs
|
||||||
$this->db->query("DELETE FROM ".$this->config->item('table_name')." WHERE station_id = ?",$clean_id);
|
$this->db->query("DELETE FROM ".$this->config->item('table_name')." WHERE station_id = ?",$clean_id);
|
||||||
|
|
||||||
|
// Also clean up static map images
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image($clean_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function set_active($current, $new) {
|
function set_active($current, $new) {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
class Stationsetup_model extends CI_Model {
|
class Stationsetup_model extends CI_Model {
|
||||||
|
|
||||||
function getContainer($id) {
|
function getContainer($id, $session = true) {
|
||||||
// Clean ID
|
// Clean ID
|
||||||
$clean_id = $this->security->xss_clean($id);
|
$clean_id = $this->security->xss_clean($id);
|
||||||
|
|
||||||
$this->db->where('user_id', $this->session->userdata('user_id'));
|
if ($session) {
|
||||||
|
$this->db->where('user_id', $this->session->userdata('user_id'));
|
||||||
|
}
|
||||||
$this->db->where('logbook_id', $clean_id);
|
$this->db->where('logbook_id', $clean_id);
|
||||||
return $this->db->get('station_logbooks');
|
return $this->db->get('station_logbooks');
|
||||||
}
|
}
|
||||||
@@ -22,6 +24,12 @@ class Stationsetup_model extends CI_Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function remove_public_slug($logbook_id) {
|
function remove_public_slug($logbook_id) {
|
||||||
|
// Also clean up static map images first
|
||||||
|
if (!$this->load->is_loaded('staticmap_model')) {
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
}
|
||||||
|
$this->staticmap_model->remove_static_map_image(null, $logbook_id);
|
||||||
|
|
||||||
$this->db->set('public_slug', null);
|
$this->db->set('public_slug', null);
|
||||||
$this->db->where('user_id', $this->session->userdata('user_id'));
|
$this->db->where('user_id', $this->session->userdata('user_id'));
|
||||||
$this->db->where('logbook_id', xss_clean($logbook_id));
|
$this->db->where('logbook_id', xss_clean($logbook_id));
|
||||||
@@ -126,13 +134,15 @@ class Stationsetup_model extends CI_Model {
|
|||||||
$this->db->where('public_slug', $this->security->xss_clean($slug));
|
$this->db->where('public_slug', $this->security->xss_clean($slug));
|
||||||
$query = $this->db->get('station_logbooks');
|
$query = $this->db->get('station_logbooks');
|
||||||
|
|
||||||
if ($query->num_rows() > 0){
|
if ($query->num_rows() == 1){
|
||||||
foreach ($query->result() as $row) {
|
return $query->row()->logbook_id;
|
||||||
return $row->logbook_id;
|
} elseif ($query->num_rows() > 1) {
|
||||||
}
|
log_message('error', 'Multiple logbooks with same public_slug found!');
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function is_public_slug_available($slug) {
|
function is_public_slug_available($slug) {
|
||||||
@@ -148,6 +158,19 @@ class Stationsetup_model extends CI_Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get public slug for a logbook
|
||||||
|
function get_slug($logbook_id) {
|
||||||
|
$this->db->where('logbook_id', $logbook_id);
|
||||||
|
$this->db->where('public_slug !=', null);
|
||||||
|
$query = $this->db->get('station_logbooks');
|
||||||
|
|
||||||
|
if ($query->num_rows() == 1){
|
||||||
|
return $query->row()->public_slug;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function locationInfo($id) {
|
function locationInfo($id) {
|
||||||
$userid = $this->session->userdata('user_id'); // Fallback to session-uid, if userid is omitted
|
$userid = $this->session->userdata('user_id'); // Fallback to session-uid, if userid is omitted
|
||||||
$this->db->select('station_profile.station_profile_name, station_profile.station_callsign, dxcc_entities.name as station_country, dxcc_entities.end as dxcc_end');
|
$this->db->select('station_profile.station_profile_name, station_profile.station_callsign, dxcc_entities.name as station_country, dxcc_entities.end as dxcc_end');
|
||||||
@@ -157,16 +180,26 @@ class Stationsetup_model extends CI_Model {
|
|||||||
return $this->db->get('station_profile');
|
return $this->db->get('station_profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_container_relations($logbook_id) {
|
function get_container_relations($id, $reverse = false) {
|
||||||
|
|
||||||
|
if ($reverse == false) {
|
||||||
|
$searchIn = 'station_logbook_id';
|
||||||
|
} else {
|
||||||
|
$searchIn = 'station_location_id';
|
||||||
|
}
|
||||||
|
|
||||||
$relationships_array = array();
|
$relationships_array = array();
|
||||||
|
|
||||||
$this->db->where('station_logbook_id', $logbook_id);
|
$this->db->where($searchIn, $id);
|
||||||
$query = $this->db->get('station_logbooks_relationship');
|
$query = $this->db->get('station_logbooks_relationship');
|
||||||
|
|
||||||
if ($query->num_rows() > 0){
|
if ($query->num_rows() > 0){
|
||||||
foreach ($query->result() as $row) {
|
foreach ($query->result() as $row) {
|
||||||
array_push($relationships_array, $row->station_location_id);
|
if ($reverse == false) {
|
||||||
|
array_push($relationships_array, $row->station_location_id);
|
||||||
|
} else {
|
||||||
|
array_push($relationships_array, $row->station_logbook_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $relationships_array;
|
return $relationships_array;
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ class Themes_model extends CI_Model {
|
|||||||
// Clean ID
|
// Clean ID
|
||||||
$clean_id = $this->security->xss_clean($id);
|
$clean_id = $this->security->xss_clean($id);
|
||||||
|
|
||||||
$sql = "SELECT * FROM themes where id =" . $clean_id;
|
$sql = "SELECT * FROM themes where id = ?";
|
||||||
|
|
||||||
$data = $this->db->query($sql);
|
$data = $this->db->query($sql, $clean_id);
|
||||||
|
|
||||||
return ($data->row());
|
return ($data->row());
|
||||||
}
|
}
|
||||||
@@ -58,9 +58,9 @@ class Themes_model extends CI_Model {
|
|||||||
$clean_theme = $this->security->xss_clean($theme);
|
$clean_theme = $this->security->xss_clean($theme);
|
||||||
$clean_location = $this->security->xss_clean($logo_location);
|
$clean_location = $this->security->xss_clean($logo_location);
|
||||||
|
|
||||||
$sql = "SELECT " . $clean_location . " FROM themes WHERE foldername = '" . $clean_theme . "'";
|
$sql = "SELECT " . $clean_location . " FROM themes WHERE foldername = ?";
|
||||||
|
|
||||||
$query = $this->db->query($sql);
|
$query = $this->db->query($sql, $clean_theme);
|
||||||
|
|
||||||
if ($query) {
|
if ($query) {
|
||||||
$result = $query->row();
|
$result = $query->row();
|
||||||
@@ -73,4 +73,22 @@ class Themes_model extends CI_Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_theme_mode($theme) {
|
||||||
|
$clean_theme = $this->security->xss_clean($theme);
|
||||||
|
|
||||||
|
$sql = "SELECT theme_mode FROM themes WHERE foldername = ?";
|
||||||
|
|
||||||
|
$query = $this->db->query($sql, $clean_theme);
|
||||||
|
|
||||||
|
if ($query) {
|
||||||
|
$result = $query->row();
|
||||||
|
$value = isset($result->theme_mode) ? $result->theme_mode : null;
|
||||||
|
|
||||||
|
return ($value !== null) ? (string) $value : null;
|
||||||
|
} else {
|
||||||
|
log_message('error', 'get_theme_mode failed');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,16 @@ class User_Model extends CI_Model {
|
|||||||
// Update the user
|
// Update the user
|
||||||
$this->db->where('user_id', $fields['id']);
|
$this->db->where('user_id', $fields['id']);
|
||||||
$this->db->update($this->config->item('auth_table'), $data);
|
$this->db->update($this->config->item('auth_table'), $data);
|
||||||
|
|
||||||
|
// Remove static map images in cache to make sure they are updated
|
||||||
|
$this->load->model('Stations');
|
||||||
|
$this->load->model('staticmap_model');
|
||||||
|
$stations = $this->Stations->all_station_ids_of_user($fields['id']);
|
||||||
|
$station_ids = explode(',', $stations);
|
||||||
|
foreach ($station_ids as $station_id) {
|
||||||
|
$this->staticmap_model->remove_static_map_image(trim($station_id));
|
||||||
|
}
|
||||||
|
|
||||||
return OK;
|
return OK;
|
||||||
} else {
|
} else {
|
||||||
return ENOSUCHUSER;
|
return ENOSUCHUSER;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class Visitor_model extends CI_Model {
|
class Visitor_model extends CI_Model {
|
||||||
|
|
||||||
function get_qsos($num, $StationLocationsArray, $band = '') {
|
function get_qsos($num, $StationLocationsArray, $band = '', $continent = '') {
|
||||||
$this->db->select($this->config->item('table_name').'.*, station_profile.*');
|
$this->db->select($this->config->item('table_name').'.*, station_profile.*');
|
||||||
$this->db->from($this->config->item('table_name'));
|
$this->db->from($this->config->item('table_name'));
|
||||||
|
|
||||||
@@ -17,6 +17,15 @@ class Visitor_model extends CI_Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($continent != '') {
|
||||||
|
$this->db->where($this->config->item('table_name').'.COL_CONT', $continent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->group_start();
|
||||||
|
$this->db->where("(" . $this->config->item('table_name') . ".COL_GRIDSQUARE != '' AND " . $this->config->item('table_name') . ".COL_GRIDSQUARE IS NOT NULL)");
|
||||||
|
$this->db->or_where("(" . $this->config->item('table_name') . ".COL_VUCC_GRIDS != '' AND " . $this->config->item('table_name') . ".COL_VUCC_GRIDS IS NOT NULL)");
|
||||||
|
$this->db->group_end();
|
||||||
|
|
||||||
$this->db->where_in($this->config->item('table_name').'.station_id', $StationLocationsArray);
|
$this->db->where_in($this->config->item('table_name').'.station_id', $StationLocationsArray);
|
||||||
$this->db->order_by(''.$this->config->item('table_name').'.COL_TIME_ON', "desc");
|
$this->db->order_by(''.$this->config->item('table_name').'.COL_TIME_ON', "desc");
|
||||||
|
|
||||||
@@ -45,4 +54,30 @@ class Visitor_model extends CI_Model {
|
|||||||
|
|
||||||
return $this->db->query($sql);
|
return $this->db->query($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function qso_is_confirmed($qso, $user_default_confirmation) {
|
||||||
|
$confirmed = false;
|
||||||
|
$qso = (array) $qso;
|
||||||
|
if (strpos($user_default_confirmation, 'Q') !== false) { // QSL
|
||||||
|
if ($qso['COL_QSL_RCVD']=='Y') { $confirmed = true; }
|
||||||
|
}
|
||||||
|
if (strpos($user_default_confirmation, 'L') !== false) { // LoTW
|
||||||
|
if ($qso['COL_LOTW_QSL_RCVD']=='Y') { $confirmed = true; }
|
||||||
|
}
|
||||||
|
if (strpos($user_default_confirmation, 'E') !== false) { // eQsl
|
||||||
|
if ($qso['COL_EQSL_QSL_RCVD']=='Y') { $confirmed = true; }
|
||||||
|
}
|
||||||
|
if (strpos($user_default_confirmation, 'Z') !== false) { // QRZ
|
||||||
|
if ($qso['COL_QRZCOM_QSO_DOWNLOAD_STATUS']=='Y') { $confirmed = true; }
|
||||||
|
}
|
||||||
|
if (strpos($user_default_confirmation, 'C') !== false) { // Clublog
|
||||||
|
if ($qso['COL_CLUBLOG_QSO_DOWNLOAD_STATUS']=='Y') { $confirmed = true; }
|
||||||
|
}
|
||||||
|
return $confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_user_default_confirmation($userid) {
|
||||||
|
$this->load->model('user_model');
|
||||||
|
return $this->user_model->get_by_id($userid)->row()->user_default_confirmation ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,17 @@
|
|||||||
<?php } ?>
|
<?php } ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>php-gd</td>
|
||||||
|
<td>
|
||||||
|
<?php if (in_array('gd', get_loaded_extensions())) { ?>
|
||||||
|
<span class="badge text-bg-success"><?= __("Installed"); ?></span>
|
||||||
|
<?php } else { ?>
|
||||||
|
<span class="badge text-bg-danger"><?= __("Not Installed"); ?></span>
|
||||||
|
<?php } ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|||||||
@@ -9,19 +9,19 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= _pgettext("Map Options", "CQ Zone overlay"); ?></td>
|
<td><?= _pgettext("Map Options", "CQ Zone overlay"); ?></td>
|
||||||
<td><div class="form-check"><input class="form-check-input" name="cqzone_layer" type="checkbox" <?php if (($exportmapoptions['cqzone_layer']->option_value ?? "true") == "true") { echo 'checked'; } ?>></div></td>
|
<td><div class="form-check"><input class="form-check-input" name="cqzone_layer" type="checkbox" <?php if (($exportmapoptions['cqzone_layer']->option_value ?? "false") == "true") { echo 'checked'; } ?>></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= _pgettext("Map Options", "Gridsquare overlay"); ?></td>
|
<td><?= _pgettext("Map Options", "Gridsquare overlay"); ?></td>
|
||||||
<td><div class="form-check"><input class="form-check-input" name="gridsquare_layer" type="checkbox" <?php if (($exportmapoptions['gridsquare_layer']->option_value ?? "true") == "true") { echo 'checked'; } ?>></div></td>
|
<td><div class="form-check"><input class="form-check-input" name="gridsquare_layer" type="checkbox" <?php if (($exportmapoptions['gridsquare_layer']->option_value ?? "false") == "true") { echo 'checked'; } ?>></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= _pgettext("Map Options", "Night shadow overlay"); ?></td>
|
<td><?= _pgettext("Map Options", "Night shadow overlay"); ?></td>
|
||||||
<td><div class="form-check"><input class="form-check-input" name="nightshadow_layer" type="checkbox" <?php if (($exportmapoptions['nightshadow_layer']->option_value ?? "true") == "true") { echo 'checked'; } ?>></div></td>
|
<td><div class="form-check"><input class="form-check-input" name="nightshadow_layer" type="checkbox" <?php if (($exportmapoptions['nightshadow_layer']->option_value ?? "false") == "true") { echo 'checked'; } ?>></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= _pgettext("Map Options", "Path lines"); ?></td>
|
<td><?= _pgettext("Map Options", "Path lines"); ?></td>
|
||||||
<td><div class="form-check"><input class="form-check-input" name="path_lines" type="checkbox" <?php if (($exportmapoptions['path_lines']->option_value ?? "true") == "true") { echo 'checked'; } ?>></div></td>
|
<td><div class="form-check"><input class="form-check-input" name="path_lines" type="checkbox" <?php if (($exportmapoptions['path_lines']->option_value ?? "false") == "true") { echo 'checked'; } ?>></div></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= __("# QSOs shown"); ?></td>
|
<td><?= __("# QSOs shown"); ?></td>
|
||||||
|
|||||||
1
assets/json/geojson/cqzones.geojson
Normal file
1
assets/json/geojson/cqzones.geojson
Normal file
File diff suppressed because one or more lines are too long
1
assets/json/geojson/ituzones.geojson
Normal file
1
assets/json/geojson/ituzones.geojson
Normal file
File diff suppressed because one or more lines are too long
23
src/StaticMap/LICENSE
Normal file
23
src/StaticMap/LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
https://github.com/DantSu/php-osm-static-api
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Franck ALARY
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
116
src/StaticMap/README.md
Normal file
116
src/StaticMap/README.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
[](https://packagist.org/packages/DantSu/php-osm-static-api)
|
||||||
|
[](https://packagist.org/packages/DantSu/php-osm-static-api)
|
||||||
|
[](https://github.com/DantSu/php-osm-static-api/blob/master/LICENSE)
|
||||||
|
|
||||||
|
# PHP OpenStreetMap Static API
|
||||||
|
https://github.com/DantSu/php-osm-static-api
|
||||||
|
|
||||||
|
PHP library to easily get static image from OpenStreetMap with markers, lines, circles and polygons.
|
||||||
|
|
||||||
|
This project uses the [Tile Server](https://wiki.openstreetmap.org/wiki/Tile_servers) of the OpenStreetMap Foundation which runs entirely on donated resources, see [Tile Usage Policy](https://operations.osmfoundation.org/policies/tiles/) for more information.
|
||||||
|
|
||||||
|
## ✨ Supporting
|
||||||
|
|
||||||
|
⭐ Star this repository to support this project. You will contribute to increase the visibility of this library 🙂
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install this library easily with composer :
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
composer require dantsu/php-osm-static-api
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Generate OpenStreetMap static image with markers and polygon :
|
||||||
|
|
||||||
|
```php
|
||||||
|
use \Wavelog\StaticMapImage\OpenStreetMap;
|
||||||
|
use \Wavelog\StaticMapImage\LatLng;
|
||||||
|
use \Wavelog\StaticMapImage\Polygon;
|
||||||
|
use \Wavelog\StaticMapImage\Markers;
|
||||||
|
|
||||||
|
\header('Content-type: image/png');
|
||||||
|
(new OpenStreetMap(new LatLng(44.351933, 2.568113), 17, 600, 400))
|
||||||
|
->addMarkers(
|
||||||
|
(new Markers(__DIR__ . '/resources/marker.png'))
|
||||||
|
->setAnchor(Markers::ANCHOR_CENTER, Markers::ANCHOR_BOTTOM)
|
||||||
|
->addMarker(new LatLng(44.351933, 2.568113))
|
||||||
|
->addMarker(new LatLng(44.351510, 2.570020))
|
||||||
|
->addMarker(new LatLng(44.351873, 2.566250))
|
||||||
|
)
|
||||||
|
->addDraw(
|
||||||
|
(new Polygon('FF0000', 2, 'FF0000DD'))
|
||||||
|
->addPoint(new LatLng(44.351172, 2.571092))
|
||||||
|
->addPoint(new LatLng(44.352097, 2.570045))
|
||||||
|
->addPoint(new LatLng(44.352665, 2.568107))
|
||||||
|
->addPoint(new LatLng(44.352887, 2.566503))
|
||||||
|
->addPoint(new LatLng(44.352806, 2.565972))
|
||||||
|
->addPoint(new LatLng(44.351517, 2.565672))
|
||||||
|
)
|
||||||
|
->getImage()
|
||||||
|
->displayPNG();
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Align and zoom the map to drawings and markers :
|
||||||
|
|
||||||
|
- `->fitToDraws(int $padding = 0)`
|
||||||
|
- `->fitToMarkers(int $padding = 0)`
|
||||||
|
- `->fitToDrawsAndMarkers(int $padding = 0)`
|
||||||
|
- `->fitToPoints(LatLng[] $points, int $padding = 0)`
|
||||||
|
|
||||||
|
`$padding` sets the amount of padding in the borders of the map that shouldn't be accounted for when setting the view to fit bounds. This can be positive or negative according to your needs.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use \Wavelog\StaticMapImage\OpenStreetMap;
|
||||||
|
use \Wavelog\StaticMapImage\LatLng;
|
||||||
|
use \Wavelog\StaticMapImage\Polygon;
|
||||||
|
use \Wavelog\StaticMapImage\Markers;
|
||||||
|
|
||||||
|
\header('Content-type: image/png');
|
||||||
|
(new OpenStreetMap(new LatLng(0, 0), 0, 600, 400))
|
||||||
|
->addMarkers(
|
||||||
|
(new Markers(__DIR__ . '/resources/marker.png'))
|
||||||
|
->setAnchor(Markers::ANCHOR_CENTER, Markers::ANCHOR_BOTTOM)
|
||||||
|
->addMarker(new LatLng(44.351933, 2.568113))
|
||||||
|
->addMarker(new LatLng(44.351510, 2.570020))
|
||||||
|
->addMarker(new LatLng(44.351873, 2.566250))
|
||||||
|
)
|
||||||
|
->addDraw(
|
||||||
|
(new Polygon('FF0000', 2, 'FF0000DD'))
|
||||||
|
->addPoint(new LatLng(44.351172, 2.571092))
|
||||||
|
->addPoint(new LatLng(44.352097, 2.570045))
|
||||||
|
->addPoint(new LatLng(44.352665, 2.568107))
|
||||||
|
->addPoint(new LatLng(44.352887, 2.566503))
|
||||||
|
->addPoint(new LatLng(44.352806, 2.565972))
|
||||||
|
->addPoint(new LatLng(44.351517, 2.565672))
|
||||||
|
)
|
||||||
|
->fitToDraws(10)
|
||||||
|
->getImage()
|
||||||
|
->displayPNG();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
| Class | Description |
|
||||||
|
|--- |--- |
|
||||||
|
| [Circle](./docs/classes/DantSu/OpenStreetMapStaticAPI/Circle.md) | Wavelog\StaticMapImage\Circle draw circle on the map.|
|
||||||
|
| [LatLng](./docs/classes/DantSu/OpenStreetMapStaticAPI/LatLng.md) | Wavelog\StaticMapImage\LatLng define latitude and longitude for map, lines, markers.|
|
||||||
|
| [Line](./docs/classes/DantSu/OpenStreetMapStaticAPI/Line.md) | Wavelog\StaticMapImage\Line draw line on the map.|
|
||||||
|
| [MapData](./docs/classes/DantSu/OpenStreetMapStaticAPI/MapData.md) | Wavelog\StaticMapImage\MapData convert latitude and longitude to image pixel position.|
|
||||||
|
| [Markers](./docs/classes/DantSu/OpenStreetMapStaticAPI/Markers.md) | Wavelog\StaticMapImage\Markers display markers on the map.|
|
||||||
|
| [OpenStreetMap](./docs/classes/DantSu/OpenStreetMapStaticAPI/OpenStreetMap.md) | Wavelog\StaticMapImage\OpenStreetMap is a PHP library created for easily get static image from OpenStreetMap with markers, lines, polygons and circles.|
|
||||||
|
| [Polygon](./docs/classes/DantSu/OpenStreetMapStaticAPI/Polygon.md) | Wavelog\StaticMapImage\Polygon draw polygon on the map.|
|
||||||
|
| [TileLayer](./docs/classes/DantSu/OpenStreetMapStaticAPI/TileLayer.md) | Wavelog\StaticMapImage\TileLayer define tile server url and related configuration|
|
||||||
|
| [XY](./docs/classes/DantSu/OpenStreetMapStaticAPI/XY.md) | Wavelog\StaticMapImage\XY define X and Y pixel position for map, lines, markers.|
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please fork this repository and contribute back using pull requests.
|
||||||
|
|
||||||
|
Any contributions, large or small, major features, bug fixes, are welcomed and appreciated but will be thoroughly reviewed.
|
||||||
|
|
||||||
132
src/StaticMap/src/Circle.php
Normal file
132
src/StaticMap/src/Circle.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\Interfaces\Draw;
|
||||||
|
use Wavelog\StaticMapImage\Utils\GeographicConverter;
|
||||||
|
use DantSu\PHPImageEditor\Geometry2D;
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\Circle draw circle on the map.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class Circle implements Draw
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $strokeColor = '000000';
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $strokeWeight = 1;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $fillColor = '000000';
|
||||||
|
/**
|
||||||
|
* @var LatLng
|
||||||
|
*/
|
||||||
|
private $center = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LatLng
|
||||||
|
*/
|
||||||
|
private $edge = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool wrap around the world or not
|
||||||
|
*/
|
||||||
|
private $wrap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Circle constructor.
|
||||||
|
*
|
||||||
|
* @param LatLng $center Latitude and longitude of the circle center
|
||||||
|
* @param string $strokeColor Hexadecimal string color
|
||||||
|
* @param int $strokeWeight pixel weight of the line
|
||||||
|
* @param string $fillColor Hexadecimal string color
|
||||||
|
*/
|
||||||
|
public function __construct(LatLng $center, string $strokeColor, int $strokeWeight, string $fillColor, bool $wrap = false)
|
||||||
|
{
|
||||||
|
$this->center = $center;
|
||||||
|
$this->edge = $center;
|
||||||
|
$this->strokeColor = \str_replace('#', '', $strokeColor);
|
||||||
|
$this->strokeWeight = $strokeWeight > 0 ? $strokeWeight : 0;
|
||||||
|
$this->fillColor = \str_replace('#', '', $fillColor);
|
||||||
|
$this->wrap = $wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a latitude and longitude to define the radius of the circle.
|
||||||
|
*
|
||||||
|
* @param LatLng $edge Latitude and longitude of the edge point of a circle
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setEdgePoint(LatLng $edge): Circle
|
||||||
|
{
|
||||||
|
$this->edge = $edge;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the radius of the circle in meters.
|
||||||
|
*
|
||||||
|
* @param float $radius radius of a circle in meters
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setRadius(float $radius): Circle
|
||||||
|
{
|
||||||
|
$this->edge = GeographicConverter::metersToLatLng($this->center, $radius, 45);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the circle on the map image.
|
||||||
|
*
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
*
|
||||||
|
* @param Image $image The map image (An instance of DantSu\PHPImageEditor\Image)
|
||||||
|
* @param MapData $mapData Bounding box of the map
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function draw(Image $image, MapData $mapData): Circle
|
||||||
|
{
|
||||||
|
$center = $mapData->convertLatLngToPxPosition($this->center, $this->wrap);
|
||||||
|
$edge = $mapData->convertLatLngToPxPosition($this->edge, $this->wrap);
|
||||||
|
|
||||||
|
$angleAndLenght = Geometry2D::getAngleAndLengthFromPoints($center->getX(), $center->getY(), $edge->getX(), $edge->getY());
|
||||||
|
$length = \round($angleAndLenght['length'] + $this->strokeWeight / 2);
|
||||||
|
|
||||||
|
$dImage = Image::newCanvas($image->getWidth(), $image->getHeight());
|
||||||
|
|
||||||
|
if ($this->strokeWeight > 0) {
|
||||||
|
$dImage->drawCircle($center->getX(), $center->getY(), $length * 2, $this->strokeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dImage->drawCircle($center->getX(), $center->getY(), ($length - $this->strokeWeight) * 2, $this->fillColor);
|
||||||
|
|
||||||
|
$image->pasteOn($dImage, 0, 0);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bounding box of the shape
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public function getBoundingBox(): array
|
||||||
|
{
|
||||||
|
$distance = GeographicConverter::latLngToMeters($this->center, $this->edge) * 1.4142;
|
||||||
|
return [
|
||||||
|
GeographicConverter::metersToLatLng($this->center, $distance, 315),
|
||||||
|
GeographicConverter::metersToLatLng($this->center, $distance, 135)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/StaticMap/src/Geometry2D.php
Normal file
61
src/StaticMap/src/Geometry2D.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DantSu\PHPImageEditor;
|
||||||
|
|
||||||
|
class Geometry2D
|
||||||
|
{
|
||||||
|
public static function degrees0to360(float $angle): float
|
||||||
|
{
|
||||||
|
while ($angle < 0 || $angle >= 360) {
|
||||||
|
if ($angle < 0) $angle += 360;
|
||||||
|
elseif ($angle >= 360) $angle -= 360;
|
||||||
|
}
|
||||||
|
return $angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDstXY(float $originX, float $originY, float $angle, float $length): array
|
||||||
|
{
|
||||||
|
$angle = 360 - $angle;
|
||||||
|
return [
|
||||||
|
'x' => $originX + \cos($angle * M_PI / 180) * $length,
|
||||||
|
'y' => $originY + \sin($angle * M_PI / 180) * $length
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDstXYRounded(float $originX, float $originY, float $angle, float $length): array
|
||||||
|
{
|
||||||
|
$xy = Geometry2D::getDstXY($originX, $originY, $angle, $length);
|
||||||
|
return [
|
||||||
|
'x' => \round($xy['x']),
|
||||||
|
'y' => \round($xy['y'])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAngleAndLengthFromPoints(float $originX, float $originY, float $dstX, float $dstY): array
|
||||||
|
{
|
||||||
|
$width = $dstX - $originX;
|
||||||
|
$height = $dstY - $originY;
|
||||||
|
$diameter = \sqrt(\pow($width, 2) + \pow($height, 2));
|
||||||
|
|
||||||
|
if($width == 0) {
|
||||||
|
$angle = 90;
|
||||||
|
} elseif ($height == 0) {
|
||||||
|
$angle = 0;
|
||||||
|
} else {
|
||||||
|
$angle = \atan2(\abs($height), \abs($width)) * 180.0 / M_PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($width < 0 && $height < 0) {
|
||||||
|
$angle += 180;
|
||||||
|
} elseif ($width < 0) {
|
||||||
|
$angle = 180 - $angle;
|
||||||
|
} elseif ($height < 0) {
|
||||||
|
$angle = 360 - $angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'angle' => 360 - $angle,
|
||||||
|
'length' => $diameter
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
1369
src/StaticMap/src/Image.php
Normal file
1369
src/StaticMap/src/Image.php
Normal file
File diff suppressed because it is too large
Load Diff
14
src/StaticMap/src/Interfaces/Draw.php
Normal file
14
src/StaticMap/src/Interfaces/Draw.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage\Interfaces;
|
||||||
|
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\MapData;
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
interface Draw
|
||||||
|
{
|
||||||
|
public function getBoundingBox(): array;
|
||||||
|
|
||||||
|
public function draw(Image $image, MapData $mapData);
|
||||||
|
}
|
||||||
54
src/StaticMap/src/LatLng.php
Normal file
54
src/StaticMap/src/LatLng.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\LatLng define latitude and longitude for map, lines, markers...
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class LatLng
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var float Latitude
|
||||||
|
*/
|
||||||
|
private $lat = 0.0;
|
||||||
|
/**
|
||||||
|
* @var float Longitude
|
||||||
|
*/
|
||||||
|
private $lng = 0.0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LatLng constructor.
|
||||||
|
* @param float $lat Latitude
|
||||||
|
* @param float $lng Longitude
|
||||||
|
*/
|
||||||
|
public function __construct(float $lat, float $lng)
|
||||||
|
{
|
||||||
|
$this->lat = $lat;
|
||||||
|
$this->lng = $lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latitude
|
||||||
|
* @return float Latitude
|
||||||
|
*/
|
||||||
|
public function getLat(): float
|
||||||
|
{
|
||||||
|
return $this->lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get longitude
|
||||||
|
* @return float Longitude
|
||||||
|
*/
|
||||||
|
public function getLng(): float
|
||||||
|
{
|
||||||
|
return $this->lng;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/StaticMap/src/Line.php
Normal file
135
src/StaticMap/src/Line.php
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
require('./src/StaticMap/src/Interfaces/Draw.php');
|
||||||
|
require('./src/StaticMap/src/Utils/GeographicConverter.php');
|
||||||
|
use Wavelog\StaticMapImage\Interfaces\Draw;
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
use Wavelog\StaticMapImage\LatLng;
|
||||||
|
use Wavelog\StaticMapImage\MapData;
|
||||||
|
use Wavelog\StaticMapImage\Utils\GeographicConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\Line draw line on the map.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class Line implements Draw
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $color = '000000';
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $weight = 1;
|
||||||
|
/**
|
||||||
|
* @var LatLng[]
|
||||||
|
*/
|
||||||
|
private $points = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool wrap around the world or not
|
||||||
|
*/
|
||||||
|
private $wrap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Line constructor.
|
||||||
|
* @param string $color Hexadecimal string color
|
||||||
|
* @param int $weight pixel weight of the line
|
||||||
|
*/
|
||||||
|
public function __construct(string $color, int $weight, bool $wrap = false)
|
||||||
|
{
|
||||||
|
$this->color = \str_replace('#', '', $color);
|
||||||
|
$this->weight = $weight;
|
||||||
|
$this->wrap = $wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a latitude and longitude to the multi-points line
|
||||||
|
* @param LatLng $latLng Latitude and longitude to add
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addPoint(LatLng $latLng): Line
|
||||||
|
{
|
||||||
|
$this->points[] = $latLng;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the line on the map image.
|
||||||
|
*
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
*
|
||||||
|
* @param Image $image The map image (An instance of DantSu\PHPImageEditor\Image)
|
||||||
|
* @param MapData $mapData Bounding box of the map
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function draw(Image $image, MapData $mapData): Line
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var $cPoints XY[]
|
||||||
|
*/
|
||||||
|
$cPoints = \array_map(
|
||||||
|
function (LatLng $p) use ($mapData) {
|
||||||
|
return $mapData->convertLatLngToPxPosition($p, !$this->wrap);
|
||||||
|
},
|
||||||
|
$this->points
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ((array) $cPoints as $k => $point) {
|
||||||
|
if (isset($cPoints[$k - 1])) {
|
||||||
|
$image->drawLine($cPoints[$k - 1]->getX(), $cPoints[$k - 1]->getY(), $point->getX(), $point->getY(), $this->weight, $this->color);
|
||||||
|
|
||||||
|
// do the same left and right if $wrap is disabled. 'Lines' are special here
|
||||||
|
if ($this->wrap) {
|
||||||
|
$image->drawLine($cPoints[$k - 1]->getX() + $image->getWidth(), $cPoints[$k - 1]->getY(), $point->getX() + $image->getWidth(), $point->getY(), $this->weight, $this->color);
|
||||||
|
$image->drawLine($cPoints[$k - 1]->getX() - $image->getWidth(), $cPoints[$k - 1]->getY(), $point->getX() - $image->getWidth(), $point->getY(), $this->weight, $this->color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bounding box of the shape
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public function getBoundingBox(): array
|
||||||
|
{
|
||||||
|
return MapData::getBoundingBoxFromPoints($this->points);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geodesic points between two coordinates
|
||||||
|
*
|
||||||
|
* @param LatLng $start
|
||||||
|
* @param LatLng $end
|
||||||
|
*
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public function geodesicPoints(LatLng $start, LatLng $end): array {
|
||||||
|
$points = [$start];
|
||||||
|
$totalDistance = GeographicConverter::latLngToMeters($start, $end);
|
||||||
|
$distanceInterval = 10000;
|
||||||
|
$currentDistance = 0;
|
||||||
|
|
||||||
|
while ($currentDistance + $distanceInterval < $totalDistance) {
|
||||||
|
$currentDistance += $distanceInterval;
|
||||||
|
|
||||||
|
$lastPoint = end($points);
|
||||||
|
$angle = GeographicConverter::getBearing($lastPoint, $end);
|
||||||
|
|
||||||
|
$nextPoint = GeographicConverter::metersToLatLng($lastPoint, $distanceInterval, $angle);
|
||||||
|
$points[] = $nextPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
$points[] = $end;
|
||||||
|
return $points;
|
||||||
|
}
|
||||||
|
}
|
||||||
361
src/StaticMap/src/MapData.php
Normal file
361
src/StaticMap/src/MapData.php
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\Utils\GeographicConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\MapData convert latitude and longitude to image pixel position.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class MapData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var LatLng Center of the map
|
||||||
|
*/
|
||||||
|
private $centerMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert longitude and zoom to horizontal OpenStreetMap tile number and pixel position.
|
||||||
|
* @param float $lon Longitude
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $tileSize Tile size
|
||||||
|
* @param bool wrap around the globe
|
||||||
|
* @return int[] OpenStreetMap tile id and pixel position of the given longitude and zoom
|
||||||
|
*/
|
||||||
|
public function lngToXTile(float $lon, int $zoom, int $tileSize, bool $wrap = false): array {
|
||||||
|
// Adjust longitude based on the map's center longitude
|
||||||
|
$centerLon = $this->centerMap->getLng();
|
||||||
|
$wrappedLon = fmod(($lon - $centerLon + 180), 360) - 180 + $centerLon;
|
||||||
|
if ($wrap) {
|
||||||
|
$x = ($wrappedLon + 180) / 360 * \pow(2, $zoom);
|
||||||
|
} else {
|
||||||
|
$x = ($lon + 180) / 360 * \pow(2, $zoom);
|
||||||
|
}
|
||||||
|
// Continue with the existing calculation
|
||||||
|
$tile = \floor($x);
|
||||||
|
return [
|
||||||
|
'id' => $tile,
|
||||||
|
'position' => \round($tileSize * ($x - $tile))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert latitude and zoom to vertical OpenStreetMap tile number and pixel position.
|
||||||
|
* @param float $lat Latitude
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $tileSize Tile size
|
||||||
|
* @return int[] OpenStreetMap tile id and pixel position of the given latitude and zoom
|
||||||
|
*/
|
||||||
|
public static function latToYTile(float $lat, int $zoom, int $tileSize): array {
|
||||||
|
$y = (1 - \log(\tan(\deg2rad($lat)) + 1 / \cos(\deg2rad($lat))) / M_PI) / 2 * \pow(2, $zoom);
|
||||||
|
$tile = \floor($y);
|
||||||
|
return [
|
||||||
|
'id' => $tile,
|
||||||
|
'position' => \round($tileSize * ($y - $tile))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert horizontal OpenStreetMap tile number ad zoom to longitude.
|
||||||
|
* @param int $id Horizontal OpenStreetMap tile id
|
||||||
|
* @param int $position Horizontal pixel position on tile
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $tileSize Tile size
|
||||||
|
* @return float Longitude of the given OpenStreetMap tile id and zoom
|
||||||
|
*/
|
||||||
|
public static function xTileToLng(int $id, int $position, int $zoom, int $tileSize): float {
|
||||||
|
return ($id + $position / $tileSize) / \pow(2, $zoom) * 360 - 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert vertical OpenStreetMap tile number and zoom to latitude.
|
||||||
|
* @param int $id Vertical OpenStreetMap tile id
|
||||||
|
* @param int $position Vertical pixel position on tile
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $tileSize Tile size
|
||||||
|
* @return float Latitude of the given OpenStreetMap tile id and zoom
|
||||||
|
*/
|
||||||
|
public static function yTileToLat(int $id, int $position, int $zoom, int $tileSize): float {
|
||||||
|
return \rad2deg(\atan(\sinh(M_PI * (1 - 2 * ($id + $position / $tileSize) / \pow(2, $zoom)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform array of LatLng to bounding box
|
||||||
|
*
|
||||||
|
* @param LatLng[] $points
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public static function getBoundingBoxFromPoints(array $points): array {
|
||||||
|
$minLat = 360;
|
||||||
|
$maxLat = -360;
|
||||||
|
$minLng = 360;
|
||||||
|
$maxLng = -360;
|
||||||
|
foreach ($points as $point) {
|
||||||
|
if ($point->getLat() < $minLat) {
|
||||||
|
$minLat = $point->getLat();
|
||||||
|
}
|
||||||
|
if ($point->getLat() > $maxLat) {
|
||||||
|
$maxLat = $point->getLat();
|
||||||
|
}
|
||||||
|
if ($point->getLng() < $minLng) {
|
||||||
|
$minLng = $point->getLng();
|
||||||
|
}
|
||||||
|
if ($point->getLng() > $maxLng) {
|
||||||
|
$maxLng = $point->getLng();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [new LatLng($maxLat, $minLng), new LatLng($minLat, $maxLng)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get center and zoom from two points.
|
||||||
|
*
|
||||||
|
* @param LatLng $topLeft
|
||||||
|
* @param LatLng $bottomRight
|
||||||
|
* @param int $padding
|
||||||
|
* @param int $imageWidth
|
||||||
|
* @param int $imageHeight
|
||||||
|
* @param int $tileSize
|
||||||
|
* @return array center : LatLng, zoom : int
|
||||||
|
*/
|
||||||
|
public static function getCenterAndZoomFromBoundingBox(LatLng $topLeft, LatLng $bottomRight, int $padding, int $imageWidth, int $imageHeight, int $tileSize): array {
|
||||||
|
$zoom = 20;
|
||||||
|
$padding *= 2;
|
||||||
|
$topTilePos = MapData::latToYTile($topLeft->getLat(), $zoom, $tileSize);
|
||||||
|
$bottomTilePos = MapData::latToYTile($bottomRight->getLat(), $zoom, $tileSize);
|
||||||
|
$leftTilePos = MapData::lngToXTile($topLeft->getLng(), $zoom, $tileSize);
|
||||||
|
$rightTilePos = MapData::lngToXTile($bottomRight->getLng(), $zoom, $tileSize);
|
||||||
|
$pxZoneWidth = ($rightTilePos['id'] - $leftTilePos['id']) * $tileSize + $rightTilePos['position'] - $leftTilePos['position'];
|
||||||
|
$pxZoneHeight = ($bottomTilePos['id'] - $topTilePos['id']) * $tileSize + $bottomTilePos['position'] - $topTilePos['position'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'center' => GeographicConverter::getCenter($topLeft, $bottomRight),
|
||||||
|
'zoom' => \intval(
|
||||||
|
\floor(
|
||||||
|
\log(
|
||||||
|
\min(
|
||||||
|
1,
|
||||||
|
($imageHeight - $padding) / $pxZoneHeight,
|
||||||
|
($imageWidth - $padding) / $pxZoneWidth
|
||||||
|
) * \pow(2, $zoom)
|
||||||
|
) / 0.69314
|
||||||
|
)
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int zoom
|
||||||
|
*/
|
||||||
|
private $zoom;
|
||||||
|
/**
|
||||||
|
* @var int tile size
|
||||||
|
*/
|
||||||
|
private $tileSize;
|
||||||
|
/**
|
||||||
|
* @var XY Width and height of the image in pixel
|
||||||
|
*/
|
||||||
|
private $outputSize;
|
||||||
|
/**
|
||||||
|
* @var XY top left tile numbers
|
||||||
|
*/
|
||||||
|
private $tileTopLeft;
|
||||||
|
/**
|
||||||
|
* @var XY bottom right tile numbers
|
||||||
|
*/
|
||||||
|
private $tileBottomRight;
|
||||||
|
/**
|
||||||
|
* @var XY left and top pixels to crop to fit final image size
|
||||||
|
*/
|
||||||
|
private $mapCropTopLeft;
|
||||||
|
/**
|
||||||
|
* @var XY bottom and right pixels to crop to fit final image size
|
||||||
|
*/
|
||||||
|
private $mapCropBottomRight;
|
||||||
|
/**
|
||||||
|
* @var LatLng Latitude and longitude of top left image
|
||||||
|
*/
|
||||||
|
private $latLngTopLeft;
|
||||||
|
/**
|
||||||
|
* @var LatLng Latitude and longitude of top right image
|
||||||
|
*/
|
||||||
|
private $latLngTopRight;
|
||||||
|
/**
|
||||||
|
* @var LatLng Latitude and longitude of bottom left image
|
||||||
|
*/
|
||||||
|
private $latLngBottomLeft;
|
||||||
|
/**
|
||||||
|
* @var LatLng Latitude and longitude of bottom right image
|
||||||
|
*/
|
||||||
|
private $latLngBottomRight;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param LatLng $centerMap
|
||||||
|
* @param int $zoom
|
||||||
|
* @param XY $outputSize
|
||||||
|
* @param int $tileSize
|
||||||
|
*/
|
||||||
|
public function __construct(LatLng $centerMap, int $zoom, XY $outputSize, int $tileSize) {
|
||||||
|
$this->centerMap = $centerMap;
|
||||||
|
$this->zoom = $zoom;
|
||||||
|
$this->outputSize = $outputSize;
|
||||||
|
$this->tileSize = $tileSize;
|
||||||
|
|
||||||
|
$x = static::lngToXTile($centerMap->getLng(), $zoom, $this->tileSize);
|
||||||
|
$y = static::latToYTile($centerMap->getLat(), $zoom, $this->tileSize);
|
||||||
|
|
||||||
|
$startX = \floor($outputSize->getX() / 2 - $x['position']);
|
||||||
|
$startY = \floor($outputSize->getY() / 2 - $y['position']);
|
||||||
|
|
||||||
|
|
||||||
|
$rightSize = $outputSize->getX() - $startX;
|
||||||
|
$bottomSize = $outputSize->getY() - $startY;
|
||||||
|
|
||||||
|
$this->mapCropTopLeft = new XY(
|
||||||
|
$startX < 0 ? \abs($startX) : ($startX % $this->tileSize == 0 ? 0 : $this->tileSize - $startX % $this->tileSize),
|
||||||
|
$startY < 0 ? \abs($startY) : ($startY % $this->tileSize == 0 ? 0 : $this->tileSize - $startY % $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->mapCropBottomRight = new XY(
|
||||||
|
($rightSize % $this->tileSize == 0 ? 0 : $this->tileSize - $rightSize % $this->tileSize),
|
||||||
|
($bottomSize % $this->tileSize == 0 ? 0 : $this->tileSize - $bottomSize % $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->tileTopLeft = new XY(
|
||||||
|
$x['id'] - \ceil($startX / $this->tileSize),
|
||||||
|
$y['id'] - \ceil($startY / $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->tileBottomRight = new XY(
|
||||||
|
$x['id'] - 1 + \ceil($rightSize / $this->tileSize),
|
||||||
|
$y['id'] - 1 + \ceil($bottomSize / $this->tileSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->latLngTopLeft = new LatLng(
|
||||||
|
static::yTileToLat($this->tileTopLeft->getY(), $this->mapCropTopLeft->getY(), $zoom, $this->tileSize),
|
||||||
|
static::xTileToLng($this->tileTopLeft->getX(), $this->mapCropTopLeft->getX(), $zoom, $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->latLngTopRight = new LatLng(
|
||||||
|
static::yTileToLat($this->tileTopLeft->getY(), $this->mapCropTopLeft->getY(), $zoom, $this->tileSize),
|
||||||
|
static::xTileToLng($this->tileBottomRight->getX(), $this->tileSize - $this->mapCropBottomRight->getX(), $zoom, $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->latLngBottomLeft = new LatLng(
|
||||||
|
static::yTileToLat($this->tileBottomRight->getY(), $this->tileSize - $this->mapCropBottomRight->getY(), $zoom, $this->tileSize),
|
||||||
|
static::xTileToLng($this->tileTopLeft->getX(), $this->mapCropTopLeft->getX(), $zoom, $this->tileSize)
|
||||||
|
);
|
||||||
|
$this->latLngBottomRight = new LatLng(
|
||||||
|
static::yTileToLat($this->tileBottomRight->getY(), $this->tileSize - $this->mapCropBottomRight->getY(), $zoom, $this->tileSize),
|
||||||
|
static::xTileToLng($this->tileBottomRight->getX(), $this->tileSize - $this->mapCropBottomRight->getX(), $zoom, $this->tileSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latitude and longitude of top left image
|
||||||
|
* @return LatLng Latitude and longitude of top left image
|
||||||
|
*/
|
||||||
|
public function getLatLngTopLeft(): LatLng {
|
||||||
|
return $this->latLngTopLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latitude and longitude of top right image
|
||||||
|
* @return LatLng Latitude and longitude of top right image
|
||||||
|
*/
|
||||||
|
public function getLatLngTopRight(): LatLng {
|
||||||
|
return $this->latLngTopRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latitude and longitude of bottom left image
|
||||||
|
* @return LatLng Latitude and longitude of bottom left image
|
||||||
|
*/
|
||||||
|
public function getLatLngBottomLeft(): LatLng {
|
||||||
|
return $this->latLngBottomLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latitude and longitude of bottom right image
|
||||||
|
* @return LatLng Latitude and longitude of bottom right image
|
||||||
|
*/
|
||||||
|
public function getLatLngBottomRight(): LatLng {
|
||||||
|
return $this->latLngBottomRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get width and height of the image in pixel
|
||||||
|
* @return XY Width and height of the image in pixel
|
||||||
|
*/
|
||||||
|
public function getOutputSize(): XY {
|
||||||
|
return $this->outputSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the zoom
|
||||||
|
* @return int zoom
|
||||||
|
*/
|
||||||
|
public function getZoom(): int {
|
||||||
|
return $this->zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tile size
|
||||||
|
* @return int tile size
|
||||||
|
*/
|
||||||
|
public function getTileSize(): int {
|
||||||
|
return $this->tileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get top left tile numbers
|
||||||
|
* @return XY top left tile numbers
|
||||||
|
*/
|
||||||
|
public function getTileTopLeft(): XY {
|
||||||
|
return $this->tileTopLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bottom right tile numbers
|
||||||
|
* @return XY bottom right tile numbers
|
||||||
|
*/
|
||||||
|
public function getTileBottomRight(): XY {
|
||||||
|
return $this->tileBottomRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get top left crop pixels
|
||||||
|
* @return XY top left crop pixels
|
||||||
|
*/
|
||||||
|
public function getMapCropTopLeft(): XY {
|
||||||
|
return $this->mapCropTopLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bottom right crop pixels
|
||||||
|
* @return XY bottom right crop pixels
|
||||||
|
*/
|
||||||
|
public function getMapCropBottomRight(): XY {
|
||||||
|
return $this->mapCropBottomRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a latitude and longitude to a XY pixel position in the image
|
||||||
|
* @param LatLng $latLng Latitude and longitude to be converted
|
||||||
|
* @return XY Pixel position of latitude and longitude in the image
|
||||||
|
*/
|
||||||
|
public function convertLatLngToPxPosition(LatLng $latLng, $wrap = false): XY {
|
||||||
|
$x = static::lngToXTile($latLng->getLng(), $this->zoom, $this->tileSize, $wrap); // we only need to wrap X
|
||||||
|
$y = static::latToYTile($latLng->getLat(), $this->zoom, $this->tileSize);
|
||||||
|
|
||||||
|
return new XY(
|
||||||
|
(int)(($x['id'] - $this->tileTopLeft->getX()) * $this->tileSize - $this->mapCropTopLeft->getX() + $x['position']),
|
||||||
|
(int)(($y['id'] - $this->tileTopLeft->getY()) * $this->tileSize - $this->mapCropTopLeft->getY() + $y['position'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/StaticMap/src/Markers.php
Normal file
145
src/StaticMap/src/Markers.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\Markers display markers on the map.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class Markers {
|
||||||
|
const ANCHOR_LEFT = 'left';
|
||||||
|
const ANCHOR_CENTER = 'center';
|
||||||
|
const ANCHOR_RIGHT = 'right';
|
||||||
|
const ANCHOR_TOP = 'top';
|
||||||
|
const ANCHOR_MIDDLE = 'middle';
|
||||||
|
const ANCHOR_BOTTOM = 'bottom';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Image Image of the marker
|
||||||
|
*/
|
||||||
|
private $image;
|
||||||
|
/**
|
||||||
|
* @var bool Wrap around the globe or not
|
||||||
|
*/
|
||||||
|
private $wrap;
|
||||||
|
/**
|
||||||
|
* @var string|int Horizontal anchor of the marker image
|
||||||
|
*/
|
||||||
|
private $horizontalAnchor = Markers::ANCHOR_CENTER;
|
||||||
|
/**
|
||||||
|
* @var string|int Vertical anchor of the marker image
|
||||||
|
*/
|
||||||
|
private $verticalAnchor = Markers::ANCHOR_MIDDLE;
|
||||||
|
/**
|
||||||
|
* @var LatLng[] Latitudes and longitudes of the markers
|
||||||
|
*/
|
||||||
|
private $coordinates = [];
|
||||||
|
|
||||||
|
public function __construct($pathImage, $wrap = false) {
|
||||||
|
$this->wrap = $wrap;
|
||||||
|
$this->image = Image::fromPath($pathImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a marker on the map.
|
||||||
|
* @param LatLng $coordinate Latitude and longitude of the marker
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addMarker(LatLng $coordinate): Markers {
|
||||||
|
$this->coordinates[] = $coordinate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize the marker image.
|
||||||
|
*
|
||||||
|
* @param int $width Width of the marker
|
||||||
|
* @param int $height Height of the marker
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function resizeMarker(int $width, int $height): Markers {
|
||||||
|
$this->image = $this->image->resize($width, $height);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the anchor point of the image marker.
|
||||||
|
* @param int|string $horizontalAnchor Horizontal anchor in pixel or you can use `Markers::ANCHOR_LEFT`, `Markers::ANCHOR_CENTER`, `Markers::ANCHOR_RIGHT`
|
||||||
|
* @param int|string $verticalAnchor Vertical anchor in pixel or you can use `Markers::ANCHOR_TOP`, `Markers::ANCHOR_MIDDLE`, `Markers::ANCHOR_BOTTOM`
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setAnchor($horizontalAnchor, $verticalAnchor): Markers {
|
||||||
|
$this->horizontalAnchor = $horizontalAnchor;
|
||||||
|
$this->verticalAnchor = $verticalAnchor;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw markers on the image map.
|
||||||
|
*
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
* @param Image $image The map image (An instance of DantSu\PHPImageEditor\Image)
|
||||||
|
* @param MapData $mapData Bounding box of the map
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function draw(Image $image, MapData $mapData): Markers {
|
||||||
|
$imageMarginLeft = $this->horizontalAnchor;
|
||||||
|
$offsetX = 1;
|
||||||
|
$offsetY = 6;
|
||||||
|
|
||||||
|
switch ($imageMarginLeft) {
|
||||||
|
case Markers::ANCHOR_LEFT:
|
||||||
|
$imageMarginLeft = 0;
|
||||||
|
break;
|
||||||
|
case Markers::ANCHOR_CENTER:
|
||||||
|
$imageMarginLeft = $this->image->getWidth() / 2;
|
||||||
|
break;
|
||||||
|
case Markers::ANCHOR_RIGHT:
|
||||||
|
$imageMarginLeft = $this->image->getWidth();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$imageMarginTop = $this->verticalAnchor;
|
||||||
|
switch ($imageMarginTop) {
|
||||||
|
case Markers::ANCHOR_TOP:
|
||||||
|
$imageMarginTop = 0;
|
||||||
|
break;
|
||||||
|
case Markers::ANCHOR_MIDDLE:
|
||||||
|
$imageMarginTop = $this->image->getHeight() / 2;
|
||||||
|
break;
|
||||||
|
case Markers::ANCHOR_BOTTOM:
|
||||||
|
$imageMarginTop = $this->image->getHeight();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->coordinates as $coordinate) {
|
||||||
|
$xy = $mapData->convertLatLngToPxPosition($coordinate);
|
||||||
|
$image->pasteOn($this->image, $xy->getX() + $offsetX - $imageMarginLeft, $xy->getY() + $offsetY - $imageMarginTop);
|
||||||
|
|
||||||
|
// pasteOn again for wrapping left and right
|
||||||
|
if ($this->wrap) {
|
||||||
|
$image->pasteOn($this->image, $xy->getX() + $offsetX - $imageMarginLeft + $image->getWidth(), $xy->getY() + $offsetY - $imageMarginTop);
|
||||||
|
$image->pasteOn($this->image, $xy->getX() + $offsetX - $imageMarginLeft - $image->getWidth(), $xy->getY() + $offsetY - $imageMarginTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bounding box of markers
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public function getBoundingBox(): array {
|
||||||
|
return MapData::getBoundingBoxFromPoints($this->coordinates);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
src/StaticMap/src/OpenStreetMap.php
Normal file
308
src/StaticMap/src/OpenStreetMap.php
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\Interfaces\Draw;
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\OpenStreetMap is a PHP library created for easily get static image from OpenStreetMap with markers, lines, polygons and circles.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class OpenStreetMap
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create new instance of OpenStreetMap.
|
||||||
|
* @param LatLng $centerMap Latitude and longitude of the map center
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $imageWidth Width of the generated map image
|
||||||
|
* @param int $imageHeight Height of the generated map image
|
||||||
|
* @param TileLayer $tileLayer Tile server configuration, defaults to OpenStreetMaps tile server
|
||||||
|
* @param int $tileSize Tile size in pixels
|
||||||
|
*/
|
||||||
|
public static function createFromLatLngZoom(LatLng $centerMap, int $zoom, int $imageWidth, int $imageHeight, TileLayer $tileLayer = null, int $tileSize = 256): OpenStreetMap
|
||||||
|
{
|
||||||
|
return new OpenStreetMap($centerMap, $zoom, $imageWidth, $imageHeight, $tileLayer, $tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new instance of OpenStreetMap.
|
||||||
|
* @param LatLng $topLeft Latitude and longitude of the map top left
|
||||||
|
* @param LatLng $bottomRight Latitude and longitude of the map bottom right
|
||||||
|
* @param int $padding Padding to add before top left and after bottom right position.
|
||||||
|
* @param int $imageWidth Width of the generated map image
|
||||||
|
* @param int $imageHeight Height of the generated map image
|
||||||
|
* @param TileLayer $tileLayer Tile server configuration, defaults to OpenStreetMaps tile server
|
||||||
|
* @param int $tileSize Tile size in pixels
|
||||||
|
* @return OpenStreetMap
|
||||||
|
*/
|
||||||
|
public static function createFromBoundingBox(LatLng $topLeft, LatLng $bottomRight, int $padding, int $imageWidth, int $imageHeight, TileLayer $tileLayer = null, int $tileSize = 256): OpenStreetMap
|
||||||
|
{
|
||||||
|
if ($tileLayer === null) {
|
||||||
|
$tileLayer = TileLayer::defaultTileLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
$latLngZoom = MapData::getCenterAndZoomFromBoundingBox($topLeft, $bottomRight, $padding, $imageWidth, $imageHeight, $tileSize);
|
||||||
|
return new OpenStreetMap($latLngZoom['center'], $latLngZoom['zoom'], $imageWidth, $imageHeight, $tileLayer, $tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MapData Data about the generated map (bounding box, size, OSM tile ids...)
|
||||||
|
*/
|
||||||
|
protected $mapData;
|
||||||
|
/**
|
||||||
|
* @var TileLayer[] Array of TileLayer instances
|
||||||
|
*/
|
||||||
|
protected $layers = [];
|
||||||
|
/**
|
||||||
|
* @var Markers[] Array of Markers instances
|
||||||
|
*/
|
||||||
|
protected $markers = [];
|
||||||
|
/**
|
||||||
|
* @var Draw[] Array of Line instances
|
||||||
|
*/
|
||||||
|
protected $draws = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenStreetMap constructor.
|
||||||
|
* @param LatLng $centerMap Latitude and longitude of the map center
|
||||||
|
* @param int $zoom Zoom
|
||||||
|
* @param int $imageWidth Width of the generated map image
|
||||||
|
* @param int $imageHeight Height of the generated map image
|
||||||
|
* @param TileLayer $tileLayer Tile server configuration, defaults to OpenStreetMaps tile server
|
||||||
|
* @param int $tileSize Tile size in pixels
|
||||||
|
*/
|
||||||
|
public function __construct(LatLng $centerMap, int $zoom, int $imageWidth, int $imageHeight, TileLayer $tileLayer = null, int $tileSize = 256)
|
||||||
|
{
|
||||||
|
if ($tileLayer === null) {
|
||||||
|
$tileLayer = TileLayer::defaultTileLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mapData = new MapData($centerMap, $tileLayer->checkZoom($zoom), new XY($imageWidth, $imageHeight), $tileSize);
|
||||||
|
$this->layers = [$tileLayer];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add tile layer to the map
|
||||||
|
* @param TileLayer $layer An instance of TileLayer
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addLayer(TileLayer $layer)
|
||||||
|
{
|
||||||
|
$this->layers[] = $layer;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add markers on the map
|
||||||
|
* @param Markers $markers An instance of Markers
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addMarkers(Markers $markers)
|
||||||
|
{
|
||||||
|
$this->markers[] = $markers;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a line on the map
|
||||||
|
* @param Draw $draw An instance of Line
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addDraw(Draw $draw)
|
||||||
|
{
|
||||||
|
$this->draws[] = $draw;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fit map to draws.
|
||||||
|
*
|
||||||
|
* @param int $padding Padding in pixel
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function fitToDraws(int $padding = 0)
|
||||||
|
{
|
||||||
|
$points = [];
|
||||||
|
foreach ($this->draws as $draw) {
|
||||||
|
$points = \array_merge($points, $draw->getBoundingBox());
|
||||||
|
}
|
||||||
|
return $this->fitToPoints($points, $padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fit map to markers.
|
||||||
|
*
|
||||||
|
* @param int $padding Padding in pixel
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function fitToMarkers(int $padding = 0)
|
||||||
|
{
|
||||||
|
$points = [];
|
||||||
|
foreach ($this->markers as $markers) {
|
||||||
|
$points = \array_merge($points, $markers->getBoundingBox());
|
||||||
|
}
|
||||||
|
return $this->fitToPoints($points, $padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fit map to draws and markers.
|
||||||
|
*
|
||||||
|
* @param int $padding Padding in pixel
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function fitToMarkersAndDraws(int $padding = 0)
|
||||||
|
{
|
||||||
|
$points = [];
|
||||||
|
foreach ($this->draws as $draw) {
|
||||||
|
$points = \array_merge($points, $draw->getBoundingBox());
|
||||||
|
}
|
||||||
|
foreach ($this->markers as $markers) {
|
||||||
|
$points = \array_merge($points, $markers->getBoundingBox());
|
||||||
|
}
|
||||||
|
return $this->fitToPoints($points, $padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fit map to an array of points.
|
||||||
|
*
|
||||||
|
* @param LatLng[] $points LatLng points
|
||||||
|
* @param int $padding Padding in pixel
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function fitToPoints(array $points, int $padding = 0)
|
||||||
|
{
|
||||||
|
$outputSize = $this->mapData->getOutputSize();
|
||||||
|
$tileSize = $this->mapData->getTileSize();
|
||||||
|
$boundingBox = MapData::getBoundingBoxFromPoints($points);
|
||||||
|
$latLngZoom = MapData::getCenterAndZoomFromBoundingBox($boundingBox[0], $boundingBox[1], $padding, $outputSize->getX(), $outputSize->getY(), $tileSize);
|
||||||
|
$this->mapData = new MapData($latLngZoom['center'], $this->layers[0]->checkZoom($latLngZoom['zoom']), $outputSize, $tileSize);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get data about the generated map (bounding box, size, OSM tile ids...)
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api/blob/master/docs/classes/DantSu/OpenStreetMapStaticAPI/MapData.md See more about MapData
|
||||||
|
* @return MapData data about the generated map (bounding box, size, OSM tile ids...)
|
||||||
|
*/
|
||||||
|
public function getMapData(): MapData
|
||||||
|
{
|
||||||
|
return $this->mapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get only the map image.
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
* @return Image An instance of DantSu\PHPImageEditor\Image
|
||||||
|
*/
|
||||||
|
protected function getMapImage($centerMap): Image
|
||||||
|
{
|
||||||
|
$imgSize = $this->mapData->getOutputSize();
|
||||||
|
$startX = $this->mapData->getMapCropTopLeft()->getX() * -1;
|
||||||
|
$startY = $this->mapData->getMapCropTopLeft()->getY() * -1;
|
||||||
|
|
||||||
|
$image = Image::newCanvas($imgSize->getX(), $imgSize->getY());
|
||||||
|
$tileSize = $this->mapData->getTileSize();
|
||||||
|
|
||||||
|
foreach ($this->layers as $tileLayer) {
|
||||||
|
$yTile = $this->mapData->getTileTopLeft()->getY();
|
||||||
|
for ($y = $startY; $y < $imgSize->getY(); $y += $tileSize) {
|
||||||
|
$xTile = $this->mapData->getTileTopLeft()->getX();
|
||||||
|
for ($x = $startX; $x < $imgSize->getX(); $x += $tileSize) {
|
||||||
|
$xTileWrapped = $this->wrapLongitudeTile($xTile);
|
||||||
|
$image->pasteOn(
|
||||||
|
$tileLayer->getTile($xTileWrapped, $yTile, $this->mapData->getZoom(), $tileSize, $centerMap),
|
||||||
|
$x,
|
||||||
|
$y
|
||||||
|
);
|
||||||
|
++$xTile;
|
||||||
|
}
|
||||||
|
++$yTile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the longitude tile.
|
||||||
|
* @param int $xTile The x tile
|
||||||
|
* @return int The wrapped x tile
|
||||||
|
*/
|
||||||
|
protected function wrapLongitudeTile($xTile)
|
||||||
|
{
|
||||||
|
$maxTile = pow(2, $this->mapData->getZoom());
|
||||||
|
return ($xTile % $maxTile + $maxTile) % $maxTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw OpenStreetMap attribution at the right bottom of the image
|
||||||
|
* @param Image $image The image of the map
|
||||||
|
* @return Image The image of the map with attribution
|
||||||
|
*/
|
||||||
|
protected function drawAttribution(Image $image): Image
|
||||||
|
{
|
||||||
|
$margin = 5;
|
||||||
|
|
||||||
|
// Anonymous function to render the attribution text
|
||||||
|
$attribution = function (Image $image, $margin): array {
|
||||||
|
// Collect attribution text without HTML tags (strip tags from each layer)
|
||||||
|
$attributionText = \implode(
|
||||||
|
' - ',
|
||||||
|
\array_map(function ($layer) {
|
||||||
|
// Strip HTML tags (like <a>) from the attribution text
|
||||||
|
return strip_tags($layer->getAttributionText());
|
||||||
|
}, $this->layers)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw the plain text onto the image and get its bounding box
|
||||||
|
return $image->writeTextAndGetBoundingBox(
|
||||||
|
$attributionText,
|
||||||
|
__DIR__ . '/resources/font.ttf', // Font path
|
||||||
|
10, // Font size
|
||||||
|
'0078A8', // Text color
|
||||||
|
$margin, // X margin
|
||||||
|
$margin, // Y margin
|
||||||
|
Image::ALIGN_LEFT, // Horizontal alignment
|
||||||
|
Image::ALIGN_TOP // Vertical alignment
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate bounding box for the attribution text
|
||||||
|
$bbox = $attribution(Image::newCanvas(1, 1), $margin);
|
||||||
|
|
||||||
|
// Create a new canvas for the attribution background
|
||||||
|
$imageAttribution = Image::newCanvas($bbox['bottom-right']['x'] + $margin, $bbox['bottom-right']['y'] + $margin);
|
||||||
|
// Draw a semi-transparent rectangle for the attribution background
|
||||||
|
$imageAttribution->drawRectangle(0, 0, $imageAttribution->getWidth(), $imageAttribution->getHeight(), 'FFFFFF33');
|
||||||
|
// Write the attribution text on the new canvas
|
||||||
|
$attribution($imageAttribution, $margin);
|
||||||
|
|
||||||
|
// Paste the attribution canvas onto the main image (bottom-right corner)
|
||||||
|
return $image->pasteOn($imageAttribution, Image::ALIGN_RIGHT, Image::ALIGN_BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the map image with markers and lines.
|
||||||
|
*
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
* @return Image An instance of DantSu\PHPImageEditor\Image
|
||||||
|
*/
|
||||||
|
public function getImage($centerMap = '00'): Image
|
||||||
|
{
|
||||||
|
$image = $this->getMapImage($centerMap);
|
||||||
|
|
||||||
|
foreach ($this->draws as $line) {
|
||||||
|
$line->draw($image, $this->mapData);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->markers as $markers) {
|
||||||
|
$markers->draw($image, $this->mapData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->drawAttribution($image);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
src/StaticMap/src/Polygon.php
Normal file
171
src/StaticMap/src/Polygon.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\Interfaces\Draw;
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\Polygon draw polygon on the map.
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class Polygon implements Draw
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $strokeColor = '000000';
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $strokeWeight = 1;
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $fillColor = '000000';
|
||||||
|
/**
|
||||||
|
* @var LatLng[]
|
||||||
|
*/
|
||||||
|
private $points = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool wrap around the world or not
|
||||||
|
*/
|
||||||
|
private $wrap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polygon constructor.
|
||||||
|
* @param string $strokeColor Hexadecimal string color
|
||||||
|
* @param int $strokeWeight pixel weight of the line
|
||||||
|
* @param string $fillColor Hexadecimal string color
|
||||||
|
*/
|
||||||
|
public function __construct(string $strokeColor, int $strokeWeight, string $fillColor, bool $wrap = false)
|
||||||
|
{
|
||||||
|
$this->strokeColor = \str_replace('#', '', $strokeColor);
|
||||||
|
$this->strokeWeight = $strokeWeight > 0 ? $strokeWeight : 0;
|
||||||
|
$this->fillColor = \str_replace('#', '', $fillColor);
|
||||||
|
$this->wrap = $wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a latitude and longitude to the polygon
|
||||||
|
* @param LatLng $latLng Latitude and longitude to add
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function addPoint(LatLng $latLng): Polygon
|
||||||
|
{
|
||||||
|
$this->points[] = $latLng;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all Points of the polygon
|
||||||
|
* @return array of points
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function getPoints(): array
|
||||||
|
{
|
||||||
|
return $this->points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw the polygon on the map image.
|
||||||
|
*
|
||||||
|
* @see https://github.com/DantSu/php-image-editor See more about DantSu\PHPImageEditor\Image
|
||||||
|
*
|
||||||
|
* @param Image $image The map image (An instance of DantSu\PHPImageEditor\Image)
|
||||||
|
* @param MapData $mapData Bounding box of the map
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function draw(Image $image, MapData $mapData): Polygon
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var $cPoints XY[]
|
||||||
|
*/
|
||||||
|
$cPoints = \array_map(
|
||||||
|
function (LatLng $p) use ($mapData) {
|
||||||
|
return $mapData->convertLatLngToPxPosition($p);
|
||||||
|
},
|
||||||
|
$this->points
|
||||||
|
);
|
||||||
|
|
||||||
|
// Funktion zum Zeichnen des Polygons, um Wiederholungen zu reduzieren
|
||||||
|
$drawPolygon = function (array $points) use ($image) {
|
||||||
|
$image->pasteOn(
|
||||||
|
Image::newCanvas($image->getWidth(), $image->getHeight())
|
||||||
|
->drawPolygon(
|
||||||
|
\array_reduce(
|
||||||
|
$points,
|
||||||
|
function (array $acc, XY $p) {
|
||||||
|
$acc[] = $p->getX();
|
||||||
|
$acc[] = $p->getY();
|
||||||
|
return $acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
),
|
||||||
|
$this->fillColor
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zeichne das ursprüngliche Polygon
|
||||||
|
$drawPolygon($cPoints);
|
||||||
|
|
||||||
|
if ($this->wrap) {
|
||||||
|
// Zeichne das Polygon links und rechts des Bildes
|
||||||
|
$width = $image->getWidth();
|
||||||
|
$shiftedLeft = \array_map(function (XY $p) use ($width) {
|
||||||
|
return new XY($p->getX() - $width, $p->getY());
|
||||||
|
}, $cPoints);
|
||||||
|
|
||||||
|
$shiftedRight = \array_map(function (XY $p) use ($width) {
|
||||||
|
return new XY($p->getX() + $width, $p->getY());
|
||||||
|
}, $cPoints);
|
||||||
|
|
||||||
|
$drawPolygon($shiftedLeft);
|
||||||
|
$drawPolygon($shiftedRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->strokeWeight > 0) {
|
||||||
|
// Zeichne die Linien zwischen den Punkten für das Hauptpolygon
|
||||||
|
foreach ($cPoints as $k => $point) {
|
||||||
|
$pK = $k - 1;
|
||||||
|
if (!isset($cPoints[$pK])) {
|
||||||
|
$pK = \count($cPoints) - 1;
|
||||||
|
}
|
||||||
|
$image->drawLine($cPoints[$pK]->getX(), $cPoints[$pK]->getY(), $point->getX(), $point->getY(), $this->strokeWeight, $this->strokeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeichne die Linien für die linken und rechten Kopien des Polygons, falls Wrap aktiv ist
|
||||||
|
if ($this->wrap) {
|
||||||
|
foreach ([$shiftedLeft, $shiftedRight] as $shiftedPoints) {
|
||||||
|
foreach ($shiftedPoints as $k => $point) {
|
||||||
|
$pK = $k - 1;
|
||||||
|
if (!isset($shiftedPoints[$pK])) {
|
||||||
|
$pK = \count($shiftedPoints) - 1;
|
||||||
|
}
|
||||||
|
$image->drawLine($shiftedPoints[$pK]->getX(), $shiftedPoints[$pK]->getY(), $point->getX(), $point->getY(), $this->strokeWeight, $this->strokeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bounding box of the shape
|
||||||
|
* @return LatLng[]
|
||||||
|
*/
|
||||||
|
public function getBoundingBox(): array
|
||||||
|
{
|
||||||
|
return MapData::getBoundingBoxFromPoints($this->points);
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/StaticMap/src/TileLayer.php
Normal file
224
src/StaticMap/src/TileLayer.php
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
use DantSu\PHPImageEditor\Image;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\TileLayer define tile server url and related configuration
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Stephan Strate <hello@stephan.codes>
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class TileLayer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default tile server. OpenStreetMaps with related attribution text
|
||||||
|
* @return TileLayer default tile server
|
||||||
|
*/
|
||||||
|
public static function defaultTileLayer(): TileLayer {
|
||||||
|
$CI = &get_instance();
|
||||||
|
$CI->load->model('themes_model');
|
||||||
|
$r = $CI->themes_model->get_theme_mode($CI->optionslib->get_option('option_theme'));
|
||||||
|
if ($r == 'dark') {
|
||||||
|
$server = 'https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png';
|
||||||
|
} else {
|
||||||
|
$server = $CI->optionslib->get_option('option_map_tile_server');
|
||||||
|
}
|
||||||
|
$attribution = $CI->optionslib->get_option('option_map_tile_server_copyright');
|
||||||
|
return new TileLayer($server, $attribution, $r);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Tile server url, defaults to OpenStreetMap tile server
|
||||||
|
*/
|
||||||
|
protected $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Theme mode, defined light or dark mode
|
||||||
|
*/
|
||||||
|
protected $thememode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string Tile server attribution according to license
|
||||||
|
*/
|
||||||
|
protected $attributionText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[] Tile server subdomains
|
||||||
|
*/
|
||||||
|
protected $subdomains;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float Opacity
|
||||||
|
*/
|
||||||
|
protected $opacity = 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @var int Max zoom value
|
||||||
|
*/
|
||||||
|
protected $maxZoom = 20;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @var int Min zoom value
|
||||||
|
*/
|
||||||
|
protected $minZoom = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @array $curlOptions Array of curl options
|
||||||
|
*/
|
||||||
|
protected $curlOptions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @bool $failCurlOnError If true, curl will throw an exception on error.
|
||||||
|
*/
|
||||||
|
protected $failCurlOnError = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TileLayer constructor
|
||||||
|
* @param string $url tile server url with placeholders (`x`, `y`, `z`, `r`, `s`)
|
||||||
|
* @param string $attributionText tile server attribution text
|
||||||
|
* @param string $subdomains tile server subdomains
|
||||||
|
* @param array $curlOptions Array of curl options
|
||||||
|
* @param bool $failCurlOnError If true, curl will throw an exception on error.
|
||||||
|
*/
|
||||||
|
public function __construct(string $url, string $attributionText, string $thememode, string $subdomains = 'abc', array $curlOptions = [], bool $failCurlOnError = false) {
|
||||||
|
$this->url = $url;
|
||||||
|
$this->thememode = $thememode;
|
||||||
|
$this->attributionText = $attributionText;
|
||||||
|
$this->subdomains = \str_split($subdomains);
|
||||||
|
$this->curlOptions = $curlOptions;
|
||||||
|
$this->failCurlOnError = $failCurlOnError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set opacity of the layer
|
||||||
|
* @param float $opacity Opacity value (0 to 1)
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setOpacity(float $opacity) {
|
||||||
|
$this->opacity = $opacity;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a max zoom value
|
||||||
|
* @param int $maxZoom
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setMaxZoom(int $maxZoom) {
|
||||||
|
$this->maxZoom = $maxZoom;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get max zoom value
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getMaxZoom(): int {
|
||||||
|
return $this->maxZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a min zoom value
|
||||||
|
* @param int $minZoom
|
||||||
|
* @return $this Fluent interface
|
||||||
|
*/
|
||||||
|
public function setMinZoom(int $minZoom) {
|
||||||
|
$this->minZoom = $minZoom;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get min zoom value
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getMinZoom(): int {
|
||||||
|
return $this->minZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if zoom value is between min zoom and max zoom
|
||||||
|
* @param int $zoom Zoom value to be checked
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function checkZoom(int $zoom): int {
|
||||||
|
return \min(\max($zoom, $this->minZoom), $this->maxZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tile url for coordinates and zoom level
|
||||||
|
* @param int $x x coordinate
|
||||||
|
* @param int $y y coordinate
|
||||||
|
* @param int $z zoom level
|
||||||
|
* @return string tile url
|
||||||
|
*/
|
||||||
|
public function getTileUrl(int $x, int $y, int $z): string {
|
||||||
|
return \str_replace(
|
||||||
|
['{r}', '{s}', '{x}', '{y}', '{z}'],
|
||||||
|
['', $this->getSubdomain($x, $y), $x, $y, $z],
|
||||||
|
$this->url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select subdomain of tile server to prevent rate limiting on remote server
|
||||||
|
* @param int $x x coordinate
|
||||||
|
* @param int $y y coordinate
|
||||||
|
* @return string selected subdomain
|
||||||
|
* @see https://github.com/Leaflet/Leaflet/blob/main/src/layer/tile/TileLayer.js#L233 Leaflet implementation
|
||||||
|
*/
|
||||||
|
protected function getSubdomain(int $x, int $y): string {
|
||||||
|
return $this->subdomains[\abs($x + $y) % \sizeof($this->subdomains)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get attribution text
|
||||||
|
* @return string Attribution text
|
||||||
|
*/
|
||||||
|
public function getAttributionText(): string {
|
||||||
|
return $this->attributionText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an image tile
|
||||||
|
* @param float $x
|
||||||
|
* @param float $y
|
||||||
|
* @param int $z
|
||||||
|
* @param int $tileSize
|
||||||
|
* @param string $centerMap
|
||||||
|
* @return Image Image instance containing the tile
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function getTile(float $x, float $y, int $z, int $tileSize, string $centerMap): Image {
|
||||||
|
$CI = &get_instance();
|
||||||
|
$namehash = substr(md5($x . $y . $z . $centerMap . $this->thememode), 0, 16);
|
||||||
|
$cacheKey = $namehash . ".png";
|
||||||
|
$cacheConfig = $CI->config->item('cache_path') == '' ? APPPATH . 'cache/' : $CI->config->item('cache_path');
|
||||||
|
$cacheDir = $cacheConfig . "tilecache/" . $z . "/" . $y . "/" . $x . "/";
|
||||||
|
$cachePath = $cacheDir . $cacheKey;
|
||||||
|
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($cachePath)) {
|
||||||
|
$tile = Image::fromPath($cachePath);
|
||||||
|
} else {
|
||||||
|
$tile = Image::fromCurl($this->getTileUrl($x, $y, $z), $this->curlOptions, $this->failCurlOnError);
|
||||||
|
$tile->savePNG($cachePath);
|
||||||
|
}
|
||||||
|
if ($this->opacity == 0) {
|
||||||
|
return Image::newCanvas($tileSize, $tileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->opacity > 0 && $this->opacity < 1) {
|
||||||
|
$tile->setOpacity($this->opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/StaticMap/src/Utils/GeographicConverter.php
Normal file
119
src/StaticMap/src/Utils/GeographicConverter.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage\Utils;
|
||||||
|
|
||||||
|
|
||||||
|
use Wavelog\StaticMapImage\LatLng;
|
||||||
|
|
||||||
|
class GeographicConverter
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the earth radius at the given latitude
|
||||||
|
*
|
||||||
|
* @param float $lat
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public static function earthRadiusAtLatitude(float $lat): float
|
||||||
|
{
|
||||||
|
$equatorial = 6378137.0;
|
||||||
|
$pole = 6356752.3;
|
||||||
|
$lat = \deg2rad($lat);
|
||||||
|
|
||||||
|
return \sqrt(
|
||||||
|
(\pow(\pow($equatorial, 2) * \cos($lat), 2) + \pow(\pow($pole, 2) * \sin($lat), 2)) /
|
||||||
|
(\pow($equatorial * \cos($lat), 2) + \pow($pole * \sin($lat), 2))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert distance and angle from a point to latitude and longitude
|
||||||
|
* 0 : top, 90 : right; 180 : bottom, 270 : left
|
||||||
|
*
|
||||||
|
* @param LatLng $from Starting coordinate
|
||||||
|
* @param float $distance Distance in meters
|
||||||
|
* @param float $angle Angle in degrees
|
||||||
|
* @return LatLng
|
||||||
|
*/
|
||||||
|
public static function metersToLatLng(LatLng $from, float $distance, float $angle): LatLng
|
||||||
|
{
|
||||||
|
$earthRadius = self::earthRadiusAtLatitude($from->getLat());
|
||||||
|
$lat = \deg2rad($from->getLat());
|
||||||
|
$lng = \deg2rad($from->getLng());
|
||||||
|
$angle = \deg2rad($angle);
|
||||||
|
|
||||||
|
return new LatLng(
|
||||||
|
\rad2deg(
|
||||||
|
\asin(
|
||||||
|
\sin($lat) * \cos($distance / $earthRadius) +
|
||||||
|
\cos($lat) * \sin($distance / $earthRadius) * \cos($angle)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
\rad2deg(
|
||||||
|
$lng + \atan2(
|
||||||
|
\sin($angle) * \sin($distance / $earthRadius) * \cos($lat),
|
||||||
|
\cos($distance / $earthRadius) - \sin($lat) * \sin($lat)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get distance in meters between two points.
|
||||||
|
*
|
||||||
|
* @param LatLng $from Starting coordinate
|
||||||
|
* @param LatLng $end Ending coordinate
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public static function latLngToMeters(LatLng $from, LatLng $end): float
|
||||||
|
{
|
||||||
|
$earthRadius = self::earthRadiusAtLatitude($from->getLat());
|
||||||
|
$lat1 = \deg2rad($from->getLat());
|
||||||
|
$lat2 = \deg2rad($end->getLat());
|
||||||
|
$lat = \deg2rad($end->getLat() - $from->getLat());
|
||||||
|
$lng = \deg2rad($end->getLng() - $from->getLng());
|
||||||
|
|
||||||
|
$a = \pow(\sin($lat / 2), 2) + \cos($lat1) * \cos($lat2) * \pow(\sin($lng / 2), 2);
|
||||||
|
return \abs($earthRadius * 2 * \atan2(\sqrt($a), \sqrt(1 - $a)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get center between two coordinates.
|
||||||
|
* @param LatLng $point1 Vertical OpenStreetMap tile id
|
||||||
|
* @param LatLng $point2 Vertical pixel position on tile
|
||||||
|
* @return LatLng midpoint between the given coordinates
|
||||||
|
*/
|
||||||
|
public static function getCenter(LatLng $point1, LatLng $point2): LatLng
|
||||||
|
{
|
||||||
|
//return new LatLng(($point1->getLat() + $point2->getLat()) / 2, ($point1->getLng() + $point2->getLng()) / 2);
|
||||||
|
$dLng = \deg2rad($point2->getLng() - $point1->getLng());
|
||||||
|
$lat1 = \deg2rad($point1->getLat());
|
||||||
|
$lat2 = \deg2rad($point2->getLat());
|
||||||
|
$lng1 = \deg2rad($point1->getLng());
|
||||||
|
$bx = \cos($lat2) * \cos($dLng);
|
||||||
|
$by = \cos($lat2) * \sin($dLng);
|
||||||
|
return new LatLng(
|
||||||
|
\rad2deg(\atan2(\sin($lat1) + \sin($lat2), \sqrt(\pow(\cos($lat1) + $bx, 2) + \pow($by, 2)))),
|
||||||
|
\rad2deg($lng1 + \atan2($by, \cos($lat1) + $bx))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get bearing between two coordinates.
|
||||||
|
* @param LatLng $from Starting coordinate
|
||||||
|
* @param LatLng $to Ending coordinate
|
||||||
|
*
|
||||||
|
* @return float bearing in degrees
|
||||||
|
*/
|
||||||
|
public static function getBearing(LatLng $from, LatLng $to): float
|
||||||
|
{
|
||||||
|
$lat1 = deg2rad($from->getLat());
|
||||||
|
$lat2 = deg2rad($to->getLat());
|
||||||
|
$dLon = deg2rad($to->getLng() - $from->getLng());
|
||||||
|
|
||||||
|
$y = sin($dLon) * cos($lat2);
|
||||||
|
$x = cos($lat1) * sin($lat2) - sin($lat1) * cos($lat2) * cos($dLon);
|
||||||
|
return fmod(rad2deg(atan2($y, $x)) + 360, 360);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/StaticMap/src/Utils/Terminator.php
Normal file
79
src/StaticMap/src/Utils/Terminator.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Terminator {
|
||||||
|
private $resolution;
|
||||||
|
|
||||||
|
public function __construct($resolution = 2) {
|
||||||
|
$this->resolution = $resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTerminatorCoordinates($time = null) {
|
||||||
|
$date = $time ? new DateTime($time) : new DateTime();
|
||||||
|
$julianDay = $this->julian($date);
|
||||||
|
$gst = $this->GMST($julianDay);
|
||||||
|
|
||||||
|
$sunEclPos = $this->sunEclipticPosition($julianDay);
|
||||||
|
$eclObliq = $this->eclipticObliquity($julianDay);
|
||||||
|
$sunEqPos = $this->sunEquatorialPosition($sunEclPos['lambda'], $eclObliq);
|
||||||
|
|
||||||
|
$latLng = [];
|
||||||
|
for ($i = 0; $i <= 720 * $this->resolution; $i++) {
|
||||||
|
$lng = -360 + $i / $this->resolution;
|
||||||
|
$ha = $this->hourAngle($lng, $sunEqPos, $gst);
|
||||||
|
$latLng[] = [$this->latitude($ha, $sunEqPos), $lng];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sunEqPos['delta'] < 0) {
|
||||||
|
array_unshift($latLng, [90, -360]);
|
||||||
|
array_push($latLng, [90, 360]);
|
||||||
|
} else {
|
||||||
|
array_unshift($latLng, [-90, -360]);
|
||||||
|
array_push($latLng, [-90, 360]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latLng;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function julian($date) {
|
||||||
|
// Julian date calculation
|
||||||
|
return ($date->getTimestamp() / 86400) + 2440587.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function GMST($julianDay) {
|
||||||
|
$d = $julianDay - 2451545.0;
|
||||||
|
return fmod(18.697374558 + 24.06570982441908 * $d, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sunEclipticPosition($julianDay) {
|
||||||
|
$n = $julianDay - 2451545.0;
|
||||||
|
$L = fmod(280.460 + 0.9856474 * $n, 360);
|
||||||
|
$g = fmod(357.528 + 0.9856003 * $n, 360);
|
||||||
|
$lambda = $L + 1.915 * sin(deg2rad($g)) + 0.02 * sin(deg2rad(2 * $g));
|
||||||
|
$R = 1.00014 - 0.01671 * cos(deg2rad($g)) - 0.0014 * cos(deg2rad(2 * $g));
|
||||||
|
return ['lambda' => $lambda, 'R' => $R];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function eclipticObliquity($julianDay) {
|
||||||
|
$n = $julianDay - 2451545.0;
|
||||||
|
$T = $n / 36525;
|
||||||
|
return 23.43929111 - $T * (46.836769 / 3600 - $T * (0.0001831 / 3600 + $T * (0.00200340 / 3600 - $T * (0.576e-6 / 3600 - $T * 4.34e-8 / 3600))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sunEquatorialPosition($sunEclLng, $eclObliq) {
|
||||||
|
$alpha = rad2deg(atan(cos(deg2rad($eclObliq)) * tan(deg2rad($sunEclLng))));
|
||||||
|
$delta = rad2deg(asin(sin(deg2rad($eclObliq)) * sin(deg2rad($sunEclLng))));
|
||||||
|
$lQuadrant = floor($sunEclLng / 90) * 90;
|
||||||
|
$raQuadrant = floor($alpha / 90) * 90;
|
||||||
|
$alpha = $alpha + ($lQuadrant - $raQuadrant);
|
||||||
|
return ['alpha' => $alpha, 'delta' => $delta];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hourAngle($lng, $sunPos, $gst) {
|
||||||
|
$lst = $gst + $lng / 15;
|
||||||
|
return $lst * 15 - $sunPos['alpha'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function latitude($ha, $sunPos) {
|
||||||
|
return rad2deg(atan(-cos(deg2rad($ha)) / tan(deg2rad($sunPos['delta']))));
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/StaticMap/src/XY.php
Normal file
52
src/StaticMap/src/XY.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wavelog\StaticMapImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wavelog\StaticMapImage\XY define X and Y pixel position for map, lines, markers...
|
||||||
|
*
|
||||||
|
* @package Wavelog\StaticMapImage
|
||||||
|
* @author Franck Alary
|
||||||
|
* @access public
|
||||||
|
* @see https://github.com/DantSu/php-osm-static-api Github page of this project
|
||||||
|
*/
|
||||||
|
class XY
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var int X
|
||||||
|
*/
|
||||||
|
private $x = 0;
|
||||||
|
/**
|
||||||
|
* @var int Y
|
||||||
|
*/
|
||||||
|
private $y = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XY constructor.
|
||||||
|
* @param int $x X
|
||||||
|
* @param int $y Y
|
||||||
|
*/
|
||||||
|
public function __construct(int $x, int $y)
|
||||||
|
{
|
||||||
|
$this->x = $x;
|
||||||
|
$this->y = $y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get X
|
||||||
|
* @return int X
|
||||||
|
*/
|
||||||
|
public function getX(): int
|
||||||
|
{
|
||||||
|
return $this->x;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Y
|
||||||
|
* @return int Y
|
||||||
|
*/
|
||||||
|
public function getY(): int
|
||||||
|
{
|
||||||
|
return $this->y;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/StaticMap/src/resources/circle-dot-red.png
Normal file
BIN
src/StaticMap/src/resources/circle-dot-red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/StaticMap/src/resources/font.ttf
Normal file
BIN
src/StaticMap/src/resources/font.ttf
Normal file
Binary file not shown.
BIN
src/StaticMap/src/resources/watermark_static_map.png
Normal file
BIN
src/StaticMap/src/resources/watermark_static_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user