From b31612d28cf2a5617667dd9dae62379c90dbaf1c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Rapha=C3=ABl=20Gertz?= Date: Wed, 15 Sep 2021 16:59:05 +0200 Subject: [PATCH 1/1] Rename OsmUtil to MapUtil --- Util/MapUtil.php | 428 +++++++++++++++++++++++++++++ Util/OsmUtil.php | 687 ----------------------------------------------- 2 files changed, 428 insertions(+), 687 deletions(-) create mode 100644 Util/MapUtil.php delete mode 100644 Util/OsmUtil.php diff --git a/Util/MapUtil.php b/Util/MapUtil.php new file mode 100644 index 0000000..5de2967 --- /dev/null +++ b/Util/MapUtil.php @@ -0,0 +1,428 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Rapsys\PackBundle\Util; + +use Symfony\Component\Routing\RouterInterface; + +/** + * Helps manage map + */ +class MapUtil { + /** + * The cycle tile server + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers + */ + const cycle = 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png'; + + /** + * The fill color + */ + const fill = '#cff'; + + /** + * The font size + */ + const fontSize = 20; + + /** + * The high fill color + */ + const highFill = '#c3c3f9'; + + /** + * The high font size + */ + const highFontSize = 30; + + /** + * The high radius size + */ + const highRadius = 6; + + /** + * The high stroke color + */ + const highStroke = '#3333c3'; + + /** + * The high stroke size + */ + const highStrokeWidth = 4; + + /** + * The map length + */ + const length = 640; + + /** + * The osm tile server + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers + */ + const osm = 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png'; + + /** + * The radius size + */ + const radius = 5; + + /** + * The stroke color + */ + const stroke = '#00c3f9'; + + /** + * The stroke size + */ + const strokeWidth = 2; + + /** + * The transport tile server + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers + */ + const transport = 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'; + + /** + * The tile size + */ + const tz = 256; + + /** + * The map zoom + */ + const zoom = 17; + + /** + * The RouterInterface instance + */ + protected RouterInterface $router; + + /** + * The SluggerUtil instance + */ + protected SluggerUtil $slugger; + + /** + * The fill color + */ + public string $fill; + + /** + * The font size + */ + public int $fontSize; + + /** + * The high fill color + */ + public string $highFill; + + /** + * The font size + */ + public int $highFontSize; + + /** + * The radius size + */ + public int $highRadius; + + /** + * The high stroke color + */ + public string $highStroke; + + /** + * The stroke size + */ + public int $highStrokeWidth; + + /** + * The stroke color + */ + public string $stroke; + + /** + * The stroke size + */ + public int $strokeWidth; + + /** + * The radius size + */ + public int $radius; + + /** + * Creates a new map util + * + * @param RouterInterface $router The RouterInterface instance + * @param SluggerUtil $slugger The SluggerUtil instance + */ + function __construct(RouterInterface $router, SluggerUtil $slugger, string $fill = self::fill, int $fontSize = self::fontSize, string $highFill = self::highFill, int $highFontSize = self::highFontSize, int $highRadius = self::highRadius, string $highStroke = self::highStroke, int $highStrokeWidth = self::highStrokeWidth, int $radius = self::radius, string $stroke = self::stroke, int $strokeWidth = self::strokeWidth) { + //Set router + $this->router = $router; + + //Set slugger + $this->slugger = $slugger; + + //Set fill + $this->fill = $fill; + + //Set font size + $this->fontSize = $fontSize; + + //Set highFill + $this->highFill = $highFill; + + //Set high font size + $this->highFontSize = $highFontSize; + + //Set high radius size + $this->highRadius = $highRadius; + + //Set highStroke + $this->highStroke = $highStroke; + + //Set high stroke size + $this->highStrokeWidth = $highStrokeWidth; + + //Set radius size + $this->radius = $radius; + + //Set stroke + $this->stroke = $stroke; + + //Set stroke size + $this->strokeWidth = $strokeWidth; + } + + /** + * Return map url + * + * @param string $caption The caption + * @param int $updated The updated timestamp + * @param float $latitude The latitude + * @param float $longitude The longitude + * @param int $zoom The zoom + * @param int $width The width + * @param int $height The height + * @return int The zoom + */ + public function mapUrl(string $caption, int $updated, float $latitude, float $longitude, int $zoom = self::zoom, int $width = self::length, int $height = self::length): array { + //Set link hash + $link = $this->slugger->serialize([$updated, $latitude, $longitude, $zoom + 1, $width * 2, $height * 2]); + + //Set src hash + $src = $this->slugger->serialize([$updated, $latitude, $longitude, $zoom, $width, $height]); + + //Return array + return [ + 'caption' => $caption, + 'link' => $this->router->generate('rapsys_pack_map', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]), + 'src' => $this->router->generate('rapsys_pack_map', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom, 'width' => $width, 'height' => $height]), + 'width' => $width, + 'height' => $height + ]; + } + + /** + * Return multi map url + * + * @param string $caption The caption + * @param int $updated The updated timestamp + * @param float $latitude The latitude + * @param float $longitude The longitude + * @param array $coordinates The coordinates array + * @param int $zoom The zoom + * @param int $width The width + * @param int $height The height + * @return int The zoom + */ + public function multiMapUrl(string $caption, int $updated, float $latitude, float $longitude, $coordinates = [], int $zoom = self::zoom, int $width = self::length, int $height = self::length): array { + //Set coordinate + $coordinate = implode('-', array_map(function ($v) { return $v['latitude'].','.$v['longitude']; }, $coordinates)); + + //Set coordinate hash + $hash = $this->slugger->hash($coordinate); + + //Set link hash + $link = $this->slugger->serialize([$updated, $latitude, $longitude, $hash, $zoom + 1, $width * 2, $height * 2]); + + //Set src hash + $src = $this->slugger->serialize([$updated, $latitude, $longitude, $hash, $zoom, $width, $height]); + + //Return array + return [ + 'caption' => $caption, + 'link' => $this->router->generate('rapsys_pack_multimap', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]), + 'src' => $this->router->generate('rapsys_pack_multimap', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom, 'width' => $width, 'height' => $height]), + 'width' => $width, + 'height' => $height + ]; + } + + /** + * Return multi map zoom + * + * Compute a zoom to have all coordinates on multi map + * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2) + * + * @see Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / self::tz) + * + * @param float $latitude The latitude + * @param float $longitude The longitude + * @param array $coordinates The coordinates array + * @param int $zoom The zoom + * @param int $width The width + * @param int $height The height + * @return int The zoom + */ + public function multiMapZoom(float $latitude, float $longitude, array $coordinates = [], int $zoom = self::zoom, int $width = self::length, int $height = self::length): int { + //Iterate on each zoom + for ($i = $zoom; $i >= 1; $i--) { + //Get tile xy + $centerX = self::longitudeToX($longitude, $i); + $centerY = self::latitudeToY($latitude, $i); + + //Calculate start xy + $startX = floor($centerX - $width / 2 / self::tz); + $startY = floor($centerY - $height / 2 / self::tz); + + //Calculate end xy + $endX = ceil($centerX + $width / 2 / self::tz); + $endY = ceil($centerY + $height / 2 / self::tz); + + //Iterate on each coordinates + foreach($coordinates as $k => $coordinate) { + //Set dest x + $destX = self::longitudeToX($coordinate['longitude'], $i); + + //With outside point + if ($startX >= $destX || $endX <= $destX) { + //Skip zoom + continue(2); + } + + //Set dest y + $destY = self::latitudeToY($coordinate['latitude'], $i); + + //With outside point + if ($startY >= $destY || $endY <= $destY) { + //Skip zoom + continue(2); + } + } + + //Found zoom + break; + } + + //Return zoom + return $i; + } + + /** + * Convert longitude to tile x number + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 + * + * @param float $longitude The longitude + * @param int $zoom The zoom + * + * @return float The tile x + */ + public static function longitudeToX(float $longitude, int $zoom): float { + return (($longitude + 180) / 360) * pow(2, $zoom); + } + + /** + * Convert latitude to tile y number + * + * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 + * + * @param $latitude The latitude + * @param $zoom The zoom + * + * @return float The tile y + */ + public static function latitudeToY(float $latitude, int $zoom): float { + return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom); + } + + /** + * Convert tile x to longitude + * + * @param float $x The tile x + * @param int $zoom The zoom + * + * @return float The longitude + */ + public static function xToLongitude(float $x, int $zoom): float { + return $x / pow(2, $zoom) * 360.0 - 180.0; + } + + /** + * Convert tile y to latitude + * + * @param float $y The tile y + * @param int $zoom The zoom + * + * @return float The latitude + */ + public static function yToLatitude(float $y, int $zoom): float { + return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom))))); + } + + /** + * Convert decimal latitude to sexagesimal + * + * @param float $latitude The decimal latitude + * + * @return string The sexagesimal longitude + */ + public static function latitudeToSexagesimal(float $latitude): string { + //Set degree + $degree = $latitude % 60; + + //Set minute + $minute = ($latitude - $degree) * 60 % 60; + + //Set second + $second = ($latitude - $degree - $minute / 60) * 3600 % 3600; + + //Return sexagesimal longitude + return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S'); + } + + /** + * Convert decimal longitude to sexagesimal + * + * @param float $longitude The decimal longitude + * + * @return string The sexagesimal longitude + */ + public static function longitudeToSexagesimal(float $longitude): string { + //Set degree + $degree = $longitude % 60; + + //Set minute + $minute = ($longitude - $degree) * 60 % 60; + + //Set second + $second = ($longitude - $degree - $minute / 60) * 3600 % 3600; + + //Return sexagesimal longitude + return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W'); + } +} diff --git a/Util/OsmUtil.php b/Util/OsmUtil.php deleted file mode 100644 index 0ac3a76..0000000 --- a/Util/OsmUtil.php +++ /dev/null @@ -1,687 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Rapsys\PackBundle\Util; - -use Symfony\Component\Filesystem\Exception\IOExceptionInterface; -use Symfony\Component\Filesystem\Filesystem; - -/** - * Helps manage osm images - */ -class OsmUtil { - /** - * The tile size - */ - const tz = 256; - - /** - * The cache directory - */ - protected $cache; - - /** - * The public path - */ - protected $path; - - /** - * The tile server - */ - protected $server; - - /** - * The tile servers - * - * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers - */ - protected $servers = [ - 'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png', - 'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png', - 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png' - ]; - - /** - * The public url - */ - protected $url; - - /** - * Creates a new osm util - * - * @param string $cache The cache directory - * @param string $path The public path - * @param string $url The public url - * @param string $server The server key - */ - function __construct(string $cache, string $path, string $url, string $server = 'osm') { - //Set cache - $this->cache = $cache.'/'.$server; - - //Set path - $this->path = $path.'/'.$server; - - //Set url - $this->url = $url.'/'.$server; - - //Set server key - $this->server = $server; - } - - /** - * Return the simple image - * - * Generate simple image in jpeg format or load it from cache - * - * @param string $pathInfo The path info - * @param string $caption The image caption - * @param int $updated The updated timestamp - * @param float $latitude The latitude - * @param float $longitude The longitude - * @param int $zoom The zoom - * @param int $width The width - * @param int $height The height - * @return array The image array - */ - #TODO: rename to getSimple ??? - public function getImage(string $pathInfo, string $caption, int $updated, float $latitude, float $longitude, int $zoom = 18, int $width = 1280, int $height = 1280): array { - //Set path file - $path = $this->path.$pathInfo.'.jpeg'; - - //Set min file - $min = $this->path.$pathInfo.'.min.jpeg'; - - //Without existing path - if (!is_dir($dir = dirname($path))) { - //Create filesystem object - $filesystem = new Filesystem(); - - try { - //Create path - //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) - $filesystem->mkdir($dir, 0775); - } catch (IOExceptionInterface $e) { - //Throw error - throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e); - } - } - - //With path and min up to date file - if (is_file($path) && is_file($min) && ($mtime = stat($path)['mtime']) && ($mintime = stat($min)['mtime']) && $mtime >= $updated && $mintime >= $updated) { - //Return image data - return [ - 'link' => $this->url.'/'.$mtime.$pathInfo.'.jpeg', - 'min' => $this->url.'/'.$mintime.$pathInfo.'.min.jpeg', - 'caption' => $caption, - 'height' => $height / 2, - 'width' => $width / 2 - ]; - } - - //Create image instance - $image = new \Imagick(); - - //Add new image - $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg'); - - //Create tile instance - $tile = new \Imagick(); - - //Init context - $ctx = stream_context_create( - [ - 'http' => [ - #'header' => ['Referer: https://www.openstreetmap.org/'], - 'max_redirects' => 5, - 'timeout' => (int)ini_get('default_socket_timeout'), - #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', - 'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0', - ] - ] - ); - - //Get tile xy - $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom)); - $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom)); - - //Calculate start xy - $startX = floor(($tileX * self::tz - $width) / self::tz); - $startY = floor(($tileY * self::tz - $height) / self::tz); - - //Calculate end xy - $endX = ceil(($tileX * self::tz + $width) / self::tz); - $endY = ceil(($tileY * self::tz + $height) / self::tz); - - for($x = $startX; $x <= $endX; $x++) { - for($y = $startY; $y <= $endY; $y++) { - //Set cache path - $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png'; - - //Without cache path - if (!is_dir($dir = dirname($cache))) { - //Create filesystem object - $filesystem = new Filesystem(); - - try { - //Create path - //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) - $filesystem->mkdir($dir, 0775); - } catch (IOExceptionInterface $e) { - //Throw error - throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); - } - } - - //Without cache image - if (!is_file($cache)) { - //Set tile url - $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers[$this->server]); - - //Store tile in cache - file_put_contents($cache, file_get_contents($tileUri, false, $ctx)); - } - - //Set dest x - $destX = intval(floor(($width / 2) - self::tz * ($centerX - $x))); - - //Set dest y - $destY = intval(floor(($height / 2) - self::tz * ($centerY - $y))); - - //Read tile from cache - $tile->readImage($cache); - - //Compose image - $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY); - - //Clear tile - $tile->clear(); - } - } - - //Add circle - //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916 - $draw = new \ImagickDraw(); - - //Set text antialias - $draw->setTextAntialias(true); - - //Set stroke antialias - $draw->setStrokeAntialias(true); - - //Set fill color - $draw->setFillColor('#c3c3f9'); - - //Set stroke color - $draw->setStrokeColor('#3333c3'); - - //Set stroke width - $draw->setStrokeWidth(2); - - //Draw circle - $draw->circle($width/2, $height/2 - 5, $width/2 + 10, $height/2 + 5); - - //Draw on image - $image->drawImage($draw); - - //Strip image exif data and properties - $image->stripImage(); - - //Add latitude - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude)); - - //Add longitude - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude)); - - //Add description - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:Description', $caption); - - //Set progressive jpeg - $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE); - - //Save image - if (!$image->writeImage($path)) { - //Throw error - throw new \Exception(sprintf('Unable to write image "%s"', $path)); - } - - //Crop using aspect ratio - $image->cropThumbnailImage($width / 2, $height / 2); - - //Set compression quality - $image->setImageCompressionQuality(70); - - //Save min image - if (!$image->writeImage($min)) { - //Throw error - throw new \Exception(sprintf('Unable to write image "%s"', $min)); - } - - //Return image data - return [ - 'link' => $this->url.'/'.stat($path)['mtime'].$pathInfo.'.jpeg', - 'min' => $this->url.'/'.stat($min)['mtime'].$pathInfo.'.min.jpeg', - 'caption' => $caption, - 'height' => $height / 2, - 'width' => $width / 2 - ]; - } - - /** - * Return the multi image - * - * Generate multi image in jpeg format or load it from cache - * - * @param string $pathInfo The path info - * @param string $caption The image caption - * @param int $updated The updated timestamp - * @param float $latitude The latitude - * @param float $longitude The longitude - * @param array $locations The latitude array - * @param int $zoom The zoom - * @param int $width The width - * @param int $height The height - * @return array The image array - */ - public function getMultiImage(string $pathInfo, string $caption, int $updated, float $latitude, float $longitude, array $locations, int $zoom = 18, int $width = 1280, int $height = 1280): array { - //Set path file - $path = $this->path.$pathInfo.'.jpeg'; - - //Set min file - $min = $this->path.$pathInfo.'.min.jpeg'; - - //Without existing path - if (!is_dir($dir = dirname($path))) { - //Create filesystem object - $filesystem = new Filesystem(); - - try { - //Create path - //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) - $filesystem->mkdir($dir, 0775); - } catch (IOExceptionInterface $e) { - //Throw error - throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e); - } - } - - //With path and min up to date file - if (is_file($path) && is_file($min) && ($mtime = stat($path)['mtime']) && ($mintime = stat($min)['mtime']) && $mtime >= $updated && $mintime >= $updated) { - //Return image data - return [ - 'link' => $this->url.'/'.$mtime.$pathInfo.'.jpeg', - 'min' => $this->url.'/'.$mintime.$pathInfo.'.min.jpeg', - 'caption' => $caption, - 'height' => $height / 2, - 'width' => $width / 2 - ]; - } - - //Create image instance - $image = new \Imagick(); - - //Add new image - $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg'); - - //Create tile instance - $tile = new \Imagick(); - - //Init context - $ctx = stream_context_create( - [ - 'http' => [ - #'header' => ['Referer: https://www.openstreetmap.org/'], - 'max_redirects' => 5, - 'timeout' => (int)ini_get('default_socket_timeout'), - #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', - 'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0', - ] - ] - ); - - //Get tile xy - $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom)); - $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom)); - - //Calculate start xy - //XXX: we draw every tile starting beween -($width / 2) and 0 - $startX = floor(($tileX * self::tz - $width) / self::tz); - //XXX: we draw every tile starting beween -($height / 2) and 0 - $startY = floor(($tileY * self::tz - $height) / self::tz); - - //Calculate end xy - //TODO: this seems stupid, check if we may just divide $width / 2 here !!! - //XXX: we draw every tile starting beween $width + ($width / 2) - $endX = ceil(($tileX * self::tz + $width) / self::tz); - //XXX: we draw every tile starting beween $width + ($width / 2) - $endY = ceil(($tileY * self::tz + $height) / self::tz); - - for($x = $startX; $x <= $endX; $x++) { - for($y = $startY; $y <= $endY; $y++) { - //Set cache path - $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png'; - - //Without cache path - if (!is_dir($dir = dirname($cache))) { - //Create filesystem object - $filesystem = new Filesystem(); - - try { - //Create path - //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) - $filesystem->mkdir($dir, 0775); - } catch (IOExceptionInterface $e) { - //Throw error - throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); - } - } - - //Without cache image - if (!is_file($cache)) { - //Set tile url - $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers[$this->server]); - - //Store tile in cache - file_put_contents($cache, file_get_contents($tileUri, false, $ctx)); - } - - //Set dest x - $destX = intval(floor(($width / 2) - self::tz * ($centerX - $x))); - - //Set dest y - $destY = intval(floor(($height / 2) - self::tz * ($centerY - $y))); - - //Read tile from cache - $tile->readImage($cache); - - //Compose image - $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY); - - //Clear tile - $tile->clear(); - } - } - - //Add circle - //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916 - $draw = new \ImagickDraw(); - - //Set text alignment - $draw->setTextAlignment(\Imagick::ALIGN_CENTER); - - //Set text antialias - $draw->setTextAntialias(true); - - //Set stroke antialias - $draw->setStrokeAntialias(true); - - //Iterate on locations - foreach($locations as $k => $location) { - //Set dest x - $destX = intval(floor(($width / 2) - self::tz * ($centerX - $this->longitudeToX($location['longitude'], $zoom)))); - - //Set dest y - $destY = intval(floor(($height / 2) - self::tz * ($centerY - $this->latitudeToY($location['latitude'], $zoom)))); - - //Set fill color - $draw->setFillColor('#cff'); - - //Set font size - $draw->setFontSize(20); - - //Set stroke color - $draw->setStrokeColor('#00c3f9'); - - //Set circle radius - $radius = 5; - - //Set stroke - $stroke = 2; - - //With matching position - if ($location['latitude'] === $latitude && $location['longitude'] == $longitude) { - //Set fill color - $draw->setFillColor('#c3c3f9'); - - //Set font size - $draw->setFontSize(30); - - //Set stroke color - $draw->setStrokeColor('#3333c3'); - - //Set circle radius - $radius = 8; - - //Set stroke - $stroke = 4; - } - - //Set stroke width - $draw->setStrokeWidth($stroke); - - //Draw circle - $draw->circle($destX, $destY - $radius, $destX + $radius * 2, $destY + $radius); - - //Set fill color - $draw->setFillColor($draw->getStrokeColor()); - - //Set stroke width - $draw->setStrokeWidth($stroke / 4); - - //Get font metrics - $metrics = $image->queryFontMetrics($draw, strval($location['id'])); - - //Add annotation - $draw->annotation($destX, $destY - $metrics['descender'] / 3, strval($location['id'])); - } - - //Draw on image - $image->drawImage($draw); - - //Strip image exif data and properties - $image->stripImage(); - - //Add latitude - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude)); - - //Add longitude - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude)); - - //Add description - //XXX: not supported by imagick :'( - $image->setImageProperty('exif:Description', $caption); - - //Set progressive jpeg - $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE); - - //Save image - if (!$image->writeImage($path)) { - //Throw error - throw new \Exception(sprintf('Unable to write image "%s"', $path)); - } - - //Crop using aspect ratio - $image->cropThumbnailImage($width / 2, $height / 2); - - //Set compression quality - $image->setImageCompressionQuality(70); - - //Save min image - if (!$image->writeImage($min)) { - //Throw error - throw new \Exception(sprintf('Unable to write image "%s"', $min)); - } - - //Return image data - return [ - 'link' => $this->url.'/'.stat($path)['mtime'].$pathInfo.'.jpeg', - 'min' => $this->url.'/'.stat($min)['mtime'].$pathInfo.'.min.jpeg', - 'caption' => $caption, - 'height' => $height / 2, - 'width' => $width / 2 - ]; - } - - /** - * Return multi zoom - * - * Compute multi image optimal zoom - * - * @param float $latitude The latitude - * @param float $longitude The longitude - * @param array $locations The latitude array - * @param int $zoom The zoom - * @param int $width The width - * @param int $height The height - * @return int The zoom - */ - public function getMultiZoom(float $latitude, float $longitude, array $locations, int $zoom = 18, int $width = 1280, int $height = 1280): int { - //Iterate on each zoom - for ($i = $zoom; $i >= 1; $i--) { - //Get tile xy - $tileX = floor($this->longitudeToX($longitude, $i)); - $tileY = floor($this->latitudeToY($latitude, $i)); - - //Calculate start xy - $startX = floor(($tileX * self::tz - $width / 2) / self::tz); - $startY = floor(($tileY * self::tz - $height / 2) / self::tz); - - //Calculate end xy - $endX = ceil(($tileX * self::tz + $width / 2) / self::tz); - $endY = ceil(($tileY * self::tz + $height / 2) / self::tz); - - //Iterate on each locations - foreach($locations as $k => $location) { - //Set dest x - $destX = floor($this->longitudeToX($location['longitude'], $i)); - - //With outside point - if ($startX >= $destX || $endX <= $destX) { - //Skip zoom - continue(2); - } - - //Set dest y - $destY = floor($this->latitudeToY($location['latitude'], $i)); - - //With outside point - if ($startY >= $destY || $endY <= $destY) { - //Skip zoom - continue(2); - } - } - - //Found zoom - break; - } - - //Return zoom - return $i; - } - - /** - * Convert longitude to tile x number - * - * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 - * - * @param float $longitude The longitude - * @param int $zoom The zoom - * - * @return float The tile x - */ - public function longitudeToX(float $longitude, int $zoom): float { - return (($longitude + 180) / 360) * pow(2, $zoom); - } - - /** - * Convert latitude to tile y number - * - * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 - * - * @param $latitude The latitude - * @param $zoom The zoom - * - * @return float The tile y - */ - public function latitudeToY(float $latitude, int $zoom): float { - return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom); - } - - /** - * Convert tile x to longitude - * - * @param float $x The tile x - * @param int $zoom The zoom - * - * @return float The longitude - */ - public function xToLongitude(float $x, int $zoom): float { - return $x / pow(2, $zoom) * 360.0 - 180.0; - } - - /** - * Convert tile y to latitude - * - * @param float $y The tile y - * @param int $zoom The zoom - * - * @return float The latitude - */ - public function yToLatitude(float $y, int $zoom): float { - return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom))))); - } - - /** - * Convert decimal latitude to sexagesimal - * - * @param float $latitude The decimal latitude - * - * @return string The sexagesimal longitude - */ - public function latitudeToSexagesimal(float $latitude): string { - //Set degree - $degree = $latitude % 60; - - //Set minute - $minute = ($latitude - $degree) * 60 % 60; - - //Set second - $second = ($latitude - $degree - $minute / 60) * 3600 % 3600; - - //Return sexagesimal longitude - return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S'); - } - - /** - * Convert decimal longitude to sexagesimal - * - * @param float $longitude The decimal longitude - * - * @return string The sexagesimal longitude - */ - public function longitudeToSexagesimal(float $longitude): string { - //Set degree - $degree = $longitude % 60; - - //Set minute - $minute = ($longitude - $degree) * 60 % 60; - - //Set second - $second = ($longitude - $degree - $minute / 60) * 3600 % 3600; - - //Return sexagesimal longitude - return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W'); - } -} -- 2.41.1