* * 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; /** * Manages 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 width */ const highStrokeWidth = 4; /** * The map width */ const width = 640; /** * The map height */ const height = 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 width */ 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; /** * Creates a new map util * * @param RouterInterface $router The RouterInterface instance * @param SluggerUtil $slugger The SluggerUtil instance */ function __construct(protected RouterInterface $router, protected SluggerUtil $slugger, protected string $fill = self::fill, protected int $fontSize = self::fontSize, protected string $highFill = self::highFill, protected int $highFontSize = self::highFontSize, protected int $highRadius = self::highRadius, protected string $highStroke = self::highStroke, protected int $highStrokeWidth = self::highStrokeWidth, protected int $radius = self::radius, protected string $stroke = self::stroke, protected int $strokeWidth = self::strokeWidth) { } /** * Get fill color */ function getFill() { return $this->fill; } /** * Get font size */ function getFontSize() { return $this->fontSize; } /** * Get high fill color */ function getHighFill() { return $this->highFill; } /** * Get high font size */ function getHighFontSize() { return $this->highFontSize; } /** * Get high radius size */ function getHighRadius() { return $this->highRadius; } /** * Get high stroke color */ function getHighStroke() { return $this->highStroke; } /** * Get high stroke width */ function getHighStrokeWidth() { return $this->highStrokeWidth; } /** * Get radius size */ function getRadius() { return $this->radius; } /** * Get stroke color */ function getStroke() { return $this->stroke; } /** * Get stroke width */ function getStrokeWidth() { return $this->strokeWidth; } /** * Get map data * * @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 array The map data */ public function getMap(string $caption, int $updated, float $latitude, float $longitude, int $zoom = self::zoom, int $width = self::width, int $height = self::height): array { //Set link hash $link = $this->slugger->hash([$updated, $latitude, $longitude, $zoom + 1, $width * 2, $height * 2]); //Set src hash $src = $this->slugger->hash([$updated, $latitude, $longitude, $zoom, $width, $height]); //Return array return [ 'caption' => $caption, 'link' => $this->router->generate('rapsyspack_map', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]), 'src' => $this->router->generate('rapsyspack_map', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'zoom' => $zoom, 'width' => $width, 'height' => $height]), 'width' => $width, 'height' => $height ]; } /** * Get multi map data * * @param string $caption The caption * @param int $updated The updated timestamp * @param array $coordinates The coordinates array * @param int $width The width * @param int $height The height * @return array The multi map data */ public function getMultiMap(string $caption, int $updated, array $coordinates, int $width = self::width, int $height = self::height): array { //Without coordinates if (empty($coordinates)) { //Return empty array return []; } //Set latitudes $latitudes = array_map(function ($v) { return $v['latitude']; }, $coordinates); //Set longitudes $longitudes = array_map(function ($v) { return $v['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->getMultiZoom($latitude, $longitude, $coordinates, $width, $height); //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->hash([$updated, $latitude, $longitude, $hash, $zoom + 1, $width * 2, $height * 2]); //Set src hash $src = $this->slugger->hash([$updated, $latitude, $longitude, $hash, $zoom, $width, $height]); //Return array return [ 'caption' => $caption, 'link' => $this->router->generate('rapsyspack_multimap', ['hash' => $link, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom + 1, 'width' => $width * 2, 'height' => $height * 2]), 'src' => $this->router->generate('rapsyspack_multimap', ['hash' => $src, 'updated' => $updated, 'latitude' => $latitude, 'longitude' => $longitude, 'coordinates' => $coordinate, 'zoom' => $zoom, 'width' => $width, 'height' => $height]), 'width' => $width, 'height' => $height ]; } /** * 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) * * @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 $width The width * @param int $height The height * @param int $zoom The zoom * @return int The zoom */ public function getMultiZoom(float $latitude, float $longitude, array $coordinates, int $width, int $height, int $zoom = self::zoom): 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 //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 static 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'); } }