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');