1 <?php 
declare(strict_types
=1); 
   4  * This file is part of the Rapsys PackBundle package. 
   6  * (c) Raphaël Gertz <symfony@rapsys.eu> 
   8  * For the full copyright and license information, please view the LICENSE 
   9  * file that was distributed with this source code. 
  12 namespace Rapsys\PackBundle\Util
; 
  14 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
; 
  15 use Symfony\Component\Filesystem\Filesystem
; 
  18  * Helps manage osm images 
  44          * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers 
  46         protected $servers = [ 
  47                 'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png', 
  48                 'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png', 
  49                 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png' 
  58          * Creates a new osm util 
  60          * @param string $cache The cache directory 
  61          * @param string $path The public path 
  62          * @param string $url The public url 
  63          * @param string $server The server key 
  65         function __construct(string $cache, string $path, string $url, string $server = 'osm') { 
  67                 $this->cache 
= $cache.'/'.$server; 
  70                 $this->path 
= $path.'/'.$server; 
  73                 $this->url 
= $url.'/'.$server; 
  76                 $this->server 
= $server; 
  82          * @desc Generate image in jpeg format or load it from cache 
  84          * @param string $pathInfo The path info 
  85          * @param string $alt The image alt 
  86          * @param int $updated The updated timestamp 
  87          * @param float $latitude The latitude 
  88          * @param float $longitude The longitude 
  89          * @param int $zoom The zoom 
  90          * @param int $width The width 
  91          * @param int $height The height 
  92          * @return array The image array 
  94         public function getImage(string $pathInfo, string $alt, int $updated, float $latitude, float $longitude, int $zoom = 18, int $width = 1280, int $height = 1280): array { 
  96                 $path = $this->path
.$pathInfo.'.jpeg'; 
  98                 //Without existing path 
  99                 if (!is_dir($dir = dirname($path))) { 
 100                         //Create filesystem object 
 101                         $filesystem = new Filesystem(); 
 105                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 106                                 $filesystem->mkdir($dir, 0775); 
 107                         } catch (IOExceptionInterface 
$e) { 
 109                                 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e); 
 117                         ($stat = stat($path)) && 
 118                         $stat['mtime'] >= $updated 
 122                                 'src' => $this->url
.'/'.$stat['mtime'].$pathInfo.'.jpeg', 
 129                 //Create image instance 
 130                 $image = new \
Imagick(); 
 133                 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg'); 
 135                 //Create tile instance 
 136                 $tile = new \
Imagick(); 
 139                 $ctx = stream_context_create( 
 142                                         #'header' => ['Referer: https://www.openstreetmap.org/'], 
 143                                         'max_redirects' => 5, 
 144                                         'timeout' => (int)ini_get('default_socket_timeout'), 
 145                                         #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', 
 146                                         'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0', 
 152                 $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom)); 
 153                 $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom)); 
 156                 $startX = floor(($tileX * self
::tz 
- $width) / self
::tz
); 
 157                 $startY = floor(($tileY * self
::tz 
- $height) / self
::tz
); 
 160                 $endX = ceil(($tileX * self
::tz + 
$width) / self
::tz
); 
 161                 $endY = ceil(($tileY * self
::tz + 
$height) / self
::tz
); 
 163                 for($x = $startX; $x <= $endX; $x++
) { 
 164                         for($y = $startY; $y <= $endY; $y++
) { 
 166                                 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png'; 
 169                                 if (!is_dir($dir = dirname($cache))) { 
 170                                         //Create filesystem object 
 171                                         $filesystem = new Filesystem(); 
 175                                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 176                                                 $filesystem->mkdir($dir, 0775); 
 177                                         } catch (IOExceptionInterface 
$e) { 
 179                                                 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 183                                 //Without cache image 
 184                                 if (!is_file($cache)) { 
 186                                         $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers
[$this->server
]); 
 188                                         //Store tile in cache 
 189                                         file_put_contents($cache, file_get_contents($tileUri, false, $ctx)); 
 193                                 $destX = intval(floor(($width / 2) - self
::tz 
* ($centerX - $x))); 
 196                                 $destY = intval(floor(($height / 2) - self
::tz 
* ($centerY - $y))); 
 198                                 //Read tile from cache 
 199                                 $tile->readImage($cache); 
 202                                 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY); 
 210                 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916 
 211                 $draw = new \
ImagickDraw(); 
 214                 $draw->setTextAntialias(true); 
 217                 $draw->setFillColor('#cff'); 
 220                 $draw->setStrokeColor('#00c3f9'); 
 223                 $draw->setStrokeWidth(2); 
 226                 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 
5, $height/2 + 
5); 
 227                 #$draw->circle($tileX/self::tz - 5, $tileY/self::tz - 5, $tileX/self::tz + 5, $tileY/self::tz + 5); 
 230                 $image->drawImage($draw); 
 232                 //Strip image exif data and properties 
 233                 $image->stripImage(); 
 236                 //XXX: not supported by imagick :'( 
 237                 $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude)); 
 240                 //XXX: not supported by imagick :'( 
 241                 $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude)); 
 244                 //XXX: not supported by imagick :'( 
 245                 $image->setImageProperty('exif:Description', $alt); 
 248                 if (!$image->writeImage($path)) { 
 250                         throw new \
Exception(sprintf('Unable to write image "%s"', $dest)); 
 258                         'src' => $this->url
.'/'.$stat['mtime'].$pathInfo.'.jpeg', 
 266          * Convert longitude to tile x number 
 268          * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 
 270          * @param float $longitude The longitude 
 271          * @param int $zoom The zoom 
 273          * @return float The tile x 
 275         public function longitudeToX(float $longitude, int $zoom): float { 
 276                 return (($longitude + 
180) / 360) * pow(2, $zoom); 
 280          * Convert latitude to tile y number 
 282          * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 
 284          * @param $latitude The latitude 
 285          * @param $zoom The zoom 
 287          * @return float The tile y 
 289         public function latitudeToY(float $latitude, int $zoom): float { 
 290                 return (1 - log(tan(deg2rad($latitude)) + 
1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom); 
 294          * Convert tile x to longitude 
 296          * @param float $x The tile x 
 297          * @param int $zoom The zoom 
 299          * @return float The longitude 
 301         public function xToLongitude(float $x, int $zoom): float { 
 302                 return $x / pow(2, $zoom) * 360.0 - 180.0; 
 306          * Convert tile y to latitude 
 308          * @param float $y The tile y 
 309          * @param int $zoom The zoom 
 311          * @return float The latitude 
 313         public function yToLatitude(float $y, int $zoom): float { 
 314                 return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom))))); 
 318          * Convert decimal latitude to sexagesimal 
 320          * @param float $latitude The decimal latitude 
 322          * @return string The sexagesimal longitude 
 324         public function latitudeToSexagesimal(float $latitude): string { 
 326                 $degree = $latitude % 
60; 
 329                 $minute = ($latitude - $degree) * 60 % 
60; 
 332                 $second = ($latitude - $degree - $minute / 60) * 3600 % 
3600; 
 334                 //Return sexagesimal longitude 
 335                 return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S'); 
 339          * Convert decimal longitude to sexagesimal 
 341          * @param float $longitude The decimal longitude 
 343          * @return string The sexagesimal longitude 
 345         public function longitudeToSexagesimal(float $longitude): string { 
 347                 $degree = $longitude % 
60; 
 350                 $minute = ($longitude - $degree) * 60 % 
60; 
 353                 $second = ($longitude - $degree - $minute / 60) * 3600 % 
3600; 
 355                 //Return sexagesimal longitude 
 356                 return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');