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\Component\HttpFoundation\HeaderUtils
;
15 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
;
16 use Symfony\Component\DependencyInjection\ContainerInterface
;
17 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
18 use Symfony\Component\Filesystem\Filesystem
;
19 use Symfony\Component\HttpFoundation\BinaryFileResponse
;
20 use Symfony\Component\HttpFoundation\Request
;
21 use Symfony\Component\HttpFoundation\Response
;
22 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException
;
23 use Symfony\Component\Routing\RequestContext
;
24 use Symfony\Contracts\Service\ServiceSubscriberInterface
;
26 use Rapsys\PackBundle\Util\MapUtil
;
27 use Rapsys\PackBundle\Util\SluggerUtil
;
32 class MapController
extends AbstractController
implements ServiceSubscriberInterface
{
36 protected string $cache;
39 * The ContainerInterface instance
41 * @var ContainerInterface
46 * The stream context instance
51 * The MapUtil instance
53 protected MapUtil
$map;
58 protected string $path;
61 * The SluggerUtil instance
63 protected SluggerUtil
$slugger;
68 protected string $url;
71 * Creates a new osm controller
73 * @param ContainerInterface $container The ContainerInterface instance
74 * @param MapUtil $map The MapUtil instance
75 * @param SluggerUtil $slugger The SluggerUtil instance
76 * @param string $cache The cache path
77 * @param string $path The public path
78 # * @param string $prefix The prefix
79 * @param string $url The tile server url
81 function __construct(ContainerInterface
$container, MapUtil
$map, SluggerUtil
$slugger, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'map', string $url = MapUtil
::osm
) {
83 $this->cache
= $cache.'/'.$prefix;
86 $this->container
= $container;
89 $this->ctx
= stream_context_create(
92 #'header' => ['Referer: https://www.openstreetmap.org/'],
94 'timeout' => (int)ini_get('default_socket_timeout'),
95 #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
96 'user_agent' => (string)ini_get('user_agent')?:'rapsys_pack/2.0.0',
105 $this->path
= $path.'/'.$prefix;
108 $this->slugger
= $slugger;
117 * @param Request $request The Request instance
118 * @param string $hash The hash
119 * @param int $updated The updated timestamp
120 * @param float $latitude The latitude
121 * @param float $longitude The longitude
122 * @param int $zoom The zoom
123 * @param int $width The width
124 * @param int $height The height
125 * @return Response The rendered image
127 public function map(Request
$request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response
{
128 //Without matching hash
129 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
130 //Throw new exception
131 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
135 $map = $this->path
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
137 //Without multi up to date file
138 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
139 //Without existing map path
140 if (!is_dir($dir = dirname($map))) {
141 //Create filesystem object
142 $filesystem = new Filesystem();
146 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
147 //XXX: on CoW filesystems execute a chattr +C before filling
148 $filesystem->mkdir($dir, 0775);
149 } catch (IOExceptionInterface
$e) {
151 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
155 //Create image instance
156 $image = new \
Imagick();
159 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
161 //Create tile instance
162 $tile = new \
Imagick();
165 $centerX = $this->map
->longitudeToX($longitude, $zoom);
166 $centerY = $this->map
->latitudeToY($latitude, $zoom);
169 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
170 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
173 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
174 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
176 for($x = $startX; $x <= $endX; $x++
) {
177 for($y = $startY; $y <= $endY; $y++
) {
179 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
181 //Without cache image
182 if (!is_file($cache)) {
184 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
187 if (!is_dir($dir = dirname($cache))) {
188 //Create filesystem object
189 $filesystem = new Filesystem();
193 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
194 $filesystem->mkdir($dir, 0775);
195 } catch (IOExceptionInterface
$e) {
197 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
201 //Store tile in cache
202 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
206 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
209 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
211 //Read tile from cache
212 $tile->readImage($cache);
215 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
222 //Add imagick draw instance
223 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
224 $draw = new \
ImagickDraw();
227 $draw->setTextAntialias(true);
229 //Set stroke antialias
230 $draw->setStrokeAntialias(true);
233 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
236 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
239 $draw->setFillColor('#cff');
242 $draw->setStrokeColor('#00c3f9');
245 $draw->setStrokeWidth(2);
248 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 +
5, $height/2 +
5);
251 $image->drawImage($draw);
253 //Strip image exif data and properties
254 $image->stripImage();
257 //XXX: not supported by imagick :'(
258 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
261 //XXX: not supported by imagick :'(
262 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
265 //XXX: not supported by imagick :'(
266 #$image->setImageProperty('exif:Description', $caption);
268 //Set progressive jpeg
269 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
271 //Set compression quality
273 $image->setImageCompressionQuality(70);
276 if (!$image->writeImage($map)) {
278 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
282 $mtime = stat($map)['mtime'];
285 //Read map from cache
286 $response = new BinaryFileResponse($map);
289 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
292 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
295 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
297 //Disable robot index
298 $response->headers
->set('X-Robots-Tag', 'noindex');
301 $response->setPublic();
303 //Return 304 response if not modified
304 $response->isNotModified($request);
311 * Return multi map image
313 * @param Request $request The Request instance
314 * @param string $hash The hash
315 * @param int $updated The updated timestamp
316 * @param float $latitude The latitude
317 * @param float $longitude The longitude
318 * @param string $coordinates The coordinates
319 * @param int $zoom The zoom
320 * @param int $width The width
321 * @param int $height The height
322 * @return Response The rendered image
324 public function multiMap(Request
$request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response
{
325 //Without matching hash
326 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger
->hash($coordinates), $zoom, $width, $height])) {
327 //Throw new exception
328 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
332 $map = $this->path
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
334 //Without multi up to date file
335 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
336 //Without existing multi path
337 if (!is_dir($dir = dirname($map))) {
338 //Create filesystem object
339 $filesystem = new Filesystem();
343 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
344 //XXX: on CoW filesystems execute a chattr +C before filling
345 $filesystem->mkdir($dir, 0775);
346 } catch (IOExceptionInterface
$e) {
348 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
352 //Create image instance
353 $image = new \
Imagick();
356 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
358 //Create tile instance
359 $tile = new \
Imagick();
362 $centerX = $this->map
->longitudeToX($longitude, $zoom);
363 $centerY = $this->map
->latitudeToY($latitude, $zoom);
366 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
367 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
370 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
371 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
373 for($x = $startX; $x <= $endX; $x++
) {
374 for($y = $startY; $y <= $endY; $y++
) {
376 $cache = $this->cache
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
378 //Without cache image
379 if (!is_file($cache)) {
381 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
384 if (!is_dir($dir = dirname($cache))) {
385 //Create filesystem object
386 $filesystem = new Filesystem();
390 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
391 $filesystem->mkdir($dir, 0775);
392 } catch (IOExceptionInterface
$e) {
394 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
398 //Store tile in cache
399 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
403 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
406 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
408 //Read tile from cache
409 $tile->readImage($cache);
412 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
419 //Add imagick draw instance
420 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
421 $draw = new \
ImagickDraw();
424 $draw->setTextAntialias(true);
426 //Set stroke antialias
427 $draw->setStrokeAntialias(true);
430 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
433 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
436 $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);
438 //Iterate on locations
439 foreach($coordinates as $id => $coordinate) {
441 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $this->map
->longitudeToX(floatval($coordinate['longitude']), $zoom))));
444 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $this->map
->latitudeToY(floatval($coordinate['latitude']), $zoom))));
447 $draw->setFillColor($this->map
->fill
);
450 $draw->setFontSize($this->map
->fontSize
);
453 $draw->setStrokeColor($this->map
->stroke
);
456 $radius = $this->map
->radius
;
459 $stroke = $this->map
->strokeWidth
;
461 //With matching position
462 if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
464 $draw->setFillColor($this->map
->highFill
);
467 $draw->setFontSize($this->map
->highFontSize
);
470 $draw->setStrokeColor($this->map
->highStroke
);
473 $radius = $this->map
->highRadius
;
476 $stroke = $this->map
->highStrokeWidth
;
480 $draw->setStrokeWidth($stroke);
483 $draw->circle($destX - $radius, $destY - $radius, $destX +
$radius, $destY +
$radius);
486 $draw->setFillColor($draw->getStrokeColor());
489 $draw->setStrokeWidth($stroke / 4);
492 #$metrics = $image->queryFontMetrics($draw, strval($id));
495 $draw->annotation($destX - $radius, $destY +
$stroke, strval($id));
499 $image->drawImage($draw);
501 //Strip image exif data and properties
502 $image->stripImage();
505 //XXX: not supported by imagick :'(
506 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
509 //XXX: not supported by imagick :'(
510 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
513 //XXX: not supported by imagick :'(
514 #$image->setImageProperty('exif:Description', $caption);
516 //Set progressive jpeg
517 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
519 //Set compression quality
521 $image->setImageCompressionQuality(70);
524 if (!$image->writeImage($map)) {
526 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
530 $mtime = stat($map)['mtime'];
533 //Read map from cache
534 $response = new BinaryFileResponse($map);
537 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
540 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
543 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
545 //Disable robot index
546 $response->headers
->set('X-Robots-Tag', 'noindex');
549 $response->setPublic();
551 //Return 304 response if not modified
552 $response->isNotModified($request);