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); 
 116                         ($stat = stat($path)) && 
 117                         $stat['mtime'] >= $updated 
 121                                 'src' => $this->url
.'/'.$stat['mtime'].$pathInfo.'.jpeg', 
 128                 //Create image instance 
 129                 $image = new \
Imagick(); 
 132                 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg'); 
 134                 //Create tile instance 
 135                 $tile = new \
Imagick(); 
 138                 $ctx = stream_context_create( 
 141                                         #'header' => ['Referer: https://www.openstreetmap.org/'], 
 142                                         'max_redirects' => 5, 
 143                                         'timeout' => (int)ini_get('default_socket_timeout'), 
 144                                         #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36', 
 145                                         'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0', 
 151                 $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom)); 
 152                 $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom)); 
 155                 $startX = floor(($tileX * self
::tz 
- $width) / self
::tz
); 
 156                 $startY = floor(($tileY * self
::tz 
- $height) / self
::tz
); 
 159                 $endX = ceil(($tileX * self
::tz + 
$width) / self
::tz
); 
 160                 $endY = ceil(($tileY * self
::tz + 
$height) / self
::tz
); 
 162                 for($x = $startX; $x <= $endX; $x++
) { 
 163                         for($y = $startY; $y <= $endY; $y++
) { 
 165                                 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png'; 
 168                                 if (!is_dir($dir = dirname($cache))) { 
 169                                         //Create filesystem object 
 170                                         $filesystem = new Filesystem(); 
 174                                                 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755) 
 175                                                 $filesystem->mkdir($dir, 0775); 
 176                                         } catch (IOExceptionInterface 
$e) { 
 178                                                 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e); 
 182                                 //Without cache image 
 183                                 if (!is_file($cache)) { 
 185                                         $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers
[$this->server
]); 
 187                                         //Store tile in cache 
 188                                         file_put_contents($cache, file_get_contents($tileUri, false, $ctx)); 
 192                                 $destX = intval(floor(($width / 2) - self
::tz 
* ($centerX - $x))); 
 195                                 $destY = intval(floor(($height / 2) - self
::tz 
* ($centerY - $y))); 
 197                                 //Read tile from cache 
 198                                 $tile->readImage($cache); 
 201                                 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY); 
 209                 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916 
 210                 $draw = new \
ImagickDraw(); 
 213                 $draw->setTextAntialias(true); 
 216                 $draw->setFillColor('#cff'); 
 219                 $draw->setStrokeColor('#00c3f9'); 
 222                 $draw->setStrokeWidth(2); 
 225                 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 
5, $height/2 + 
5); 
 226                 #$draw->circle($tileX/self::tz - 5, $tileY/self::tz - 5, $tileX/self::tz + 5, $tileY/self::tz + 5); 
 229                 $image->drawImage($draw); 
 231                 //Strip image exif data and properties 
 232                 $image->stripImage(); 
 235                 //XXX: not supported by imagick :'( 
 236                 $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude)); 
 239                 //XXX: not supported by imagick :'( 
 240                 $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude)); 
 243                 //XXX: not supported by imagick :'( 
 244                 $image->setImageProperty('exif:Description', $alt); 
 247                 if (!$image->writeImage($path)) { 
 249                         throw new \
Exception(sprintf('Unable to write image "%s"', $dest)); 
 257                         'src' => $this->url
.'/'.$stat['mtime'].$pathInfo.'.jpeg', 
 265          * Convert longitude to tile x number 
 267          * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 
 269          * @param float $longitude The longitude 
 270          * @param int $zoom The zoom 
 272          * @return float The tile x 
 274         public function longitudeToX(float $longitude, int $zoom): float { 
 275                 return (($longitude + 
180) / 360) * pow(2, $zoom); 
 279          * Convert latitude to tile y number 
 281          * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5 
 283          * @param $latitude The latitude 
 284          * @param $zoom The zoom 
 286          * @return float The tile y 
 288         public function latitudeToY(float $latitude, int $zoom): float { 
 289                 return (1 - log(tan(deg2rad($latitude)) + 
1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom); 
 293          * Convert tile x to longitude 
 295          * @param float $x The tile x 
 296          * @param int $zoom The zoom 
 298          * @return float The longitude 
 300         public function xToLongitude(float $x, int $zoom): float { 
 301                 return $x / pow(2, $zoom) * 360.0 - 180.0; 
 305          * Convert tile y to latitude 
 307          * @param float $y The tile y 
 308          * @param int $zoom The zoom 
 310          * @return float The latitude 
 312         public function yToLatitude(float $y, int $zoom): float { 
 313                 return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom))))); 
 317          * Convert decimal latitude to sexagesimal 
 319          * @param float $latitude The decimal latitude 
 321          * @return string The sexagesimal longitude 
 323         public function latitudeToSexagesimal(float $latitude): string { 
 325                 $degree = $latitude % 
60; 
 328                 $minute = ($latitude - $degree) * 60 % 
60; 
 331                 $second = ($latitude - $degree - $minute / 60) * 3600 % 
3600; 
 333                 //Return sexagesimal longitude 
 334                 return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S'); 
 338          * Convert decimal longitude to sexagesimal 
 340          * @param float $longitude The decimal longitude 
 342          * @return string The sexagesimal longitude 
 344         public function longitudeToSexagesimal(float $longitude): string { 
 346                 $degree = $longitude % 
60; 
 349                 $minute = ($longitude - $degree) * 60 % 
60; 
 352                 $second = ($longitude - $degree - $minute / 60) * 3600 % 
3600; 
 354                 //Return sexagesimal longitude 
 355                 return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');