namespace Rapsys\PackBundle\Util;
+use Psr\Container\ContainerInterface;
+
+use Rapsys\PackBundle\RapsysPackBundle;
+
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
/**
- * Helps manage map
+ * Manages image
*/
class ImageUtil {
/**
- * The captcha width
+ * Alias string
*/
- const captchaWidth = 192;
+ protected string $alias;
/**
- * The captcha height
+ * Config array
*/
- const captchaHeight = 52;
+ protected array $config;
/**
- * The captcha background color
+ * Creates a new image util
+ *
+ * @param ContainerInterface $container The container instance
+ * @param RouterInterface $router The RouterInterface instance
+ * @param SluggerUtil $slugger The SluggerUtil instance
*/
- const captchaBackground = 'white';
+ public function __construct(protected ContainerInterface $container, protected RouterInterface $router, protected SluggerUtil $slugger) {
+ //Retrieve config
+ $this->config = $container->getParameter($this->alias = RapsysPackBundle::getAlias());
+ }
/**
- * The captcha fill color
+ * Get captcha data
+ *
+ * @param ?int $height The height
+ * @param ?int $width The width
+ * @return array The captcha data
*/
- const captchaFill = '#cff';
+ public function getCaptcha(?int $height = null, ?int $width = null): array {
+ //Without height
+ if ($height === null) {
+ //Set height from config
+ $height = $this->config['captcha']['height'];
+ }
- /**
- * The captcha font size
- */
- const captchaFontSize = 45;
+ //Without width
+ if ($width === null) {
+ //Set width from config
+ $width = $this->config['captcha']['width'];
+ }
- /**
- * The captcha stroke color
- */
- const captchaStroke = '#00c3f9';
+ //Get random
+ $random = rand(0, 999);
- /**
- * The captcha stroke width
- */
- const captchaStrokeWidth = 2;
+ //Set a
+ $a = $random % 10;
- /**
- * The thumb width
- */
- const thumbWidth = 640;
+ //Set b
+ $b = $random / 10 % 10;
- /**
- * The thumb height
- */
- const thumbHeight = 640;
+ //Set c
+ $c = $random / 100 % 10;
- /**
- * The cache path
- */
- protected string $cache;
+ //Set equation
+ $equation = $a.' * '.$b.' + '.$c;
- /**
- * The path
- */
- protected string $path;
+ //Short path
+ $short = $this->slugger->short($equation);
- /**
- * The RouterInterface instance
- */
- protected RouterInterface $router;
+ //Set hash
+ $hash = $this->slugger->serialize([$short, $height, $width]);
+
+ //Return array
+ return [
+ 'token' => $this->slugger->hash(strval($a * $b + $c)),
+ 'value' => strval($a * $b + $c),
+ 'equation' => str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation),
+ 'src' => $this->router->generate('rapsyspack_captcha', ['hash' => $hash, 'equation' => $short, 'height' => $height, 'width' => $width, '_format' => $this->config['captcha']['format']]),
+ 'width' => $width,
+ 'height' => $height
+ ];
+ }
/**
- * The SluggerUtil instance
+ * Return the facebook image
+ *
+ * Generate simple image in jpeg format or load it from cache
+ *
+ * @TODO: move to a svg merging system ?
+ *
+ * @param string $path The request path info
+ * @param array $texts The image texts
+ * @param int $updated The updated timestamp
+ * @param ?string $source The image source
+ * @param ?int $height The height
+ * @param ?int $width The width
+ * @return array The image array
*/
- protected SluggerUtil $slugger;
+ public function getFacebook(string $path, array $texts, int $updated, ?string $source = null, ?int $height = null, ?int $width = null): array {
+ //Without source
+ if ($source === null && $this->config['facebook']['source'] === null) {
+ //Return empty image data
+ return [];
+ //Without local source
+ } elseif ($source === null) {
+ //Set local source
+ $source = $this->config['facebook']['source'];
+ }
- //Captcha background
- protected string $captchaBackground;
+ //Without height
+ if ($height === null) {
+ //Set height from config
+ $height = $this->config['facebook']['height'];
+ }
- //Captcha fill
- protected string $captchaFill;
+ //Without width
+ if ($width === null) {
+ //Set width from config
+ $width = $this->config['facebook']['width'];
+ }
- //Captcha font size
- protected int $captchaFontSize;
+ //Set path file
+ $facebook = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.jpeg';
+
+ //Without existing path
+ if (!is_dir($dir = dirname($facebook))) {
+ //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);
+ }
+ }
- //Captcha stroke
- protected string $captchaStroke;
+ //With path file
+ if (is_file($facebook) && ($mtime = stat($facebook)['mtime']) && $mtime >= $updated) {
+ #XXX: we used to drop texts with $data['canonical'] === true !!!
- //Captcha stroke width
- protected int $captchaStrokeWidth;
+ //Set short path
+ $short = $this->slugger->short($path);
- /**
- * Creates a new image util
- *
- * @param RouterInterface $router The RouterInterface instance
- * @param SluggerUtil $slugger The SluggerUtil instance
- * @param string $cache The cache directory
- * @param string $path The public path
- * @param string $prefix The prefix
- */
- function __construct(RouterInterface $router, SluggerUtil $slugger, string $cache = '../var/cache', string $path = './bundles/rapsyspack', string $prefix = 'image', $captchaBackground = self::captchaBackground, $captchaFill = self::captchaFill, $captchaFontSize = self::captchaFontSize, $captchaStroke = self::captchaStroke, $captchaStrokeWidth = self::captchaStrokeWidth) {
- //Set cache
- $this->cache = $cache.'/'.$prefix;
+ //Set hash
+ $hash = $this->slugger->serialize([$short, $height, $width]);
+
+ //Return image data
+ return [
+ 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => $mtime, '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
+ 'og:image:height' => $height,
+ 'og:image:width' => $width
+ ];
+ }
+
+ //Set cache path
+ $cache = $this->config['cache'].'/'.$this->config['prefixes']['facebook'].$path.'.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);
+ }
+ }
+
+ //Create image object
+ $image = new \Imagick();
+
+ //Without cache image
+ if (!is_file($cache) || stat($cache)['mtime'] < stat($source)['mtime']) {
+ //Check target directory
+ if (!is_dir($dir = dirname($cache))) {
+ //Create filesystem object
+ $filesystem = new Filesystem();
+
+ try {
+ //Create dir
+ //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 source
+ if (!is_file($source)) {
+ //Throw error
+ throw new \Exception(sprintf('Source file "%s" do not exists', $source));
+ }
+
+ //Convert to absolute path
+ $source = realpath($source);
+
+ //Read image
+ //XXX: Imagick::readImage only supports absolute path
+ $image->readImage($source);
+
+ //Crop using aspect ratio
+ //XXX: for better result upload image directly in aspect ratio :)
+ $image->cropThumbnailImage($width, $height);
+
+ //Strip image exif data and properties
+ $image->stripImage();
+
+ //Save cache image
+ if (!$image->writeImage($cache)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $cache));
+ }
+ //With cache
+ } else {
+ //Read image
+ $image->readImage($cache);
+ }
+
+ //Create draw
+ $draw = new \ImagickDraw();
+
+ //Set stroke antialias
+ $draw->setStrokeAntialias(true);
+
+ //Set text antialias
+ $draw->setTextAntialias(true);
+
+ //Set align aliases
+ $aligns = [
+ 'left' => \Imagick::ALIGN_LEFT,
+ 'center' => \Imagick::ALIGN_CENTER,
+ 'right' => \Imagick::ALIGN_RIGHT
+ ];
+
+ //Init counter
+ $i = 1;
+
+ //Set text count
+ $count = count($texts);
+
+ //Draw each text stroke
+ foreach($texts as $text => $data) {
+ //Set font
+ $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]);
+
+ //Set font size
+ $draw->setFontSize($data['size']??$this->config['facebook']['size']);
+
+ //Set stroke width
+ $draw->setStrokeWidth($data['thickness']??$this->config['facebook']['thickness']);
+
+ //Set text alignment
+ $draw->setTextAlignment($align = ($aligns[$data['align']??$this->config['facebook']['align']]));
+
+ //Get font metrics
+ $metrics = $image->queryFontMetrics($draw, $text);
+
+ //Without y
+ if (empty($data['y'])) {
+ //Position verticaly each text evenly
+ $texts[$text]['y'] = $data['y'] = (($height + 100) / (count($texts) + 1) * $i) - 50;
+ }
+
+ //Without x
+ if (empty($data['x'])) {
+ if ($align == \Imagick::ALIGN_CENTER) {
+ $texts[$text]['x'] = $data['x'] = $width/2;
+ } elseif ($align == \Imagick::ALIGN_LEFT) {
+ $texts[$text]['x'] = $data['x'] = 50;
+ } elseif ($align == \Imagick::ALIGN_RIGHT) {
+ $texts[$text]['x'] = $data['x'] = $width - 50;
+ }
+ }
+
+ //Center verticaly
+ //XXX: add ascender part then center it back by half of textHeight
+ //TODO: maybe add a boundingbox ???
+ $texts[$text]['y'] = $data['y'] += $metrics['ascender'] - $metrics['textHeight']/2;
+
+ //Set stroke color
+ $draw->setStrokeColor(new \ImagickPixel($data['border']??$this->config['facebook']['border']));
+
+ //Set fill color
+ $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill']));
+
+ //Add annotation
+ $draw->annotation($data['x'], $data['y'], $text);
+
+ //Increase counter
+ $i++;
+ }
+
+ //Create stroke object
+ $stroke = new \Imagick();
+
+ //Add new image
+ $stroke->newImage($width, $height, new \ImagickPixel('transparent'));
+
+ //Draw on image
+ $stroke->drawImage($draw);
+
+ //Blur image
+ //XXX: blur the stroke canvas only
+ $stroke->blurImage(5,3);
+
+ //Set opacity to 0.5
+ //XXX: see https://www.php.net/manual/en/image.evaluateimage.php
+ $stroke->evaluateImage(\Imagick::EVALUATE_DIVIDE, 1.5, \Imagick::CHANNEL_ALPHA);
+
+ //Compose image
+ $image->compositeImage($stroke, \Imagick::COMPOSITE_OVER, 0, 0);
+
+ //Clear stroke
+ $stroke->clear();
+
+ //Destroy stroke
+ unset($stroke);
+
+ //Clear draw
+ $draw->clear();
+
+ //Set text antialias
+ $draw->setTextAntialias(true);
+
+ //Draw each text
+ foreach($texts as $text => $data) {
+ //Set font
+ $draw->setFont($this->config['fonts'][$data['font']??$this->config['facebook']['font']]);
+
+ //Set font size
+ $draw->setFontSize($data['size']??$this->config['facebook']['size']);
+
+ //Set text alignment
+ $draw->setTextAlignment($aligns[$data['align']??$this->config['facebook']['align']]);
+
+ //Set fill color
+ $draw->setFillColor(new \ImagickPixel($data['fill']??$this->config['facebook']['fill']));
+
+ //Add annotation
+ $draw->annotation($data['x'], $data['y'], $text);
+
+ //With canonical text
+ if (!empty($data['canonical'])) {
+ //Prevent canonical to finish in alt
+ unset($texts[$text]);
+ }
+ }
+
+ //Draw on image
+ $image->drawImage($draw);
+
+ //Strip image exif data and properties
+ $image->stripImage();
- //set captcha background
- $this->captchaBackground = $captchaBackground;
+ //Set image format
+ $image->setImageFormat('jpeg');
- //set captcha fill
- $this->captchaFill = $captchaFill;
+ //Set progressive jpeg
+ $image->setInterlaceScheme(\Imagick::INTERLACE_PLANE);
- //set captcha font size
- $this->captchaFontSize = $captchaFontSize;
+ //Save image
+ if (!$image->writeImage($facebook)) {
+ //Throw error
+ throw new \Exception(sprintf('Unable to write image "%s"', $facebook));
+ }
+
+ //Set short path
+ $short = $this->slugger->short($path);
- //set captcha stroke
- $this->captchaStroke = $captchaStroke;
+ //Set hash
+ $hash = $this->slugger->serialize([$short, $height, $width]);
+
+ //Return image data
+ return [
+ 'og:image' => $this->router->generate('rapsyspack_facebook', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width, 'u' => stat($facebook)['mtime'], '_format' => $this->config['facebook']['format']], UrlGeneratorInterface::ABSOLUTE_URL),
+ 'og:image:alt' => str_replace("\n", ' ', implode(' - ', array_keys($texts))),
+ 'og:image:height' => $height,
+ 'og:image:width' => $width
+ ];
+ }
- //set captcha stroke width
- $this->captchaStrokeWidth = $captchaStrokeWidth;
+ /**
+ * Get map data
+ *
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param ?int $height The height
+ * @param ?int $width The width
+ * @param ?int $zoom The zoom
+ * @return array The map data
+ */
+ public function getMap(float $latitude, float $longitude, ?int $height = null, ?int $width = null, ?int $zoom = null): array {
+ //Without height
+ if ($height === null) {
+ //Set height from config
+ $height = $this->config['map']['height'];
+ }
- //Set path
- $this->path = $path.'/'.$prefix;
+ //Without width
+ if ($width === null) {
+ //Set width from config
+ $width = $this->config['map']['width'];
+ }
- //Set router
- $this->router = $router;
+ //Without zoom
+ if ($zoom === null) {
+ //Set zoom from config
+ $zoom = $this->config['map']['zoom'];
+ }
- //Set slugger
- $this->slugger = $slugger;
+ //Set hash
+ $hash = $this->slugger->hash([$height, $width, $zoom, $latitude, $longitude]);
+
+ //Return array
+ return [
+ 'latitude' => $latitude,
+ 'longitude' => $longitude,
+ 'height' => $height,
+ 'src' => $this->router->generate('rapsyspack_map', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'latitude' => $latitude, 'longitude' => $longitude, '_format' => $this->config['map']['format']]),
+ 'width' => $width,
+ 'zoom' => $zoom
+ ];
}
/**
- * Get captcha data
+ * Get multi map data
*
- * @param int $updated The updated timestamp
- * @param int $width The width
- * @param int $height The height
- * @return array The captcha data
+ * @param array $coordinates The coordinates array
+ * @param ?int $height The height
+ * @param ?int $width The width
+ * @param ?int $zoom The zoom
+ * @return array The multi map data
*/
- public function getCaptcha(int $updated, int $width = self::captchaWidth, int $height = self::captchaHeight): array {
- //Set a
- $a = rand(0, 9);
+ public function getMulti(array $coordinates, ?int $height = null, ?int $width = null, ?int $zoom = null): array {
+ //Without coordinates
+ if ($coordinates === []) {
+ //Throw error
+ throw new \Exception('Missing coordinates');
+ }
- //Set b
- $b = rand(0, 5);
+ //Without height
+ if ($height === null) {
+ //Set height from config
+ $height = $this->config['multi']['height'];
+ }
- //Set c
- $c = rand(0, 9);
+ //Without width
+ if ($width === null) {
+ //Set width from config
+ $width = $this->config['multi']['width'];
+ }
- //Set equation
- $equation = $a.' * '.$b.' + '.$c;
+ //Without zoom
+ if ($zoom === null) {
+ //Set zoom from config
+ $zoom = $this->config['multi']['zoom'];
+ }
- //Short path
- $short = $this->slugger->short($equation);
+ //Initialize latitudes and longitudes arrays
+ $latitudes = $longitudes = [];
+
+ //Set coordinate
+ $coordinate = implode(
+ '-',
+ array_map(
+ function ($v) use (&$latitudes, &$longitudes) {
+ //Get latitude and longitude
+ list($latitude, $longitude) = $v;
+
+ //Append latitude
+ $latitudes[] = $latitude;
+
+ //Append longitude
+ $longitudes[] = $longitude;
+
+ //Append coordinate
+ return $latitude.','.$longitude;
+ },
+ $coordinates
+ )
+ );
+
+ //Set latitude
+ $latitude = round((min($latitudes)+max($latitudes))/2, 6);
+
+ //Set longitude
+ $longitude = round((min($longitudes)+max($longitudes))/2, 6);
+
+ //Set zoom
+ $zoom = $this->getZoom($latitude, $longitude, $coordinates, $height, $width, $zoom);
//Set hash
- $hash = $this->slugger->serialize([$updated, $short, $width, $height]);
+ $hash = $this->slugger->hash([$height, $width, $zoom, $coordinate]);
//Return array
return [
- 'token' => $this->slugger->hash(strval($a * $b + $c)),
- 'value' => strval($a * $b + $c),
- 'equation' => str_replace([' ', '*', '+'], ['-', 'mul', 'add'], $equation),
- 'src' => $this->router->generate('rapsys_pack_captcha', ['hash' => $hash, 'updated' => $updated, 'equation' => $short, 'width' => $width, 'height' => $height]),
+ 'coordinate' => $coordinate,
+ 'height' => $height,
+ 'src' => $this->router->generate('rapsyspack_multi', ['hash' => $hash, 'height' => $height, 'width' => $width, 'zoom' => $zoom, 'coordinate' => $coordinate, '_format' => $this->config['multi']['format']]),
'width' => $width,
- 'height' => $height
+ 'zoom' => $zoom
];
}
+ /**
+ * Get multi 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)
+ *
+ * @TODO Wether we need to take in consideration circle radius in coordinates comparisons, likely +/-(radius / $this->config['multi']['tz'])
+ *
+ * @param float $latitude The latitude
+ * @param float $longitude The longitude
+ * @param array $coordinates The coordinates array
+ * @param int $height The height
+ * @param int $width The width
+ * @param int $zoom The zoom
+ * @return int The zoom
+ */
+ public function getZoom(float $latitude, float $longitude, array $coordinates, int $height, int $width, int $zoom): int {
+ //Iterate on each zoom
+ for ($i = $zoom; $i >= 1; $i--) {
+ //Get tile xy
+ $centerX = $this->longitudeToX($longitude, $i);
+ $centerY = $this->latitudeToY($latitude, $i);
+
+ //Calculate start xy
+ $startX = floor($centerX - $width / 2 / $this->config['multi']['tz']);
+ $startY = floor($centerY - $height / 2 / $this->config['multi']['tz']);
+
+ //Calculate end xy
+ $endX = ceil($centerX + $width / 2 / $this->config['multi']['tz']);
+ $endY = ceil($centerY + $height / 2 / $this->config['multi']['tz']);
+
+ //Iterate on each coordinates
+ foreach($coordinates as $k => $coordinate) {
+ //Get coordinates
+ list($clatitude, $clongitude) = $coordinate;
+
+ //Set dest x
+ $destX = $this->longitudeToX($clongitude, $i);
+
+ //With outside point
+ if ($startX >= $destX || $endX <= $destX) {
+ //Skip zoom
+ continue(2);
+ }
+
+ //Set dest y
+ $destY = $this->latitudeToY($clatitude, $i);
+
+ //With outside point
+ if ($startY >= $destY || $endY <= $destY) {
+ //Skip zoom
+ continue(2);
+ }
+ }
+
+ //Found zoom
+ break;
+ }
+
+ //Return zoom
+ return $i;
+ }
+
/**
* Get thumb data
*
- * @param string $caption The caption
- * @param int $updated The updated timestamp
* @param string $path The path
- * @param int $width The width
- * @param int $height The height
+ * @param ?int $height The height
+ * @param ?int $width The width
* @return array The thumb data
*/
- public function getThumb(string $caption, int $updated, string $path, int $width = self::thumbWidth, int $height = self::thumbHeight): array {
- //Get image width and height
- list($imageWidth, $imageHeight) = getimagesize($path);
+ public function getThumb(string $path, ?int $height = null, ?int $width = null): array {
+ //Without height
+ if ($height === null) {
+ //Set height from config
+ $height = $this->config['thumb']['height'];
+ }
+
+ //Without width
+ if ($width === null) {
+ //Set width from config
+ $width = $this->config['thumb']['width'];
+ }
//Short path
$short = $this->slugger->short($path);
- //Set link hash
- $link = $this->slugger->serialize([$updated, $short, $imageWidth, $imageHeight]);
+ //Set hash
+ $hash = $this->slugger->serialize([$short, $height, $width]);
- //Set src hash
- $src = $this->slugger->serialize([$updated, $short, $width, $height]);
+ #TODO: compute thumb from file type ?
+ #TODO: see if setting format there is smart ? we do not yet know if we want a image or movie thumb ?
+ #TODO: do we add to route '_format' => $this->config['thumb']['format']
//Return array
return [
- 'caption' => $caption,
- 'link' => $this->router->generate('rapsys_pack_thumb', ['hash' => $link, 'updated' => $updated, 'path' => $short, 'width' => $imageWidth, 'height' => $imageHeight]),
- 'src' => $this->router->generate('rapsys_pack_thumb', ['hash' => $src, 'updated' => $updated, 'path' => $short, 'width' => $width, 'height' => $height]),
+ 'src' => $this->router->generate('rapsyspack_thumb', ['hash' => $hash, 'path' => $short, 'height' => $height, 'width' => $width]),
'width' => $width,
'height' => $height
];
}
+ /**
+ * 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
+ //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
+ $degree = round($latitude) % 60;
+
+ //Set minute
+ $minute = round(($latitude - $degree) * 60) % 60;
+
+ //Set second
+ $second = round(($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
+ //TODO: see if round or intval is better suited to fix the Deprecated: Implicit conversion from float to int loses precision
+ $degree = round($longitude) % 60;
+
+ //Set minute
+ $minute = round(($longitude - $degree) * 60) % 60;
+
+ //Set second
+ $second = round(($longitude - $degree - $minute / 60) * 3600) % 3600;
+
+ //Return sexagesimal longitude
+ return $degree.'°'.$minute.'\''.$second.'"'.($longitude >= 0 ? 'E' : 'W');
+ }
+
/**
* Remove image
*
* @param int $updated The updated timestamp
+ * @param string $prefix The prefix
* @param string $path The path
* @return array The thumb clear success
*/
- public function remove(int $updated, string $path): bool {
+ public function remove(int $updated, string $prefix, string $path): bool {
+ die('TODO: see how to make it work');
+
+ //Without valid prefix
+ if (!isset($this->config['prefixes'][$prefix])) {
+ //Throw error
+ throw new \Exception(sprintf('Invalid prefix "%s"', $prefix));
+ }
+
//Set hash tree
$hash = array_reverse(str_split(strval($updated)));
//Set dir
- $dir = $this->path.'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$updated.'/'.$this->slugger->short($path);
+ $dir = $this->config['public'].'/'.$this->config['prefixes'][$prefix].'/'.$hash[0].'/'.$hash[1].'/'.$hash[2].'/'.$this->slugger->short($path);
//Set removes
$removes = [];