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