--- /dev/null
+<?php declare(strict_types=1);
+
+/*
+ * This file is part of the Rapsys PackBundle package.
+ *
+ * (c) Raphaël Gertz <symfony@rapsys.eu>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Rapsys\PackBundle\Util;
+
+use Symfony\Component\Routing\RouterInterface;
+
+/**
+ * Helps manage map
+ */
+class MapUtil {
+ /**
+ * The cycle tile server
+ *
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
+ */
+ const cycle = 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png';
+
+ /**
+ * The fill color
+ */
+ const fill = '#cff';
+
+ /**
+ * The font size
+ */
+ const fontSize = 20;
+
+ /**
+ * The high fill color
+ */
+ const highFill = '#c3c3f9';
+
+ /**
+ * The high font size
+ */
+ const highFontSize = 30;
+
+ /**
+ * The high radius size
+ */
+ const highRadius = 6;
+
+ /**
+ * The high stroke color
+ */
+ const highStroke = '#3333c3';
+
+ /**
+ * The high stroke size
+ */
+ const highStrokeWidth = 4;
+
+ /**
+ * The map length
+ */
+ const length = 640;
+
+ /**
+ * The osm tile server
+ *
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
+ */
+ const osm = 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png';
+
+ /**
+ * The radius size
+ */
+ const radius = 5;
+
+ /**
+ * The stroke color
+ */
+ const stroke = '#00c3f9';
+
+ /**
+ * The stroke size
+ */
+ const strokeWidth = 2;
+
+ /**
+ * The transport tile server
+ *
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
+ */
+ const transport = 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png';
+
+ /**
+ * The tile size
+ */
+ const tz = 256;
+
+ /**
+ * The map zoom
+ */
+ const zoom = 17;
+
+ /**
+ * The RouterInterface instance
+ */
+ protected RouterInterface $router;
+
+ /**
+ * The SluggerUtil instance
+ */
+ protected SluggerUtil $slugger;
+
+ /**
+ * The fill color
+ */
+ public string $fill;
+
+ /**
+ * The font size
+ */
+ public int $fontSize;
+
+ /**
+ * The high fill color
+ */
+ public string $highFill;
+
+ /**
+ * The font size
+ */
+ public int $highFontSize;
+
+ /**
+ * The radius size
+ */
+ public int $highRadius;
+
+ /**
+ * The high stroke color
+ */
+ public string $highStroke;
+
+ /**
+ * The stroke size
+ */
+ public int $highStrokeWidth;
+
+ /**
+ * The stroke color
+ */
+ public string $stroke;
+
+ /**
+ * The stroke size
+ */
+ public int $strokeWidth;
+
+ /**
+ * The radius size
+ */
+ public int $radius;
+
+ /**
+ * Creates a new map util
+ *
+ * @param RouterInterface $router The RouterInterface instance
+ * @param SluggerUtil $slugger The SluggerUtil instance
+ */
+ function __construct(RouterInterface $router, SluggerUtil $slugger, string $fill = self::fill, int $fontSize = self::fontSize, string $highFill = self::highFill, int $highFontSize = self::highFontSize, int $highRadius = self::highRadius, string $highStroke = self::highStroke, int $highStrokeWidth = self::highStrokeWidth, int $radius = self::radius, string $stroke = self::stroke, int $strokeWidth = self::strokeWidth) {
+ //Set router
+ $this->router = $router;
+
+ //Set slugger
+ $this->slugger = $slugger;
+
+ //Set fill
+ $this->fill = $fill;
+
+ //Set font size
+ $this->fontSize = $fontSize;
+
+ //Set highFill
+ $this->highFill = $highFill;
+
+ //Set high font size
+ $this->highFontSize = $highFontSize;
+
+ //Set high radius size
+ $this->highRadius = $highRadius;
+
+ //Set highStroke
+ $this->highStroke = $highStroke;
+
+ //Set high stroke size
+ $this->highStrokeWidth = $highStrokeWidth;
+
+ //Set radius size
+ $this->radius = $radius;
+
+ //Set stroke
+ $this->stroke = $stroke;
+
+ //Set stroke size
+ $this->strokeWidth = $strokeWidth;
+ }
+
+ /**
+ * Return map url
+ *
+ * @param string $caption The caption
+ * @param int $updated The updated timestamp
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param int $zoom The zoom
+ * @param int $width The width
+ * @param int $height The height
+ * @return int The zoom
+ */
+ public function mapUrl(string $caption, int $updated, float $latitude, float $longitude, int $zoom = self::zoom, int $width = self::length, int $height = self::length): array {
+ //Set link hash
+ $link = $this->slugger->serialize([$updated, $latitude, $longitude, $zoom + 1, $width * 2, $height * 2]);
+
+ //Set src hash
+ $src = $this->slugger->serialize([$updated, $latitude, $longitude, $zoom, $width, $height]);
+
+ //Return array
+ return [
+ 'caption' => $caption,
+ 'link' => $this->router->generate('rapsys_pack_map', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
+ 'src' => $this->router->generate('rapsys_pack_map', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
+ 'width' => $width,
+ 'height' => $height
+ ];
+ }
+
+ /**
+ * Return multi map url
+ *
+ * @param string $caption The caption
+ * @param int $updated The updated timestamp
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param array $coordinates The coordinates array
+ * @param int $zoom The zoom
+ * @param int $width The width
+ * @param int $height The height
+ * @return int The zoom
+ */
+ public function multiMapUrl(string $caption, int $updated, float $latitude, float $longitude, $coordinates = [], int $zoom = self::zoom, int $width = self::length, int $height = self::length): array {
+ //Set coordinate
+ $coordinate = implode('-', array_map(function ($v) { return $v['latitude'].','.$v['longitude']; }, $coordinates));
+
+ //Set coordinate hash
+ $hash = $this->slugger->hash($coordinate);
+
+ //Set link hash
+ $link = $this->slugger->serialize([$updated, $latitude, $longitude, $hash, $zoom + 1, $width * 2, $height * 2]);
+
+ //Set src hash
+ $src = $this->slugger->serialize([$updated, $latitude, $longitude, $hash, $zoom, $width, $height]);
+
+ //Return array
+ return [
+ 'caption' => $caption,
+ 'link' => $this->router->generate('rapsys_pack_multimap', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]),
+ 'src' => $this->router->generate('rapsys_pack_multimap', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom, 'width' => $width, 'height' => $height]),
+ 'width' => $width,
+ 'height' => $height
+ ];
+ }
+
+ /**
+ * Return multi map zoom
+ *
+ * Compute a zoom to have all coordinates on multi map
+ * Multi map visible only from -($width / 2) until ($width / 2) and from -($height / 2) until ($height / 2)
+ *
+ * @see Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / self::tz)
+ *
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param array $coordinates The coordinates array
+ * @param int $zoom The zoom
+ * @param int $width The width
+ * @param int $height The height
+ * @return int The zoom
+ */
+ public function multiMapZoom(float $latitude, float $longitude, array $coordinates = [], int $zoom = self::zoom, int $width = self::length, int $height = self::length): int {
+ //Iterate on each zoom
+ for ($i = $zoom; $i >= 1; $i--) {
+ //Get tile xy
+ $centerX = self::longitudeToX($longitude, $i);
+ $centerY = self::latitudeToY($latitude, $i);
+
+ //Calculate start xy
+ $startX = floor($centerX - $width / 2 / self::tz);
+ $startY = floor($centerY - $height / 2 / self::tz);
+
+ //Calculate end xy
+ $endX = ceil($centerX + $width / 2 / self::tz);
+ $endY = ceil($centerY + $height / 2 / self::tz);
+
+ //Iterate on each coordinates
+ foreach($coordinates as $k => $coordinate) {
+ //Set dest x
+ $destX = self::longitudeToX($coordinate['longitude'], $i);
+
+ //With outside point
+ if ($startX >= $destX || $endX <= $destX) {
+ //Skip zoom
+ continue(2);
+ }
+
+ //Set dest y
+ $destY = self::latitudeToY($coordinate['latitude'], $i);
+
+ //With outside point
+ if ($startY >= $destY || $endY <= $destY) {
+ //Skip zoom
+ continue(2);
+ }
+ }
+
+ //Found zoom
+ break;
+ }
+
+ //Return zoom
+ return $i;
+ }
+
+ /**
+ * Convert longitude to tile x number
+ *
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
+ *
+ * @param float $longitude The longitude
+ * @param int $zoom The zoom
+ *
+ * @return float The tile x
+ */
+ public static function longitudeToX(float $longitude, int $zoom): float {
+ return (($longitude + 180) / 360) * pow(2, $zoom);
+ }
+
+ /**
+ * Convert latitude to tile y number
+ *
+ * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
+ *
+ * @param $latitude The latitude
+ * @param $zoom The zoom
+ *
+ * @return float The tile y
+ */
+ public static function latitudeToY(float $latitude, int $zoom): float {
+ return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
+ }
+
+ /**
+ * Convert tile x to longitude
+ *
+ * @param float $x The tile x
+ * @param int $zoom The zoom
+ *
+ * @return float The longitude
+ */
+ public static function xToLongitude(float $x, int $zoom): float {
+ return $x / pow(2, $zoom) * 360.0 - 180.0;
+ }
+
+ /**
+ * Convert tile y to latitude
+ *
+ * @param float $y The tile y
+ * @param int $zoom The zoom
+ *
+ * @return float The latitude
+ */
+ public static function yToLatitude(float $y, int $zoom): float {
+ return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
+ }
+
+ /**
+ * Convert decimal latitude to sexagesimal
+ *
+ * @param float $latitude The decimal latitude
+ *
+ * @return string The sexagesimal longitude
+ */
+ public static function latitudeToSexagesimal(float $latitude): string {
+ //Set degree
+ $degree = $latitude % 60;
+
+ //Set minute
+ $minute = ($latitude - $degree) * 60 % 60;
+
+ //Set second
+ $second = ($latitude - $degree - $minute / 60) * 3600 % 3600;
+
+ //Return sexagesimal longitude
+ return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
+ }
+
+ /**
+ * Convert decimal longitude to sexagesimal
+ *
+ * @param float $longitude The decimal longitude
+ *
+ * @return string The sexagesimal longitude
+ */
+ public static function longitudeToSexagesimal(float $longitude): string {
+ //Set degree
+ $degree = $longitude % 60;
+
+ //Set minute
+ $minute = ($longitude - $degree) * 60 % 60;
+
+ //Set second
+ $second = ($longitude - $degree - $minute / 60) * 3600 % 3600;
+
+ //Return sexagesimal longitude
+ return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
+ }
+}
+++ /dev/null
-<?php declare(strict_types=1);
-
-/*
- * This file is part of the Rapsys PackBundle package.
- *
- * (c) Raphaël Gertz <symfony@rapsys.eu>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Rapsys\PackBundle\Util;
-
-use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
-use Symfony\Component\Filesystem\Filesystem;
-
-/**
- * Helps manage osm images
- */
-class OsmUtil {
- /**
- * The tile size
- */
- const tz = 256;
-
- /**
- * The cache directory
- */
- protected $cache;
-
- /**
- * The public path
- */
- protected $path;
-
- /**
- * The tile server
- */
- protected $server;
-
- /**
- * The tile servers
- *
- * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Tile_servers
- */
- protected $servers = [
- 'osm' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png',
- 'cycle' => 'http://a.tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png',
- 'transport' => 'http://a.tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
- ];
-
- /**
- * The public url
- */
- protected $url;
-
- /**
- * Creates a new osm util
- *
- * @param string $cache The cache directory
- * @param string $path The public path
- * @param string $url The public url
- * @param string $server The server key
- */
- function __construct(string $cache, string $path, string $url, string $server = 'osm') {
- //Set cache
- $this->cache = $cache.'/'.$server;
-
- //Set path
- $this->path = $path.'/'.$server;
-
- //Set url
- $this->url = $url.'/'.$server;
-
- //Set server key
- $this->server = $server;
- }
-
- /**
- * Return the simple image
- *
- * Generate simple image in jpeg format or load it from cache
- *
- * @param string $pathInfo The path info
- * @param string $caption The image caption
- * @param int $updated The updated timestamp
- * @param float $latitude The latitude
- * @param float $longitude The longitude
- * @param int $zoom The zoom
- * @param int $width The width
- * @param int $height The height
- * @return array The image array
- */
- #TODO: rename to getSimple ???
- public function getImage(string $pathInfo, string $caption, int $updated, float $latitude, float $longitude, int $zoom = 18, int $width = 1280, int $height = 1280): array {
- //Set path file
- $path = $this->path.$pathInfo.'.jpeg';
-
- //Set min file
- $min = $this->path.$pathInfo.'.min.jpeg';
-
- //Without existing path
- if (!is_dir($dir = dirname($path))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //With path and min up to date file
- if (is_file($path) && is_file($min) && ($mtime = stat($path)['mtime']) && ($mintime = stat($min)['mtime']) && $mtime >= $updated && $mintime >= $updated) {
- //Return image data
- return [
- 'link' => $this->url.'/'.$mtime.$pathInfo.'.jpeg',
- 'min' => $this->url.'/'.$mintime.$pathInfo.'.min.jpeg',
- 'caption' => $caption,
- 'height' => $height / 2,
- 'width' => $width / 2
- ];
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Add new image
- $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
- //Create tile instance
- $tile = new \Imagick();
-
- //Init context
- $ctx = stream_context_create(
- [
- 'http' => [
- #'header' => ['Referer: https://www.openstreetmap.org/'],
- 'max_redirects' => 5,
- 'timeout' => (int)ini_get('default_socket_timeout'),
- #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
- 'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0',
- ]
- ]
- );
-
- //Get tile xy
- $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom));
- $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom));
-
- //Calculate start xy
- $startX = floor(($tileX * self::tz - $width) / self::tz);
- $startY = floor(($tileY * self::tz - $height) / self::tz);
-
- //Calculate end xy
- $endX = ceil(($tileX * self::tz + $width) / self::tz);
- $endY = ceil(($tileY * self::tz + $height) / self::tz);
-
- for($x = $startX; $x <= $endX; $x++) {
- for($y = $startY; $y <= $endY; $y++) {
- //Set cache path
- $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
- //Without cache path
- if (!is_dir($dir = dirname($cache))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Without cache image
- if (!is_file($cache)) {
- //Set tile url
- $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers[$this->server]);
-
- //Store tile in cache
- file_put_contents($cache, file_get_contents($tileUri, false, $ctx));
- }
-
- //Set dest x
- $destX = intval(floor(($width / 2) - self::tz * ($centerX - $x)));
-
- //Set dest y
- $destY = intval(floor(($height / 2) - self::tz * ($centerY - $y)));
-
- //Read tile from cache
- $tile->readImage($cache);
-
- //Compose image
- $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
- //Clear tile
- $tile->clear();
- }
- }
-
- //Add circle
- //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
- $draw = new \ImagickDraw();
-
- //Set text antialias
- $draw->setTextAntialias(true);
-
- //Set stroke antialias
- $draw->setStrokeAntialias(true);
-
- //Set fill color
- $draw->setFillColor('#c3c3f9');
-
- //Set stroke color
- $draw->setStrokeColor('#3333c3');
-
- //Set stroke width
- $draw->setStrokeWidth(2);
-
- //Draw circle
- $draw->circle($width/2, $height/2 - 5, $width/2 + 10, $height/2 + 5);
-
- //Draw on image
- $image->drawImage($draw);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Add latitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude));
-
- //Add longitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude));
-
- //Add description
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:Description', $caption);
-
- //Set progressive jpeg
- $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
- //Save image
- if (!$image->writeImage($path)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $path));
- }
-
- //Crop using aspect ratio
- $image->cropThumbnailImage($width / 2, $height / 2);
-
- //Set compression quality
- $image->setImageCompressionQuality(70);
-
- //Save min image
- if (!$image->writeImage($min)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $min));
- }
-
- //Return image data
- return [
- 'link' => $this->url.'/'.stat($path)['mtime'].$pathInfo.'.jpeg',
- 'min' => $this->url.'/'.stat($min)['mtime'].$pathInfo.'.min.jpeg',
- 'caption' => $caption,
- 'height' => $height / 2,
- 'width' => $width / 2
- ];
- }
-
- /**
- * Return the multi image
- *
- * Generate multi image in jpeg format or load it from cache
- *
- * @param string $pathInfo The path info
- * @param string $caption The image caption
- * @param int $updated The updated timestamp
- * @param float $latitude The latitude
- * @param float $longitude The longitude
- * @param array $locations The latitude array
- * @param int $zoom The zoom
- * @param int $width The width
- * @param int $height The height
- * @return array The image array
- */
- public function getMultiImage(string $pathInfo, string $caption, int $updated, float $latitude, float $longitude, array $locations, int $zoom = 18, int $width = 1280, int $height = 1280): array {
- //Set path file
- $path = $this->path.$pathInfo.'.jpeg';
-
- //Set min file
- $min = $this->path.$pathInfo.'.min.jpeg';
-
- //Without existing path
- if (!is_dir($dir = dirname($path))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output path "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //With path and min up to date file
- if (is_file($path) && is_file($min) && ($mtime = stat($path)['mtime']) && ($mintime = stat($min)['mtime']) && $mtime >= $updated && $mintime >= $updated) {
- //Return image data
- return [
- 'link' => $this->url.'/'.$mtime.$pathInfo.'.jpeg',
- 'min' => $this->url.'/'.$mintime.$pathInfo.'.min.jpeg',
- 'caption' => $caption,
- 'height' => $height / 2,
- 'width' => $width / 2
- ];
- }
-
- //Create image instance
- $image = new \Imagick();
-
- //Add new image
- $image->newImage($width, $height, new \ImagickPixel('transparent'), 'jpeg');
-
- //Create tile instance
- $tile = new \Imagick();
-
- //Init context
- $ctx = stream_context_create(
- [
- 'http' => [
- #'header' => ['Referer: https://www.openstreetmap.org/'],
- 'max_redirects' => 5,
- 'timeout' => (int)ini_get('default_socket_timeout'),
- #'user_agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36',
- 'user_agent' => (string)ini_get('user_agent')?:'rapsys_air/2.0.0',
- ]
- ]
- );
-
- //Get tile xy
- $tileX = floor($centerX = $this->longitudeToX($longitude, $zoom));
- $tileY = floor($centerY = $this->latitudeToY($latitude, $zoom));
-
- //Calculate start xy
- //XXX: we draw every tile starting beween -($width / 2) and 0
- $startX = floor(($tileX * self::tz - $width) / self::tz);
- //XXX: we draw every tile starting beween -($height / 2) and 0
- $startY = floor(($tileY * self::tz - $height) / self::tz);
-
- //Calculate end xy
- //TODO: this seems stupid, check if we may just divide $width / 2 here !!!
- //XXX: we draw every tile starting beween $width + ($width / 2)
- $endX = ceil(($tileX * self::tz + $width) / self::tz);
- //XXX: we draw every tile starting beween $width + ($width / 2)
- $endY = ceil(($tileY * self::tz + $height) / self::tz);
-
- for($x = $startX; $x <= $endX; $x++) {
- for($y = $startY; $y <= $endY; $y++) {
- //Set cache path
- $cache = $this->cache.'/'.$zoom.'/'.$x.'/'.$y.'.png';
-
- //Without cache path
- if (!is_dir($dir = dirname($cache))) {
- //Create filesystem object
- $filesystem = new Filesystem();
-
- try {
- //Create path
- //XXX: set as 0775, symfony umask (0022) will reduce rights (0755)
- $filesystem->mkdir($dir, 0775);
- } catch (IOExceptionInterface $e) {
- //Throw error
- throw new \Exception(sprintf('Output directory "%s" do not exists and unable to create it', $dir), 0, $e);
- }
- }
-
- //Without cache image
- if (!is_file($cache)) {
- //Set tile url
- $tileUri = str_replace(['{Z}', '{X}', '{Y}'], [$zoom, $x, $y], $this->servers[$this->server]);
-
- //Store tile in cache
- file_put_contents($cache, file_get_contents($tileUri, false, $ctx));
- }
-
- //Set dest x
- $destX = intval(floor(($width / 2) - self::tz * ($centerX - $x)));
-
- //Set dest y
- $destY = intval(floor(($height / 2) - self::tz * ($centerY - $y)));
-
- //Read tile from cache
- $tile->readImage($cache);
-
- //Compose image
- $image->compositeImage($tile, \Imagick::COMPOSITE_OVER, $destX, $destY);
-
- //Clear tile
- $tile->clear();
- }
- }
-
- //Add circle
- //XXX: see https://www.php.net/manual/fr/imagick.examples-1.php#example-3916
- $draw = new \ImagickDraw();
-
- //Set text alignment
- $draw->setTextAlignment(\Imagick::ALIGN_CENTER);
-
- //Set text antialias
- $draw->setTextAntialias(true);
-
- //Set stroke antialias
- $draw->setStrokeAntialias(true);
-
- //Iterate on locations
- foreach($locations as $k => $location) {
- //Set dest x
- $destX = intval(floor(($width / 2) - self::tz * ($centerX - $this->longitudeToX($location['longitude'], $zoom))));
-
- //Set dest y
- $destY = intval(floor(($height / 2) - self::tz * ($centerY - $this->latitudeToY($location['latitude'], $zoom))));
-
- //Set fill color
- $draw->setFillColor('#cff');
-
- //Set font size
- $draw->setFontSize(20);
-
- //Set stroke color
- $draw->setStrokeColor('#00c3f9');
-
- //Set circle radius
- $radius = 5;
-
- //Set stroke
- $stroke = 2;
-
- //With matching position
- if ($location['latitude'] === $latitude && $location['longitude'] == $longitude) {
- //Set fill color
- $draw->setFillColor('#c3c3f9');
-
- //Set font size
- $draw->setFontSize(30);
-
- //Set stroke color
- $draw->setStrokeColor('#3333c3');
-
- //Set circle radius
- $radius = 8;
-
- //Set stroke
- $stroke = 4;
- }
-
- //Set stroke width
- $draw->setStrokeWidth($stroke);
-
- //Draw circle
- $draw->circle($destX, $destY - $radius, $destX + $radius * 2, $destY + $radius);
-
- //Set fill color
- $draw->setFillColor($draw->getStrokeColor());
-
- //Set stroke width
- $draw->setStrokeWidth($stroke / 4);
-
- //Get font metrics
- $metrics = $image->queryFontMetrics($draw, strval($location['id']));
-
- //Add annotation
- $draw->annotation($destX, $destY - $metrics['descender'] / 3, strval($location['id']));
- }
-
- //Draw on image
- $image->drawImage($draw);
-
- //Strip image exif data and properties
- $image->stripImage();
-
- //Add latitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLatitude', $this->latitudeToSexagesimal($latitude));
-
- //Add longitude
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:GPSLongitude', $this->longitudeToSexagesimal($longitude));
-
- //Add description
- //XXX: not supported by imagick :'(
- $image->setImageProperty('exif:Description', $caption);
-
- //Set progressive jpeg
- $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
-
- //Save image
- if (!$image->writeImage($path)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $path));
- }
-
- //Crop using aspect ratio
- $image->cropThumbnailImage($width / 2, $height / 2);
-
- //Set compression quality
- $image->setImageCompressionQuality(70);
-
- //Save min image
- if (!$image->writeImage($min)) {
- //Throw error
- throw new \Exception(sprintf('Unable to write image "%s"', $min));
- }
-
- //Return image data
- return [
- 'link' => $this->url.'/'.stat($path)['mtime'].$pathInfo.'.jpeg',
- 'min' => $this->url.'/'.stat($min)['mtime'].$pathInfo.'.min.jpeg',
- 'caption' => $caption,
- 'height' => $height / 2,
- 'width' => $width / 2
- ];
- }
-
- /**
- * Return multi zoom
- *
- * Compute multi image optimal zoom
- *
- * @param float $latitude The latitude
- * @param float $longitude The longitude
- * @param array $locations The latitude array
- * @param int $zoom The zoom
- * @param int $width The width
- * @param int $height The height
- * @return int The zoom
- */
- public function getMultiZoom(float $latitude, float $longitude, array $locations, int $zoom = 18, int $width = 1280, int $height = 1280): int {
- //Iterate on each zoom
- for ($i = $zoom; $i >= 1; $i--) {
- //Get tile xy
- $tileX = floor($this->longitudeToX($longitude, $i));
- $tileY = floor($this->latitudeToY($latitude, $i));
-
- //Calculate start xy
- $startX = floor(($tileX * self::tz - $width / 2) / self::tz);
- $startY = floor(($tileY * self::tz - $height / 2) / self::tz);
-
- //Calculate end xy
- $endX = ceil(($tileX * self::tz + $width / 2) / self::tz);
- $endY = ceil(($tileY * self::tz + $height / 2) / self::tz);
-
- //Iterate on each locations
- foreach($locations as $k => $location) {
- //Set dest x
- $destX = floor($this->longitudeToX($location['longitude'], $i));
-
- //With outside point
- if ($startX >= $destX || $endX <= $destX) {
- //Skip zoom
- continue(2);
- }
-
- //Set dest y
- $destY = floor($this->latitudeToY($location['latitude'], $i));
-
- //With outside point
- if ($startY >= $destY || $endY <= $destY) {
- //Skip zoom
- continue(2);
- }
- }
-
- //Found zoom
- break;
- }
-
- //Return zoom
- return $i;
- }
-
- /**
- * Convert longitude to tile x number
- *
- * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
- *
- * @param float $longitude The longitude
- * @param int $zoom The zoom
- *
- * @return float The tile x
- */
- public function longitudeToX(float $longitude, int $zoom): float {
- return (($longitude + 180) / 360) * pow(2, $zoom);
- }
-
- /**
- * Convert latitude to tile y number
- *
- * @see https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Lon..2Flat._to_tile_numbers_5
- *
- * @param $latitude The latitude
- * @param $zoom The zoom
- *
- * @return float The tile y
- */
- public function latitudeToY(float $latitude, int $zoom): float {
- return (1 - log(tan(deg2rad($latitude)) + 1 / cos(deg2rad($latitude))) / pi()) / 2 * pow(2, $zoom);
- }
-
- /**
- * Convert tile x to longitude
- *
- * @param float $x The tile x
- * @param int $zoom The zoom
- *
- * @return float The longitude
- */
- public function xToLongitude(float $x, int $zoom): float {
- return $x / pow(2, $zoom) * 360.0 - 180.0;
- }
-
- /**
- * Convert tile y to latitude
- *
- * @param float $y The tile y
- * @param int $zoom The zoom
- *
- * @return float The latitude
- */
- public function yToLatitude(float $y, int $zoom): float {
- return rad2deg(atan(sinh(pi() * (1 - 2 * $y / pow(2, $zoom)))));
- }
-
- /**
- * Convert decimal latitude to sexagesimal
- *
- * @param float $latitude The decimal latitude
- *
- * @return string The sexagesimal longitude
- */
- public function latitudeToSexagesimal(float $latitude): string {
- //Set degree
- $degree = $latitude % 60;
-
- //Set minute
- $minute = ($latitude - $degree) * 60 % 60;
-
- //Set second
- $second = ($latitude - $degree - $minute / 60) * 3600 % 3600;
-
- //Return sexagesimal longitude
- return $degree.'°'.$minute.'\''.$second.'"'.($latitude >= 0 ? 'N' : 'S');
- }
-
- /**
- * Convert decimal longitude to sexagesimal
- *
- * @param float $longitude The decimal longitude
- *
- * @return string The sexagesimal longitude
- */
- public function longitudeToSexagesimal(float $longitude): string {
- //Set degree
- $degree = $longitude % 60;
-
- //Set minute
- $minute = ($longitude - $degree) * 60 % 60;
-
- //Set second
- $second = ($longitude - $degree - $minute / 60) * 3600 % 3600;
-
- //Return sexagesimal longitude
- return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
- }
-}