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\Controller
;
14 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
;
15 use Symfony\Component\DependencyInjection\ContainerInterface
;
16 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
17 use Symfony\Component\Filesystem\Filesystem
;
18 use Symfony\Component\HttpFoundation\BinaryFileResponse
;
19 use Symfony\Component\HttpFoundation\Request
;
20 use Symfony\Component\HttpFoundation\Response
;
21 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException
;
22 use Symfony\Component\Routing\RequestContext
;
23 use Symfony\Contracts\Service\ServiceSubscriberInterface
;
25 use Rapsys\PackBundle\Util\MapUtil
;
26 use Rapsys\PackBundle\Util\SluggerUtil
;
31 class MapController
extends AbstractController
implements ServiceSubscriberInterface
{
35 protected string $cache;
38 * The ContainerInterface instance
40 * @var ContainerInterface
45 * The stream context instance
50 * The MapUtil instance
52 protected MapUtil
$map;
57 protected string $public;
60 * The SluggerUtil instance
62 protected SluggerUtil
$slugger;
67 protected string $url;
70 * Creates a new osm util
72 * @param ContainerInterface $container The ContainerInterface instance
73 * @param MapUtil $map The MapUtil instance
74 * @param SluggerUtil $slugger The SluggerUtil instance
75 * @param string $cache The cache path
76 * @param string $public The public path
77 * @param string $url The tile server url
79 function __construct(ContainerInterface
$container, MapUtil
$map, SluggerUtil
$slugger, string $cache = '../var/cache/map', string $public = './bundles/rapsyspack/map', string $url = MapUtil
::osm
) {
81 $this->cache
= $cache;
84 $this->container
= $container;
87 $this->ctx
= stream_context_create(
90 #'header' => ['Referer: https://www.openstreetmap.org/'],
92 'timeout' => (int)ini_get('default_socket_timeout'),
93 #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
94 'user_agent' => (string)ini_get('user_agent')?:'rapsys_pack/2.0.0',
103 $this->public = $public;
106 $this->slugger
= $slugger;
115 * @param Request $request The Request instance
116 * @param string $hash The hash
117 * @param int $updated The updated timestamp
118 * @param float $latitude The latitude
119 * @param float $longitude The longitude
120 * @param int $zoom The zoom
121 * @param int $width The width
122 * @param int $height The height
123 * @return Response The rendered image
125 public function map(Request
$request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response
{
126 //Without matching hash
127 if ($hash !== $this->slugger
->serialize([$updated, $latitude, $longitude, $zoom, $width, $height])) {
128 //Throw new exception
129 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
133 $map = $this->public.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
135 //With map up to date file
136 if (is_file($map) && ($mtime = stat($map)['mtime']) && $mtime >= $updated) {
137 //Read map from cache
138 //TODO: handle modified, etag, cache, etc ???
139 return new BinaryFileResponse($map);
142 //Without existing map
143 if (!is_dir($dir = dirname($map))) {
144 //Create filesystem object
145 $filesystem = new Filesystem();
149 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
150 //XXX: on CoW filesystems execute a chattr +C before filling
151 $filesystem->mkdir($dir, 0775);
152 } catch (IOExceptionInterface
$e) {
154 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
158 //Create image instance
159 $image = new \
Imagick();
162 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
164 //Create tile instance
165 $tile = new \
Imagick();
168 $centerX = $this->map
->longitudeToX($longitude, $zoom);
169 $centerY = $this->map
->latitudeToY($latitude, $zoom);
172 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
173 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
176 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
177 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
179 for($x = $startX; $x <= $endX; $x++
) {
180 for($y = $startY; $y <= $endY; $y++
) {
182 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
184 //Without cache image
185 if (!is_file($cache)) {
187 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
190 if (!is_dir($dir = dirname($cache))) {
191 //Create filesystem object
192 $filesystem = new Filesystem();
196 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
197 $filesystem->mkdir($dir, 0775);
198 } catch (IOExceptionInterface
$e) {
200 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
204 //Store tile in cache
205 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
209 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
212 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
214 //Read tile from cache
215 $tile->readImage($cache);
218 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
225 //Add imagick draw instance
226 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
227 $draw = new \
ImagickDraw();
230 $draw->setTextAntialias(true);
232 //Set stroke antialias
233 $draw->setStrokeAntialias(true);
236 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
239 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
242 $draw->setFillColor('#cff');
245 $draw->setStrokeColor('#00c3f9');
248 $draw->setStrokeWidth(2);
251 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 +
5, $height/2 +
5);
254 $image->drawImage($draw);
256 //Strip image exif data and properties
257 $image->stripImage();
260 //XXX: not supported by imagick :'(
261 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
264 //XXX: not supported by imagick :'(
265 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
268 //XXX: not supported by imagick :'(
269 #$image->setImageProperty('exif:Description', $caption);
271 //Set progressive jpeg
272 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
274 //Set compression quality
276 $image->setImageCompressionQuality(70);
279 if (!$image->writeImage($map)) {
281 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
285 //TODO: générer l'image ici à partir du cache :p
286 #return new Response($image->getImageBlob());
287 return new BinaryFileResponse($map);
291 * Return multi map image
293 * @param Request $request The Request instance
294 * @param string $hash The hash
295 * @param int $updated The updated timestamp
296 * @param float $latitude The latitude
297 * @param float $longitude The longitude
298 * @param string $coordinates The coordinates
299 * @param int $zoom The zoom
300 * @param int $width The width
301 * @param int $height The height
302 * @return Response The rendered image
304 public function multiMap(Request
$request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response
{
305 //Without matching hash
306 if ($hash !== $this->slugger
->serialize([$updated, $latitude, $longitude, $coordinate = $this->slugger
->hash($coordinates), $zoom, $width, $height])) {
307 //Throw new exception
308 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
312 $map = $this->public.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
314 //With multi up to date file
315 if (is_file($map) && ($mtime = stat($map)['mtime']) && $mtime >= $updated) {
316 //Read multi from cache
317 //TODO: handle modified, etag, cache, etc ???
318 return new BinaryFileResponse($map);
321 //Without existing multi
322 if (!is_dir($dir = dirname($map))) {
323 //Create filesystem object
324 $filesystem = new Filesystem();
328 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
329 //XXX: on CoW filesystems execute a chattr +C before filling
330 $filesystem->mkdir($dir, 0775);
331 } catch (IOExceptionInterface
$e) {
333 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
337 //Create image instance
338 $image = new \
Imagick();
341 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
343 //Create tile instance
344 $tile = new \
Imagick();
347 $centerX = $this->map
->longitudeToX($longitude, $zoom);
348 $centerY = $this->map
->latitudeToY($latitude, $zoom);
351 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
352 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
355 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
356 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
358 for($x = $startX; $x <= $endX; $x++
) {
359 for($y = $startY; $y <= $endY; $y++
) {
361 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
363 //Without cache image
364 if (!is_file($cache)) {
366 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
369 if (!is_dir($dir = dirname($cache))) {
370 //Create filesystem object
371 $filesystem = new Filesystem();
375 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
376 $filesystem->mkdir($dir, 0775);
377 } catch (IOExceptionInterface
$e) {
379 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
383 //Store tile in cache
384 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
388 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
391 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
393 //Read tile from cache
394 $tile->readImage($cache);
397 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
404 //Add imagick draw instance
405 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
406 $draw = new \
ImagickDraw();
409 $draw->setTextAntialias(true);
411 //Set stroke antialias
412 $draw->setStrokeAntialias(true);
415 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
418 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
421 $coordinates = array_reverse(array_map(function ($v) { $p
= strpos($v
, ','); return ['latitude' => floatval(substr($v
, 0, $p
)), 'longitude' => floatval(substr($v
, $p +
1))]; }, explode('-', $coordinates)), true);
423 //Iterate on locations
424 foreach($coordinates as $id => $coordinate) {
426 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $this->map
->longitudeToX(floatval($coordinate['longitude']), $zoom))));
429 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $this->map
->latitudeToY(floatval($coordinate['latitude']), $zoom))));
432 $draw->setFillColor($this->map
->fill
);
435 $draw->setFontSize($this->map
->fontSize
);
438 $draw->setStrokeColor($this->map
->stroke
);
441 $radius = $this->map
->radius
;
444 $stroke = $this->map
->strokeWidth
;
446 //With matching position
447 if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
449 $draw->setFillColor($this->map
->highFill
);
452 $draw->setFontSize($this->map
->highFontSize
);
455 $draw->setStrokeColor($this->map
->highStroke
);
458 $radius = $this->map
->highRadius
;
461 $stroke = $this->map
->highStrokeWidth
;
465 $draw->setStrokeWidth($stroke);
468 $draw->circle($destX - $radius, $destY - $radius, $destX +
$radius, $destY +
$radius);
471 $draw->setFillColor($draw->getStrokeColor());
474 $draw->setStrokeWidth($stroke / 4);
477 $metrics = $image->queryFontMetrics($draw, strval($id));
480 $draw->annotation($destX - $radius, $destY +
$stroke, strval($id));
484 $image->drawImage($draw);
486 //Strip image exif data and properties
487 $image->stripImage();
490 //XXX: not supported by imagick :'(
491 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
494 //XXX: not supported by imagick :'(
495 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
498 //XXX: not supported by imagick :'(
499 #$image->setImageProperty('exif:Description', $caption);
501 //Set progressive jpeg
502 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
504 //Set compression quality
506 $image->setImageCompressionQuality(70);
509 if (!$image->writeImage($map)) {
511 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
515 //TODO: générer l'image ici à partir du cache :p
516 #return new Response($image->getImageBlob());
517 return new BinaryFileResponse($map);