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 Psr\Container\ContainerInterface
;
16 use Symfony\Component\HttpFoundation\HeaderUtils
;
17 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController
;
18 use Symfony\Component\Filesystem\Exception\IOExceptionInterface
;
19 use Symfony\Component\Filesystem\Filesystem
;
20 use Symfony\Component\HttpFoundation\BinaryFileResponse
;
21 use Symfony\Component\HttpFoundation\Request
;
22 use Symfony\Component\HttpFoundation\Response
;
23 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException
;
24 use Symfony\Component\Routing\RequestContext
;
25 use Symfony\Contracts\Service\ServiceSubscriberInterface
;
27 use Rapsys\PackBundle\Util\MapUtil
;
28 use Rapsys\PackBundle\Util\SluggerUtil
;
33 class MapController
extends AbstractController
implements ServiceSubscriberInterface
{
35 * The stream context instance
40 * Creates a new osm controller
42 * @param ContainerInterface $container The ContainerInterface instance
43 * @param MapUtil $map The MapUtil instance
44 * @param SluggerUtil $slugger The SluggerUtil instance
45 * @param string $cache The cache path
46 * @param string $path The public path
47 * @param string $prefix The prefix
48 * @param string $url The tile server url
50 function __construct(protected ContainerInterface
$container, protected MapUtil
$map, protected SluggerUtil
$slugger, protected string $cache = '../var/cache', protected string $path = './bundles/rapsyspack', protected string $prefix = 'map', protected string $url = MapUtil
::osm
) {
52 $this->ctx
= stream_context_create(
55 #'header' => ['Referer: https://www.openstreetmap.org/'],
57 //TODO: set as bundle env config
58 'timeout' => (int)ini_get('default_socket_timeout'),
59 #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
60 //TODO: set as bundle env config
61 'user_agent' => (string)ini_get('user_agent')?:'rapsys_pack/2.0.0',
70 * @param Request $request The Request instance
71 * @param string $hash The hash
72 * @param int $updated The updated timestamp
73 * @param float $latitude The latitude
74 * @param float $longitude The longitude
75 * @param int $zoom The zoom
76 * @param int $width The width
77 * @param int $height The height
78 * @return Response The rendered image
80 public function map(Request
$request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response
{
81 //Without matching hash
82 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
84 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
88 $map = $this->path
.'/'.$this->prefix
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
90 //Without multi up to date file
91 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
92 //Without existing map path
93 if (!is_dir($dir = dirname($map))) {
94 //Create filesystem object
95 $filesystem = new Filesystem();
99 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
100 //XXX: on CoW filesystems execute a chattr +C before filling
101 $filesystem->mkdir($dir, 0775);
102 } catch (IOExceptionInterface
$e) {
104 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
108 //Create image instance
109 $image = new \
Imagick();
112 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
114 //Create tile instance
115 $tile = new \
Imagick();
118 $centerX = $this->map
->longitudeToX($longitude, $zoom);
119 $centerY = $this->map
->latitudeToY($latitude, $zoom);
122 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
123 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
126 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
127 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
129 for($x = $startX; $x <= $endX; $x++
) {
130 for($y = $startY; $y <= $endY; $y++
) {
132 $cache = $this->cache
.'/'.$this->prefix
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
134 //Without cache image
135 if (!is_file($cache)) {
137 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
140 if (!is_dir($dir = dirname($cache))) {
141 //Create filesystem object
142 $filesystem = new Filesystem();
146 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
147 $filesystem->mkdir($dir, 0775);
148 } catch (IOExceptionInterface
$e) {
150 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
154 //Store tile in cache
155 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
159 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
162 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
164 //Read tile from cache
165 $tile->readImage($cache);
168 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
175 //Add imagick draw instance
176 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
177 $draw = new \
ImagickDraw();
180 $draw->setTextAntialias(true);
182 //Set stroke antialias
183 $draw->setStrokeAntialias(true);
186 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
189 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
192 $draw->setFillColor('#cff');
195 $draw->setStrokeColor('#00c3f9');
198 $draw->setStrokeWidth(2);
201 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 +
5, $height/2 +
5);
204 $image->drawImage($draw);
206 //Strip image exif data and properties
207 $image->stripImage();
210 //XXX: not supported by imagick :'(
211 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
214 //XXX: not supported by imagick :'(
215 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
218 //XXX: not supported by imagick :'(
219 #$image->setImageProperty('exif:Description', $caption);
221 //Set progressive jpeg
222 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
224 //Set compression quality
226 $image->setImageCompressionQuality(70);
229 if (!$image->writeImage($map)) {
231 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
235 $mtime = stat($map)['mtime'];
238 //Read map from cache
239 $response = new BinaryFileResponse($map);
242 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
245 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
248 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
250 //Disable robot index
251 $response->headers
->set('X-Robots-Tag', 'noindex');
254 $response->setPublic();
256 //Return 304 response if not modified
257 $response->isNotModified($request);
264 * Return multi map image
266 * @param Request $request The Request instance
267 * @param string $hash The hash
268 * @param int $updated The updated timestamp
269 * @param float $latitude The latitude
270 * @param float $longitude The longitude
271 * @param string $coordinates The coordinates
272 * @param int $zoom The zoom
273 * @param int $width The width
274 * @param int $height The height
275 * @return Response The rendered image
277 public function multiMap(Request
$request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response
{
278 //Without matching hash
279 if ($hash !== $this->slugger
->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger
->hash($coordinates), $zoom, $width, $height])) {
280 //Throw new exception
281 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
285 $map = $this->path
.'/'.$this->prefix
.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
287 //Without multi up to date file
288 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
289 //Without existing multi path
290 if (!is_dir($dir = dirname($map))) {
291 //Create filesystem object
292 $filesystem = new Filesystem();
296 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
297 //XXX: on CoW filesystems execute a chattr +C before filling
298 $filesystem->mkdir($dir, 0775);
299 } catch (IOExceptionInterface
$e) {
301 throw new \
Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
305 //Create image instance
306 $image = new \
Imagick();
309 $image->newImage($width, $height, new \
ImagickPixel('transparent'), 'jpeg');
311 //Create tile instance
312 $tile = new \
Imagick();
315 $centerX = $this->map
->longitudeToX($longitude, $zoom);
316 $centerY = $this->map
->latitudeToY($latitude, $zoom);
319 $startX = floor(floor($centerX) - $width / MapUtil
::tz
);
320 $startY = floor(floor($centerY) - $height / MapUtil
::tz
);
323 $endX = ceil(ceil($centerX) +
$width / MapUtil
::tz
);
324 $endY = ceil(ceil($centerY) +
$height / MapUtil
::tz
);
326 for($x = $startX; $x <= $endX; $x++
) {
327 for($y = $startY; $y <= $endY; $y++
) {
329 $cache = $this->cache
.'/'.$this->prefix
.'/'.$zoom.'/'.$x.'/'.$y.'.png';
331 //Without cache image
332 if (!is_file($cache)) {
334 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url
);
337 if (!is_dir($dir = dirname($cache))) {
338 //Create filesystem object
339 $filesystem = new Filesystem();
343 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
344 $filesystem->mkdir($dir, 0775);
345 } catch (IOExceptionInterface
$e) {
347 throw new \
Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
351 //Store tile in cache
352 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx
));
356 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $x)));
359 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $y)));
361 //Read tile from cache
362 $tile->readImage($cache);
365 $image->compositeImage($tile, \Imagick
::COMPOSITE_OVER
, $destX, $destY);
372 //Add imagick draw instance
373 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
374 $draw = new \
ImagickDraw();
377 $draw->setTextAntialias(true);
379 //Set stroke antialias
380 $draw->setStrokeAntialias(true);
383 $draw->setTextAlignment(\Imagick
::ALIGN_CENTER
);
386 $draw->setGravity(\Imagick
::GRAVITY_CENTER
);
389 $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);
391 //Iterate on locations
392 foreach($coordinates as $id => $coordinate) {
394 $destX = intval(floor($width / 2 - MapUtil
::tz
* ($centerX - $this->map
->longitudeToX(floatval($coordinate['longitude']), $zoom))));
397 $destY = intval(floor($height / 2 - MapUtil
::tz
* ($centerY - $this->map
->latitudeToY(floatval($coordinate['latitude']), $zoom))));
400 $draw->setFillColor($this->map
->getFill());
403 $draw->setFontSize($this->map
->getFontSize());
406 $draw->setStrokeColor($this->map
->getStroke());
409 $radius = $this->map
->getRadius();
412 $stroke = $this->map
->getStrokeWidth();
414 //With matching position
415 if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
417 $draw->setFillColor($this->map
->getHighFill());
420 $draw->setFontSize($this->map
->getHighFontSize());
423 $draw->setStrokeColor($this->map
->getHighStroke());
426 $radius = $this->map
->getHighRadius();
429 $stroke = $this->map
->getHighStrokeWidth();
433 $draw->setStrokeWidth($stroke);
436 $draw->circle($destX - $radius, $destY - $radius, $destX +
$radius, $destY +
$radius);
439 $draw->setFillColor($draw->getStrokeColor());
442 $draw->setStrokeWidth($stroke / 4);
445 #$metrics = $image->queryFontMetrics($draw, strval($id));
448 $draw->annotation($destX - $radius, $destY +
$stroke, strval($id));
452 $image->drawImage($draw);
454 //Strip image exif data and properties
455 $image->stripImage();
458 //XXX: not supported by imagick :'(
459 $image->setImageProperty('exif:GPSLatitude', $this->map
->latitudeToSexagesimal($latitude));
462 //XXX: not supported by imagick :'(
463 $image->setImageProperty('exif:GPSLongitude', $this->map
->longitudeToSexagesimal($longitude));
466 //XXX: not supported by imagick :'(
467 #$image->setImageProperty('exif:Description', $caption);
469 //Set progressive jpeg
470 $image->setInterlaceScheme(\Imagick
::INTERLACE_PLANE
);
472 //Set compression quality
474 $image->setImageCompressionQuality(70);
477 if (!$image->writeImage($map)) {
479 throw new \
Exception(sprintf('Unable to write image "%s"', $path));
483 $mtime = stat($map)['mtime'];
486 //Read map from cache
487 $response = new BinaryFileResponse($map);
490 $response->setContentDisposition(HeaderUtils
::DISPOSITION_INLINE
, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
493 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
496 $response->setLastModified(\DateTime
::createFromFormat('U', strval($mtime)));
498 //Disable robot index
499 $response->headers
->set('X-Robots-Tag', 'noindex');
502 $response->setPublic();
504 //Return 304 response if not modified
505 $response->isNotModified($request);