Files
wavelog/application/models/Staticmap_model.php
Fabian Berg c70c2ec5cd Clubstations for Wavelog (#1334)
* feat[clubstations]: New DB structure

* feat[clubstations]: Add clubstationstable in user managment

* feat[clubstations]: Show last operator

* feat[clubstations]: Better solution for last operator. tnx for the hint @int2001

* feat[clubstations]: New Club Model and Controller

* feat[clubstations]: Add "Add User" and "Edit User" functionality

* docs[clubstations]: move comment

* feat[clubstations]: Add "Delete Member" functionality

* feat[clubstations]: some enhancements and javascript

* fix[clubstations]: Wrong message class for flashmessages

* feat[clubstations]: Added Switch in the Header menu (not functional yet)

* feat[clubstations]: clubswitch modal

* fix[clubstations]: Load encryption library if not already loaded

* fix[clubstations]: Prevent direct login attempts to clubstations and enhance impersonation authorization

* fix[clubstations]: Typo

* feat[clubstations]: Only show the operator dialog if there is something fishy

* fix[user]: little UI bug

* feat[impersonate]: Add source uid to session data

* fix[impersonate]: logic adjustment

* feat[clubstations]: Add manage button in header menu for club officers

* fix[clubstations]: typo in permission level check

* fix[clubstations]: Full rights for the admin

* feat[impersonate]: Custom sessiondata

* feat[impersonate]: Implement stop impersonation feature with modal confirmation; "the way back"

* fix(modal): Fix bug where modal was hidden when mouse leaved the browser content

* docs(config): Adjust config description for special callsigns and clubstations

* feat(club): Add club access check helper

* typo

* fix[impersonation]: Better text

* feat(club): Selectize for a efficient user search

* feat(clubstations): Restrict clubstations based on users permission level part 1/x

* adjustments for dev merge

* Adjusted club right for the advanced logbook

* feat[user]: Refactoring of the Action Buttons in the user table

* fix[club_permissions]: normal button instead small one for club permissions

* remove unnecessary line break in modal body

* feat[clubstations]: Add Club Mode badge to the header

* fix[clubstations]: fix maintenance mode

* allow switch back on http

* feat(simplefle): display operator input based on club_access

* small UI adjustments

* small UI adjustments

* moved api page to a index.php file and added support for clubstations

* removed unused stuff

* typo

* radios and api keys

* missed one binding

* fix qso view, even officers do just see their own radios in QSO logging

* omit the need for a relogin to see the changes as an admin

* Omit the need for relogin after club changes in general. It's a question of UX. It's better to accept a little higher DB load (if clubstations are enabled) then the need of an user to relogin. There is some room for improvement by changing user_model->get_by_id() and adding a join there. This can be done later if we see that the load is too high

* If the user is not the creator of the API key, it's likely a clubstation. In this case the callsign of the clubstation can not be the same as the callsign of the user (operator call provided by the user). If this is the case, we need to use the callsign of the creator of the API key

* remove debug messages

* better UI in header

* found a typo

* full access in clubstations for admins (if accessed via admin usertable)

* adjusted text

* adjusted text

* adjust text

* reduce required chars

* bugfix: missing the correct authentication in case the admin was not member of the club. he wasn't able to switch back

* reduce debug messages

* fixed UI bug related to tooltips

* load js in controller

* upps..

* some UI adjustments

* corrected permissions

* if user gets delete we need to remove data in club_permissions and also api keys which were created by this user

* Notify members about new memberships or changes in permission level

* add spinner to save button

* make login/logout process more bulletproof

* remove the relogin cookie after the attempt

* better strategy

* bug where switch back failed if user is no admin

* make api keys more secure

* mask not owned api keys

* removed annoying link

* if a user gets removed from a club we also should delete the corresponding api keys and cat radios

* adjusted wiki link

* Auto creation of logbook and location when new user is created

* store and display locator in uppercase

* same for callsign

* fixed a bug in user/club creation

* Revert "Auto creation of logbook and location when new user is created"
We found another solution to which will be addressed in a second PR
This reverts commit f05f4b7bf0.

* Optimized SQL for stats at userlist

* Source query for lastop "out", because mysql<9.0 can't handle Windowed functions

* adjust migration

* add new columns to users table to get created_at and modified_at

* added a partial down function

* add operator dropdown for clubstations

* fix mig version

* Add some backend restrictions in case a user wants to try something funny with the club

---------

Co-authored-by: Andreas Kristiansen <6977712+AndreasK79@users.noreply.github.com>
Co-authored-by: int2001 <joerg@dj7nt.de>
2025-01-02 10:22:23 +01:00

780 lines
36 KiB
PHP

<?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, $watermark = true) {
//===============================================================================================================================
//=============================================== 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 &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a>';
$subdomains = $this->optionslib->get_option('option_map_tile_subdomains') ?? 'abc';
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, $subdomains);
} 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, $subdomains);
} 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";
}
// free memory
unset($options_object);
// 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]));
}
// free memory
unset($terminator);
}
//===============================================================================================================================
//============================================ 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);
// free memory
unset($cqzones_polygon_array);
} 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);
// free memory
unset($ituzones_polygon_array);
} 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
if ($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 = new \Wavelog\StaticMapImage\LatLng($start[0], $start[1]);
$end = new \Wavelog\StaticMapImage\LatLng($end[0], $end[1]);
$path = new \Wavelog\StaticMapImage\Line($color, $weight, !$continent);
foreach ($path->geodesicPoints($start, $end, $continent) 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('info', "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('info', "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;
}
}