]> Raphaël G. Git Repositories - packbundle/blob - Controller/MapController.php
Fix deprecated implicit conversion from float to int loses precision message
[packbundle] / Controller / MapController.php
1 <?php declare(strict_types=1);
2
3 /*
4 * This file is part of the Rapsys PackBundle package.
5 *
6 * (c) Raphaël Gertz <symfony@rapsys.eu>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Rapsys\PackBundle\Controller;
13
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;
25
26 use Rapsys\PackBundle\Util\MapUtil;
27 use Rapsys\PackBundle\Util\SluggerUtil;
28
29 /**
30 * {@inheritdoc}
31 */
32 class MapController extends AbstractController implements ServiceSubscriberInterface {
33 /**
34 * The cache path
35 */
36 protected string $cache;
37
38 /**
39 * The stream context instance
40 */
41 protected mixed $ctx;
42
43 /**
44 * The MapUtil instance
45 */
46 protected MapUtil $map;
47
48 /**
49 * The public path
50 */
51 protected string $path;
52
53 /**
54 * The SluggerUtil instance
55 */
56 protected SluggerUtil $slugger;
57
58 /**
59 * The tile server url
60 */
61 protected string $url;
62
63 /**
64 * Creates a new osm controller
65 *
66 * @param ContainerInterface $container The ContainerInterface instance
67 * @param MapUtil $map The MapUtil instance
68 * @param SluggerUtil $slugger The SluggerUtil instance
69 * @param string $cache The cache path
70 * @param string $path The public path
71 # * @param string $prefix The prefix
72 * @param string $url The tile server url
73 */
74 function __construct(ContainerInterface $container, MapUtil $map, SluggerUtil $slugger, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'map', string $url = MapUtil::osm) {
75 //Set cache
76 $this->cache = $cache.'/'.$prefix;
77
78 //Set container
79 $this->container = $container;
80
81 //Set ctx
82 $this->ctx = stream_context_create(
83 [
84 'http' => [
85 #'header' => ['Referer: https://www.openstreetmap.org/'],
86 'max_redirects' => 5,
87 'timeout' => (int)ini_get('default_socket_timeout'),
88 #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
89 'user_agent' => (string)ini_get('user_agent')?:'rapsys_pack/2.0.0',
90 ]
91 ]
92 );
93
94 //Set map
95 $this->map = $map;
96
97 //Set path
98 $this->path = $path.'/'.$prefix;
99
100 //Set slugger
101 $this->slugger = $slugger;
102
103 //Set url
104 $this->url = $url;
105 }
106
107 /**
108 * Return map image
109 *
110 * @param Request $request The Request instance
111 * @param string $hash The hash
112 * @param int $updated The updated timestamp
113 * @param float $latitude The latitude
114 * @param float $longitude The longitude
115 * @param int $zoom The zoom
116 * @param int $width The width
117 * @param int $height The height
118 * @return Response The rendered image
119 */
120 public function map(Request $request, string $hash, int $updated, float $latitude, float $longitude, int $zoom, int $width, int $height): Response {
121 //Without matching hash
122 if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height])) {
123 //Throw new exception
124 throw new NotFoundHttpException(sprintf('Unable to match map hash: %s', $hash));
125 }
126
127 //Set map
128 $map = $this->path.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$width.'x'.$height.'.jpeg';
129
130 //Without multi up to date file
131 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
132 //Without existing map path
133 if (!is_dir($dir = dirname($map))) {
134 //Create filesystem object
135 $filesystem = new Filesystem();
136
137 try {
138 //Create path
139 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
140 //XXX: on CoW filesystems execute a chattr +C before filling
141 $filesystem->mkdir($dir, 0775);
142 } catch (IOExceptionInterface $e) {
143 //Throw error
144 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
145 }
146 }
147
148 //Create image instance
149 $image = new \Imagick();
150
151 //Add new image
152 $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
153
154 //Create tile instance
155 $tile = new \Imagick();
156
157 //Get tile xy
158 $centerX = $this->map->longitudeToX($longitude, $zoom);
159 $centerY = $this->map->latitudeToY($latitude, $zoom);
160
161 //Calculate start xy
162 $startX = floor(floor($centerX) - $width / MapUtil::tz);
163 $startY = floor(floor($centerY) - $height / MapUtil::tz);
164
165 //Calculate end xy
166 $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
167 $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
168
169 for($x = $startX; $x <= $endX; $x++) {
170 for($y = $startY; $y <= $endY; $y++) {
171 //Set cache path
172 $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png';
173
174 //Without cache image
175 if (!is_file($cache)) {
176 //Set tile url
177 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
178
179 //Without cache path
180 if (!is_dir($dir = dirname($cache))) {
181 //Create filesystem object
182 $filesystem = new Filesystem();
183
184 try {
185 //Create path
186 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
187 $filesystem->mkdir($dir, 0775);
188 } catch (IOExceptionInterface $e) {
189 //Throw error
190 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
191 }
192 }
193
194 //Store tile in cache
195 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
196 }
197
198 //Set dest x
199 $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
200
201 //Set dest y
202 $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
203
204 //Read tile from cache
205 $tile->readImage($cache);
206
207 //Compose image
208 $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
209
210 //Clear tile
211 $tile->clear();
212 }
213 }
214
215 //Add imagick draw instance
216 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
217 $draw = new \ImagickDraw();
218
219 //Set text antialias
220 $draw->setTextAntialias(true);
221
222 //Set stroke antialias
223 $draw->setStrokeAntialias(true);
224
225 //Set text alignment
226 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
227
228 //Set gravity
229 $draw->setGravity(\Imagick::GRAVITY_CENTER);
230
231 //Set fill color
232 $draw->setFillColor('#cff');
233
234 //Set stroke color
235 $draw->setStrokeColor('#00c3f9');
236
237 //Set stroke width
238 $draw->setStrokeWidth(2);
239
240 //Draw circle
241 $draw->circle($width/2 - 5, $height/2 - 5, $width/2 + 5, $height/2 + 5);
242
243 //Draw on image
244 $image->drawImage($draw);
245
246 //Strip image exif data and properties
247 $image->stripImage();
248
249 //Add latitude
250 //XXX: not supported by imagick :'(
251 $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
252
253 //Add longitude
254 //XXX: not supported by imagick :'(
255 $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
256
257 //Add description
258 //XXX: not supported by imagick :'(
259 #$image->setImageProperty('exif:Description', $caption);
260
261 //Set progressive jpeg
262 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
263
264 //Set compression quality
265 //TODO: ajust that
266 $image->setImageCompressionQuality(70);
267
268 //Save image
269 if (!$image->writeImage($map)) {
270 //Throw error
271 throw new \Exception(sprintf('Unable to write image "%s"', $path));
272 }
273
274 //Set mtime
275 $mtime = stat($map)['mtime'];
276 }
277
278 //Read map from cache
279 $response = new BinaryFileResponse($map);
280
281 //Set file name
282 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'map-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
283
284 //Set etag
285 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
286
287 //Set last modified
288 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
289
290 //Disable robot index
291 $response->headers->set('X-Robots-Tag', 'noindex');
292
293 //Set as public
294 $response->setPublic();
295
296 //Return 304 response if not modified
297 $response->isNotModified($request);
298
299 //Return response
300 return $response;
301 }
302
303 /**
304 * Return multi map image
305 *
306 * @param Request $request The Request instance
307 * @param string $hash The hash
308 * @param int $updated The updated timestamp
309 * @param float $latitude The latitude
310 * @param float $longitude The longitude
311 * @param string $coordinates The coordinates
312 * @param int $zoom The zoom
313 * @param int $width The width
314 * @param int $height The height
315 * @return Response The rendered image
316 */
317 public function multiMap(Request $request, string $hash, int $updated, float $latitude, float $longitude, string $coordinates, int $zoom, int $width, int $height): Response {
318 //Without matching hash
319 if ($hash !== $this->slugger->hash([$updated, $latitude, $longitude, $coordinate = $this->slugger->hash($coordinates), $zoom, $width, $height])) {
320 //Throw new exception
321 throw new NotFoundHttpException(sprintf('Unable to match multi map hash: %s', $hash));
322 }
323
324 //Set multi
325 $map = $this->path.'/'.$zoom.'/'.$latitude.'/'.$longitude.'/'.$coordinate.'/'.$width.'x'.$height.'.jpeg';
326
327 //Without multi up to date file
328 if (!is_file($map) || !($mtime = stat($map)['mtime']) || $mtime < $updated) {
329 //Without existing multi path
330 if (!is_dir($dir = dirname($map))) {
331 //Create filesystem object
332 $filesystem = new Filesystem();
333
334 try {
335 //Create path
336 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
337 //XXX: on CoW filesystems execute a chattr +C before filling
338 $filesystem->mkdir($dir, 0775);
339 } catch (IOExceptionInterface $e) {
340 //Throw error
341 throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
342 }
343 }
344
345 //Create image instance
346 $image = new \Imagick();
347
348 //Add new image
349 $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
350
351 //Create tile instance
352 $tile = new \Imagick();
353
354 //Get tile xy
355 $centerX = $this->map->longitudeToX($longitude, $zoom);
356 $centerY = $this->map->latitudeToY($latitude, $zoom);
357
358 //Calculate start xy
359 $startX = floor(floor($centerX) - $width / MapUtil::tz);
360 $startY = floor(floor($centerY) - $height / MapUtil::tz);
361
362 //Calculate end xy
363 $endX = ceil(ceil($centerX) + $width / MapUtil::tz);
364 $endY = ceil(ceil($centerY) + $height / MapUtil::tz);
365
366 for($x = $startX; $x <= $endX; $x++) {
367 for($y = $startY; $y <= $endY; $y++) {
368 //Set cache path
369 $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png';
370
371 //Without cache image
372 if (!is_file($cache)) {
373 //Set tile url
374 $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->url);
375
376 //Without cache path
377 if (!is_dir($dir = dirname($cache))) {
378 //Create filesystem object
379 $filesystem = new Filesystem();
380
381 try {
382 //Create path
383 //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
384 $filesystem->mkdir($dir, 0775);
385 } catch (IOExceptionInterface $e) {
386 //Throw error
387 throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
388 }
389 }
390
391 //Store tile in cache
392 file_put_contents($cache, file_get_contents($tileUri, false, $this->ctx));
393 }
394
395 //Set dest x
396 $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $x)));
397
398 //Set dest y
399 $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $y)));
400
401 //Read tile from cache
402 $tile->readImage($cache);
403
404 //Compose image
405 $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
406
407 //Clear tile
408 $tile->clear();
409 }
410 }
411
412 //Add imagick draw instance
413 //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
414 $draw = new \ImagickDraw();
415
416 //Set text antialias
417 $draw->setTextAntialias(true);
418
419 //Set stroke antialias
420 $draw->setStrokeAntialias(true);
421
422 //Set text alignment
423 $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
424
425 //Set gravity
426 $draw->setGravity(\Imagick::GRAVITY_CENTER);
427
428 //Convert to array
429 $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);
430
431 //Iterate on locations
432 foreach($coordinates as $id => $coordinate) {
433 //Set dest x
434 $destX = intval(floor($width / 2 - MapUtil::tz * ($centerX - $this->map->longitudeToX(floatval($coordinate['longitude']), $zoom))));
435
436 //Set dest y
437 $destY = intval(floor($height / 2 - MapUtil::tz * ($centerY - $this->map->latitudeToY(floatval($coordinate['latitude']), $zoom))));
438
439 //Set fill color
440 $draw->setFillColor($this->map->fill);
441
442 //Set font size
443 $draw->setFontSize($this->map->fontSize);
444
445 //Set stroke color
446 $draw->setStrokeColor($this->map->stroke);
447
448 //Set circle radius
449 $radius = $this->map->radius;
450
451 //Set stroke width
452 $stroke = $this->map->strokeWidth;
453
454 //With matching position
455 if ($coordinate['latitude'] === $latitude && $coordinate['longitude'] == $longitude) {
456 //Set fill color
457 $draw->setFillColor($this->map->highFill);
458
459 //Set font size
460 $draw->setFontSize($this->map->highFontSize);
461
462 //Set stroke color
463 $draw->setStrokeColor($this->map->highStroke);
464
465 //Set circle radius
466 $radius = $this->map->highRadius;
467
468 //Set stroke width
469 $stroke = $this->map->highStrokeWidth;
470 }
471
472 //Set stroke width
473 $draw->setStrokeWidth($stroke);
474
475 //Draw circle
476 $draw->circle($destX - $radius, $destY - $radius, $destX + $radius, $destY + $radius);
477
478 //Set fill color
479 $draw->setFillColor($draw->getStrokeColor());
480
481 //Set stroke width
482 $draw->setStrokeWidth($stroke / 4);
483
484 //Get font metrics
485 #$metrics = $image->queryFontMetrics($draw, strval($id));
486
487 //Add annotation
488 $draw->annotation($destX - $radius, $destY + $stroke, strval($id));
489 }
490
491 //Draw on image
492 $image->drawImage($draw);
493
494 //Strip image exif data and properties
495 $image->stripImage();
496
497 //Add latitude
498 //XXX: not supported by imagick :'(
499 $image->setImageProperty('exif:GPSLatitude', $this->map->latitudeToSexagesimal($latitude));
500
501 //Add longitude
502 //XXX: not supported by imagick :'(
503 $image->setImageProperty('exif:GPSLongitude', $this->map->longitudeToSexagesimal($longitude));
504
505 //Add description
506 //XXX: not supported by imagick :'(
507 #$image->setImageProperty('exif:Description', $caption);
508
509 //Set progressive jpeg
510 $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
511
512 //Set compression quality
513 //TODO: ajust that
514 $image->setImageCompressionQuality(70);
515
516 //Save image
517 if (!$image->writeImage($map)) {
518 //Throw error
519 throw new \Exception(sprintf('Unable to write image "%s"', $path));
520 }
521
522 //Set mtime
523 $mtime = stat($map)['mtime'];
524 }
525
526 //Read map from cache
527 $response = new BinaryFileResponse($map);
528
529 //Set file name
530 $response->setContentDisposition(HeaderUtils::DISPOSITION_INLINE, 'multimap-'.$latitude.','.$longitude.'-'.$zoom.'-'.$width.'x'.$height.'.jpeg');
531
532 //Set etag
533 $response->setEtag(md5(serialize([$updated, $latitude, $longitude, $zoom, $width, $height])));
534
535 //Set last modified
536 $response->setLastModified(\DateTime::createFromFormat('U', strval($mtime)));
537
538 //Disable robot index
539 $response->headers->set('X-Robots-Tag', 'noindex');
540
541 //Set as public
542 $response->setPublic();
543
544 //Return 304 response if not modified
545 $response->isNotModified($request);
546
547 //Return response
548 return $response;
549 }
550 }