From 57f0e0c8581fa1a0a5d8f85078a73ded4d2d621e Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 18 Oct 2016 15:18:28 +0200 Subject: [PATCH 1/3] use PHP 5.6 for stable test on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1bdb03d..4702fd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ matrix: include: - php: 5.5 env: setup=lowest - - php: 5.5 + - php: 5.6 env: setup=stable - php: 5.6 env: setup=phar From 5a7ce5754ab365aff380e04ec88851609fb078bf Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 18 Oct 2016 15:19:47 +0200 Subject: [PATCH 2/3] Add first revision of static maps --- src/Common/APIObject.php | 27 +++ src/Common/Continent.php | 20 +++ src/Common/ItemStack.php | 22 +-- src/Common/Map.php | 18 ++ src/Maps/Coordinate.php | 85 +++++++++ src/Maps/StaticMap.php | 313 ++++++++++++++++++++++++++++++++++ src/Maps/assets/landmark.png | Bin 0 -> 532 bytes src/Maps/assets/menomonia.ttf | Bin 0 -> 44372 bytes src/Maps/assets/vista.png | Bin 0 -> 1165 bytes src/Maps/assets/waypoint.png | Bin 0 -> 4204 bytes 10 files changed, 464 insertions(+), 21 deletions(-) create mode 100644 src/Common/APIObject.php create mode 100644 src/Common/Continent.php create mode 100644 src/Common/Map.php create mode 100644 src/Maps/Coordinate.php create mode 100644 src/Maps/StaticMap.php create mode 100644 src/Maps/assets/landmark.png create mode 100644 src/Maps/assets/menomonia.ttf create mode 100644 src/Maps/assets/vista.png create mode 100644 src/Maps/assets/waypoint.png diff --git a/src/Common/APIObject.php b/src/Common/APIObject.php new file mode 100644 index 0000000..1907c4e --- /dev/null +++ b/src/Common/APIObject.php @@ -0,0 +1,27 @@ + &$value) { + $itemStack->{$property} = $value; + } + + return $itemStack; + } + + public static function fromArray(array $array) { + $itemStack = new static(); + + foreach ($array as $property => &$value) { + $itemStack->{$property} = $value; + } + + return $itemStack; + } +} diff --git a/src/Common/Continent.php b/src/Common/Continent.php new file mode 100644 index 0000000..e3dad08 --- /dev/null +++ b/src/Common/Continent.php @@ -0,0 +1,20 @@ + &$value) { - $itemStack->{$property} = $value; - } - - return $itemStack; - } - - public static function fromArray(array $array) { - $itemStack = new ItemStack(); - - foreach ($array as $property => &$value) { - $itemStack->{$property} = $value; - } - - return $itemStack; - } } diff --git a/src/Common/Map.php b/src/Common/Map.php new file mode 100644 index 0000000..210c04b --- /dev/null +++ b/src/Common/Map.php @@ -0,0 +1,18 @@ +x = $x; + $this->y = $y; + } + + public static function fromArray(array $array) { + return new Coordinate($array[0], $array[1]); + } + + public function multiply($x, $y = null) { + if($y == null) { + $y = $x; + } + + return new Coordinate( + $this->x * $x, + $this->y * $y + ); + } + + public function add($x, $y = null) { + if($y == null) { + $y = $x; + } + + return new Coordinate( + $this->x + $x, + $this->y + $y + ); + } + + public function subtract($x, $y = null) { + if($y == null) { + $y = $x; + } + + return new Coordinate( + $this->x - $x, + $this->y - $y + ); + } + + public function floor() { + return new Coordinate(floor($this->x), floor($this->y)); + } + + public function round() { + return new Coordinate(round($this->x), round($this->y)); + } + + public function ceil() { + return new Coordinate(ceil($this->x), ceil($this->y)); + } + + public function distance(Coordinate $other) { + return sqrt($this->distanceSquared($other)); + } + + public function distanceSquared(Coordinate $other) { + return pow($this->x - $other->x, 2) + pow($this->y - $other->y, 2); + } + + public function min(Coordinate $other) { + return new Coordinate(min($this->x, $other->x), min($this->y, $other->y)); + } + + public function max(Coordinate $other) { + return new Coordinate(max($this->x, $other->x), max($this->y, $other->y)); + } +} diff --git a/src/Maps/StaticMap.php b/src/Maps/StaticMap.php new file mode 100644 index 0000000..aaf0033 --- /dev/null +++ b/src/Maps/StaticMap.php @@ -0,0 +1,313 @@ +floors)) { + throw new \Exception("Floor $floor is not a valid floor of {$continent->name}."); + } + + $this->continent = $continent; + $this->floor = $floor; + + $this->setCachePath('./_mapcache'); + } + + /** + * Sets the bounding rectangle of the map. + * + * @param Coordinate $nwCorner + * @param Coordinate $seCorner + */ + public function setBounds(Coordinate $nwCorner, Coordinate $seCorner) { + if($nwCorner->x < 0 || $nwCorner->y < 0 || $seCorner->x > $this->continent->continent_dims[0] || $seCorner->y > $this->continent->continent_dims[1]) { + throw new \InvalidArgumentException("The map bounds are not within the continent dimensions."); + } + + $this->nwCorner = $nwCorner; + $this->seCorner = $seCorner; + } + + public function addWaypoint(Coordinate $position, $label) { + $this->pointsOfInterest[] = ['type' => 'waypoint'] + compact('position', 'label'); + } + + public function addLandmark(Coordinate $position, $label) { + $this->pointsOfInterest[] = ['type' => 'landmark'] + compact('position', 'label'); + } + + public function addVista(Coordinate $position) { + $this->pointsOfInterest[] = ['type' => 'vista', 'label' => null] + compact('position'); + } + + /** + * Adds a custom point of interest. + * + * @param Coordinate $position + * @param resource $icon + * @param string|null $label + */ + public function addCustomPointOfInterest(Coordinate $position, $icon, $label) { + $this->pointsOfInterest[] = ['type' => 'custom'] + compact('position', 'label', 'icon'); + } + + /** + * Converts world coordinates to tile coordinates + * + * @param Coordinate $coordinate + * @param int $zoom + * @return Coordinate + */ + protected function getTileCoordinatesOf(Coordinate $coordinate, $zoom) { + $tileCount = 1 << $zoom; + + return new Coordinate( + floor($coordinate->x / ($this->continent->continent_dims[0] / $tileCount)), + floor($coordinate->y / ($this->continent->continent_dims[1] / $tileCount)) + ); + } + + public function setCachePath($path) { + $this->cachePath = realpath($path); + } + + public function getCachePath() { + return $this->cachePath; + } + + protected function getTile($x, $y, $zoom) { + $cachePath = $this->getCachePath(); + $url = "https://tiles.guildwars2.com/{$this->continent->id}/{$this->floor}/$zoom/$x/$y.jpg"; + $cacheFileName = $cachePath."/{$this->continent->id}-{$this->floor}-$x-$y-$zoom.jpg"; + + if($cachePath === false) { + return imagecreatefromjpeg($url); + } + + if(!file_exists($cacheFileName)) { + $file = file_get_contents($url); + file_put_contents($cacheFileName, $file); + + return imagecreatefromstring($file); + } + + return imagecreatefromjpeg($cacheFileName); + } + + /** + * @param int $width + * @param int $height + * @param int $scale + * + * @return resource + */ + public function render($width, $height, $scale) { + if($width <= 0 || $height <= 0 || $scale <= 0) { + throw new \InvalidArgumentException("Parameters for render have to be positive."); + } + + // always use max_zoom + $zoom = $this->continent->max_zoom; + + $continentSize = new Coordinate($this->continent->continent_dims[0], $this->continent->continent_dims[1]); + $tileCount = 1 << $zoom; + $tileSize = $continentSize->x / $tileCount; + + // get the tiles of the north-west and south-east corner + $tileNW = $this->getTileCoordinatesOf($this->nwCorner, $zoom); + $tileSE = $this->getTileCoordinatesOf($this->seCorner, $zoom); + + // calculate how many tiles we are displaying + $tileCountX = $tileSE->x - $tileNW->x + 1; + $tileCountY = $tileSE->y - $tileNW->y + 1; + + // create a temp image buffer that gets resized later + // this prevents seems beetween tiles but uses a lot of memory if there are to many tiles… + $buffer = imagecreatetruecolor($tileCountX * 256, $tileCountY * 256); + + // load all tiles + // TODO: parallel loading + for($x = $tileNW->x; $x <= $tileSE->x; $x++) { + for($y = $tileNW->y; $y <= $tileSE->y; $y++) { + $tile = $this->getTile($x, $y, $zoom); + + // render the tile in the buffer + imagecopy($buffer, $tile, ($x - $tileNW->x) * 256, ($y - $tileNW->y) * 256, 0, 0, 256, 256); + imagedestroy($tile); + unset($tile); + } + } + + // get the world coordinates of the nw corner of the nw tile + $tileNWWorld = new Coordinate( + $tileSize * $tileNW->x, + $tileSize * $tileNW->y + ); + + // get the world coordinates of the sw corner of the sw tile + $tileSEWorld = new Coordinate( + $tileSize * $tileSE->x + $tileSize, + $tileSize * $tileSE->y + $tileSize + ); + + // get the size in world coordinates of the rendered buffer + $renderedSize = new Coordinate( + $tileSEWorld->x - $tileNWWorld->x, + $tileSEWorld->y - $tileNWWorld->y + ); + + // get the position of the NW corner of the requested bounds within the buffer + $rectMin = new Coordinate( + ($this->nwCorner->x - $tileNWWorld->x) / $renderedSize->x, + ($this->nwCorner->y - $tileNWWorld->y) / $renderedSize->y + ); + + // get the position of the SW corner of the requested bounds within the buffer + $rectMax = new Coordinate( + ($this->seCorner->x - $tileNWWorld->x) / $renderedSize->x, + ($this->seCorner->y - $tileNWWorld->y) / $renderedSize->y + ); + + + // get the aspect ration of the map bounds and the size of the image + $aspectRatio = $width / $height; + $mapAspectRatio = ($this->seCorner->x - $this->nwCorner->x) / ($this->seCorner->y - $this->nwCorner->y); + + // resize the image if needed + // TODO: adjust the bounds of rendered map instead? Always return exactly the requested image size… + if($aspectRatio < $mapAspectRatio) { + $width = ceil($height * $mapAspectRatio); + } else { + $height = ceil($width * (1 / $mapAspectRatio)); + } + + // create the image + $image = imagecreatetruecolor($width * $scale, $height * $scale); + $white = imagecolorallocate($buffer, 255, 255, 255); + $black = imagecolorallocate($buffer, 0, 0, 0); + + // copy the buffer into the image + imagecopyresampled($image, $buffer, + // destination position + 0, 0, + // source position + $rectMin->x * $tileCountX * 256, $rectMin->y * $tileCountY * 256, + // destination size + $width * $scale, $height * $scale, + // source size + ($rectMax->x - $rectMin->x) * $tileCountX * 256, ($rectMax->y - $rectMin->y) * $tileCountY * 256 + ); + imagedestroy($buffer); + unset($buffer); + + // render waypoints/pois/… + $poiIcons = []; + foreach($this->pointsOfInterest as $pointOfInterest) { + $hasCustomIcon = array_key_exists('icon', $pointOfInterest) && is_resource($pointOfInterest['icon']); + + if(!$hasCustomIcon && !array_key_exists($pointOfInterest['type'], $poiIcons)) { + $poiIcons[$pointOfInterest['type']] = imagecreatefrompng(__DIR__.'/assets/'.$pointOfInterest['type'].'.png'); + } + + $icon = $hasCustomIcon + ? $pointOfInterest['icon'] + : $poiIcons[$pointOfInterest['type']]; + + $poiPosition = $this->worldCoordinateToBoundary($pointOfInterest['position']) + ->multiply($width * $scale, $height * $scale)->round(); + + $this->drawIconWithLabel($image, $icon, $pointOfInterest['label'], $poiPosition, $scale, $white, $black); + } + + foreach($poiIcons as $icon) { + imagedestroy($icon); + unset($icon); + } + unset($poiIcons); + + return $image; + } + + protected function drawIconWithLabel(&$image, &$icon, $text, $position, $scale, $color, $shadow) { + imagecopyresampled($image, $icon, $position->x - 8 * $scale, $position->y - 8 * $scale, 0, 0, 16 * $scale, 16 * $scale, imagesx($icon), imagesy($icon)); + + if($text === null) { + return; + } + + $fontFile = __DIR__.'/assets/menomonia.ttf'; + + $fontSize = 10 * ($scale * 0.75 + 0.25); + $textSize = imagettfbbox($fontSize, 0, $fontFile, $text); + $textWidth = $textSize[2] - $textSize[0]; + $textHeight = $textSize[3] - $textSize[1]; + + $imageWidth = imagesx($image); + $imageHeight = imagesy($image); + + $x = min(max(8 * $scale, $position->x - $textWidth / 2), $imageWidth - 8 * $scale - $textWidth); + $y = min(max($textHeight + 8 * $scale, $position->y + $textHeight + 18 * $scale), $imageHeight - 8 * $scale); + + imagettftext($image, $fontSize, 0, $x + 1, $y + 0, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x - 1, $y + 0, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x + 0, $y + 1, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x + 0, $y - 1, $shadow, $fontFile, $text); + + imagettftext($image, $fontSize, 0, $x + 1, $y + 1, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x + 1, $y - 1, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x - 1, $y + 1, $shadow, $fontFile, $text); + imagettftext($image, $fontSize, 0, $x - 1, $y - 1, $shadow, $fontFile, $text); + + imagettftext($image, $fontSize, 0, $x + 0, $y + 0, $color, $fontFile, $text); + } + + public function worldCoordinateToBoundary(Coordinate $coordinate) { + return new Coordinate( + ($coordinate->x - $this->nwCorner->x) / ($this->seCorner->x - $this->nwCorner->x), + ($coordinate->y - $this->nwCorner->y) / ($this->seCorner->y - $this->nwCorner->y) + ); + } + + public static function mapCoordinateToContinent(Coordinate $coordinate, Map $map) { + $continentMin = Coordinate::fromArray($map->continent_rect[0]); + $continentMax = Coordinate::fromArray($map->continent_rect[1]); + + $mapMin = Coordinate::fromArray($map->map_rect[0]); + $mapMax = Coordinate::fromArray($map->map_rect[1]); + + return new Coordinate( + $continentMin->x + ($continentMax->x - $continentMin->x) * (($coordinate->x - $mapMin->x) / ($mapMax->x - $mapMin->x)), + $continentMin->y + ($continentMax->y - $continentMin->y) * (($coordinate->y - $mapMin->y) / ($mapMax->y - $mapMin->y)) + ); + } +} diff --git a/src/Maps/assets/landmark.png b/src/Maps/assets/landmark.png new file mode 100644 index 0000000000000000000000000000000000000000..7352e564f593d5c3538390426799c73773ac7e1d GIT binary patch literal 532 zcmV+v0_**WP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0jWtuK~z{r?Up}F z!$26u6I1m-MF)pWVh1NDzks+2E{cY{fm6K!4iC=q z-_(xt#IVF)2^{JI@Ac;t1hkt6;a*n&^@7&R71exK0}c3SpmIP&ZG8()um!X6{sU2g zo%97XZG;zefo668w+%eRZy+jA?rnf26kNTcXA4+hCEgbN%(CaE#U;Sr@*xD&d7#F= zgDFtwz$*k6Oa&~2@5K~&OjH08O%`#7AWuPVfdy7g8{jQilr60B2!RT?2iKqsN?=!x zC4^qc@IoRD^sT^l_f%|3JZf5!o-K-M|+F WS?phxuPy8V00000t5*Az9|TZNKp|% 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 ~;KcEt9SF=UgvhPc%Nbmg> zUNWH~zSVKF;xHi5GS26@k?Z6u`$DFG=8g^pi_tWl?UZagHNKcoCMcN&kua|!%L%Jz zfb*qMj!PH$H(Z3M-^Tw?q=GG}HjRxzg(_hZ&~XK5k$jBpihy#KQ*_ahVDSf9WI)y3 zkGX#P+N?=v%JO<`#zteqnmd*Bft%=nsMtn-j?{arSt_X&7?r+!&djd~-loSw4b>9P z-EyFWt%$G@ZNlS3PIy(<);Q#%EEcy}#dnB~1Y9&+}({~@xl>7fa zV_>kq&4)xtt;f1M>H3P`J-yRBsZ71dK?UeX%+pRa!_Mw|#YyNgt}kS?_r*VUUe_*- zw3-C{+ldxADfwIE1(VJxZ2^AIqFZAd{bglkxV#rHaUY%Y`9vza_aERmxU|@egbW@H zwZ01t=R-O?<~rB2@ue5?BPE0?k~DgmiSQkVc+_l&8C28BWI4=u*4#Zk>fs(?VjdK4 vHJZA;6Mnf6MCozuw4N;Yap>bZbaXj7jz~R%W1-8#fPQqeZfU+z$3^`Q*WSzS literal 0 HcmV?d00001 From 8c0c78221cfef3923ecc728cd72b23e33a8cd069 Mon Sep 17 00:00:00 2001 From: darthmaim Date: Mon, 24 Oct 2016 23:39:48 +0200 Subject: [PATCH 3/3] always render labels on top of icons --- src/Maps/StaticMap.php | 81 ++++++++++++++++++------------------- src/Maps/StaticMapIcon.php | 72 +++++++++++++++++++++++++++++++++ src/Maps/StaticMapLabel.php | 60 +++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 42 deletions(-) create mode 100644 src/Maps/StaticMapIcon.php create mode 100644 src/Maps/StaticMapLabel.php diff --git a/src/Maps/StaticMap.php b/src/Maps/StaticMap.php index aaf0033..af73b8e 100644 --- a/src/Maps/StaticMap.php +++ b/src/Maps/StaticMap.php @@ -22,8 +22,11 @@ class StaticMap { /** @var string $cachePath */ protected $cachePath; - /** @var array $pointsOfInterest */ - protected $pointsOfInterest = []; + /** @var StaticMapIcon[] $icons */ + protected $icons = []; + + /** @var StaticMapLabel[] $labels */ + protected $labels = []; /** * StaticMap constructor. @@ -59,26 +62,25 @@ public function setBounds(Coordinate $nwCorner, Coordinate $seCorner) { } public function addWaypoint(Coordinate $position, $label) { - $this->pointsOfInterest[] = ['type' => 'waypoint'] + compact('position', 'label'); + $this->addLabel(new StaticMapLabel($label, $position, true)); + $this->addIcon(StaticMapIcon::waypoint($position)); } public function addLandmark(Coordinate $position, $label) { - $this->pointsOfInterest[] = ['type' => 'landmark'] + compact('position', 'label'); + $this->addLabel(new StaticMapLabel($label, $position, true)); + $this->addIcon(StaticMapIcon::landmark($position)); } public function addVista(Coordinate $position) { - $this->pointsOfInterest[] = ['type' => 'vista', 'label' => null] + compact('position'); + $this->addIcon(StaticMapIcon::vista($position)); } - /** - * Adds a custom point of interest. - * - * @param Coordinate $position - * @param resource $icon - * @param string|null $label - */ - public function addCustomPointOfInterest(Coordinate $position, $icon, $label) { - $this->pointsOfInterest[] = ['type' => 'custom'] + compact('position', 'label', 'icon'); + public function addIcon(StaticMapIcon $icon) { + $this->icons[] = $icon; + } + + public function addLabel(StaticMapLabel $label) { + $this->labels[] = $label; } /** @@ -230,50 +232,45 @@ public function render($width, $height, $scale) { imagedestroy($buffer); unset($buffer); - // render waypoints/pois/… - $poiIcons = []; - foreach($this->pointsOfInterest as $pointOfInterest) { - $hasCustomIcon = array_key_exists('icon', $pointOfInterest) && is_resource($pointOfInterest['icon']); - - if(!$hasCustomIcon && !array_key_exists($pointOfInterest['type'], $poiIcons)) { - $poiIcons[$pointOfInterest['type']] = imagecreatefrompng(__DIR__.'/assets/'.$pointOfInterest['type'].'.png'); - } - - $icon = $hasCustomIcon - ? $pointOfInterest['icon'] - : $poiIcons[$pointOfInterest['type']]; - - $poiPosition = $this->worldCoordinateToBoundary($pointOfInterest['position']) - ->multiply($width * $scale, $height * $scale)->round(); - - $this->drawIconWithLabel($image, $icon, $pointOfInterest['label'], $poiPosition, $scale, $white, $black); + // render icons + foreach($this->icons as $icon) { + $this->renderIcon($image, $icon, $width, $height, $scale); } - foreach($poiIcons as $icon) { - imagedestroy($icon); - unset($icon); + // render labels + foreach($this->labels as $label) { + $this->renderLabel($image, $label, $width, $height, $scale); } - unset($poiIcons); return $image; } - protected function drawIconWithLabel(&$image, &$icon, $text, $position, $scale, $color, $shadow) { - imagecopyresampled($image, $icon, $position->x - 8 * $scale, $position->y - 8 * $scale, 0, 0, 16 * $scale, 16 * $scale, imagesx($icon), imagesy($icon)); + protected function renderIcon(&$image, StaticMapIcon $icon, $width, $height, $scale) { + $position = $this->worldCoordinateToBoundary($icon->getPosition()) + ->multiply($width * $scale, $height * $scale)->round(); - if($text === null) { - return; - } + $img = $icon->getIcon(); + imagecopyresampled($image, $img, $position->x - 8 * $scale, $position->y - 8 * $scale, 0, 0, 16 * $scale, 16 * $scale, imagesx($img), imagesy($img)); + } + + protected function renderLabel(&$image, StaticMapLabel $label, $width, $height, $scale) { $fontFile = __DIR__.'/assets/menomonia.ttf'; + $color = $label->getColor(); + $color = imagecolorallocate($image, $color[0], $color[1], $color[2]); + $shadow = imagecolorallocate($image, 0, 0, 0); + $text = $label->getText(); $fontSize = 10 * ($scale * 0.75 + 0.25); $textSize = imagettfbbox($fontSize, 0, $fontFile, $text); $textWidth = $textSize[2] - $textSize[0]; $textHeight = $textSize[3] - $textSize[1]; - $imageWidth = imagesx($image); - $imageHeight = imagesy($image); + $imageWidth = $width * $scale; + $imageHeight = $height * $scale; + + $position = $this->worldCoordinateToBoundary($label->getPosition()) + ->multiply($width * $scale, $height * $scale)->round(); $x = min(max(8 * $scale, $position->x - $textWidth / 2), $imageWidth - 8 * $scale - $textWidth); $y = min(max($textHeight + 8 * $scale, $position->y + $textHeight + 18 * $scale), $imageHeight - 8 * $scale); diff --git a/src/Maps/StaticMapIcon.php b/src/Maps/StaticMapIcon.php new file mode 100644 index 0000000..5d8ed52 --- /dev/null +++ b/src/Maps/StaticMapIcon.php @@ -0,0 +1,72 @@ +icon = $icon; + $this->position = $position; + } + + public function getIcon() { + return $this->icon; + } + + public function getPosition() { + return $this->position; + } + + // === STATIC === + + public static function waypoint(Coordinate $position) { + $waypoint = new static(null, $position); + $waypoint->icon = self::getAsset(self::ASSET_WAYPOINT); + + return $waypoint; + } + + public static function landmark(Coordinate $position) { + $waypoint = new static(null, $position); + $waypoint->icon = self::getAsset(self::ASSET_LANDMARK); + + return $waypoint; + } + + public static function vista(Coordinate $position) { + $waypoint = new static(null, $position); + $waypoint->icon = self::getAsset(self::ASSET_VISTA); + + return $waypoint; + } + + /** @var resource[] $assets */ + private static $_assets = []; + + const ASSET_WAYPOINT = 'waypoint'; + const ASSET_LANDMARK = 'landmark'; + const ASSET_VISTA = 'vista'; + + /** + * @param string $asset + * @return resource + */ + private static function getAsset($asset) { + if(!array_key_exists($asset, self::$_assets)) { + self::$_assets[$asset] = imagecreatefrompng(__DIR__.'/assets/'.$asset.'.png'); + } + + return self::$_assets[$asset]; + } +} diff --git a/src/Maps/StaticMapLabel.php b/src/Maps/StaticMapLabel.php new file mode 100644 index 0000000..a3d769d --- /dev/null +++ b/src/Maps/StaticMapLabel.php @@ -0,0 +1,60 @@ +text = $text; + $this->position = $position; + $this->hasIcon = $hasIcon; + $this->color = $color; + } + + /** + * @return string + */ + public function getText() { + return $this->text; + } + + /** + * @return Coordinate + */ + public function getPosition() { + return $this->position; + } + + /** + * @return boolean + */ + public function hasIcon() { + return $this->hasIcon; + } + + /** + * @return int[] + */ + public function getColor() { + return $this->color; + } +}