1<?php 2/* 3 * Copyright (c) 2012-2018 Mark C. Prins <mprins@users.sf.net> 4 * 5 * In part based on staticMapLite 0.03 available at http://staticmaplite.svn.sourceforge.net/viewvc/staticmaplite/ 6 * 7 * Copyright (c) 2009 Gerhard Koch <gerhard.koch AT ymail.com> 8 * 9 * Licensed under the Apache License, Version 2.0 (the "License"); 10 * you may not use this file except in compliance with the License. 11 * You may obtain a copy of the License at 12 * 13 * http://www.apache.org/licenses/LICENSE-2.0 14 * 15 * Unless required by applicable law or agreed to in writing, software 16 * distributed under the License is distributed on an "AS IS" BASIS, 17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 * See the License for the specific language governing permissions and 19 * limitations under the License. 20 */ 21 22use geoPHP\Geometry\Geometry; 23use geoPHP\Geometry\GeometryCollection; 24use geoPHP\Geometry\LineString; 25use geoPHP\Geometry\Point; 26use geoPHP\Geometry\Polygon; 27use geoPHP\geoPHP; 28 29// phpcs:disable PSR1.Files.SideEffects 30// TODO resolve side effect 31require_once __DIR__ . '/../geophp/vendor/autoload.php'; 32 33/** 34 * 35 * @author Mark C. Prins <mprins@users.sf.net> 36 * @author Gerhard Koch <gerhard.koch AT ymail.com> 37 * 38 */ 39class StaticMap { 40 41 // the final output 42 private $tileSize = 256; 43 private $tileInfo = array( 44 // OSM sources 45 'openstreetmap' => array( 46 'txt' => '(c) OpenStreetMap data/ODbl', 47 'logo' => 'osm_logo.png', 48 'url' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png' 49 ), 50 // OpenTopoMap sources 51 'opentopomap' => array( 52 'txt' => '(c) OpenStreetMap data/ODbl, SRTM | style: (c) OpenTopoMap', 53 'logo' => 'osm_logo.png', 54 'url' => 'https:/tile.opentopomap.org/{Z}/{X}/{Y}.png' 55 ), 56 // OCM sources 57 'cycle' => array( 58 'txt' => '(c) Thunderforest maps', 59 'logo' => 'tf_logo.png', 60 'url' => 'https://tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png' 61 ), 62 'transport' => array( 63 'txt' => '(c) Thunderforest maps', 64 'logo' => 'tf_logo.png', 65 'url' => 'https://tile.thunderforest.com/transport/{Z}/{X}/{Y}.png' 66 ), 67 'landscape' => array( 68 'txt' => '(c) Thunderforest maps', 69 'logo' => 'tf_logo.png', 70 'url' => 'https://tile.thunderforest.com/landscape/{Z}/{X}/{Y}.png' 71 ), 72 'outdoors' => array( 73 'txt' => '(c) Thunderforest maps', 74 'logo' => 'tf_logo.png', 75 'url' => 'https://tile.thunderforest.com/outdoors/{Z}/{X}/{Y}.png' 76 ), 77 'toner-lite' => array( 78 'txt' => 'Stamen tiles', 79 'logo' => 'stamen.png', 80 'url' => 'https://stamen-tiles.a.ssl.fastly.net/toner/{Z}/{X}/{Y}.png' 81 ), 82 'terrain' => array( 83 'txt' => 'Stamen tiles', 84 'logo' => 'stamen.png', 85 'url' => 'https://stamen-tiles.a.ssl.fastly.net/terrain/{Z}/{X}/{Y}.jpg' 86 ) 87 //, 88 // 'piste'=>array( 89 // 'txt'=>'OpenPisteMap tiles', 90 // 'logo'=>'piste_logo.png', 91 // 'url'=>''), 92 // 'sea'=>array( 93 // 'txt'=>'OpenSeaMap tiles', 94 // 'logo'=>'sea_logo.png', 95 // 'url'=>''), 96 // H&B sources 97 // 'hikeandbike' => array ( 98 // 'txt' => 'Hike & Bike Map', 99 // 'logo' => 'hnb_logo.png', 100 // //'url' => 'http://toolserver.org/tiles/hikebike/{Z}/{X}/{Y}.png' 101 // //moved to: https://www.toolserver.org/tiles/hikebike/12/2105/1388.png 102 // 'url' => 'http://c.tiles.wmflabs.org/hikebike/{Z}/{X}/{Y}.png' 103 // ) 104 ); 105 private $tileDefaultSrc = 'openstreetmap'; 106 107 // set up markers 108 private $markerPrototypes = array( 109 // found at http://www.mapito.net/map-marker-icons.html 110 // these are 17x19 px with a pointer at the bottom left 111 'lightblue' => array( 112 'regex' => '/^lightblue([0-9]+)$/', 113 'extension' => '.png', 114 'shadow' => false, 115 'offsetImage' => '0,-19', 116 'offsetShadow' => false 117 ), 118 // openlayers std markers are 21x25px with shadow 119 'ol-marker' => array( 120 'regex' => '/^marker(|-blue|-gold|-green|-red)+$/', 121 'extension' => '.png', 122 'shadow' => 'marker_shadow.png', 123 'offsetImage' => '-10,-25', 124 'offsetShadow' => '-1,-13' 125 ), 126 // these are 16x16 px 127 'ww_icon' => array( 128 'regex' => '/ww_\S+$/', 129 'extension' => '.png', 130 'shadow' => false, 131 'offsetImage' => '-8,-8', 132 'offsetShadow' => false 133 ), 134 // assume these are 16x16 px 135 'rest' => array( 136 'regex' => '/^(?!lightblue([0-9]+)$)(?!(ww_\S+$))(?!marker(|-blue|-gold|-green|-red)+$)(.*)/', 137 'extension' => '.png', 138 'shadow' => 'marker_shadow.png', 139 'offsetImage' => '-8,-8', 140 'offsetShadow' => '-1,-1' 141 ) 142 ); 143 private $centerX; 144 private $centerY; 145 private $offsetX; 146 private $offsetY; 147 private $image; 148 private $zoom; 149 private $lat; 150 private $lon; 151 private $width; 152 private $height; 153 private $markers; 154 private $maptype; 155 private $kmlFileName; 156 private $gpxFileName; 157 private $geojsonFileName; 158 private $autoZoomExtent; 159 private $apikey; 160 private $tileCacheBaseDir; 161 private $mapCacheBaseDir; 162 private $mediaBaseDir; 163 private $useTileCache; 164 private $mapCacheID = ''; 165 private $mapCacheFile = ''; 166 private $mapCacheExtension = 'png'; 167 168 /** 169 * Constructor. 170 * 171 * @param float $lat 172 * Latitude (x) of center of map 173 * @param float $lon 174 * Longitude (y) of center of map 175 * @param int $zoom 176 * Zoomlevel 177 * @param int $width 178 * Width in pixels 179 * @param int $height 180 * Height in pixels 181 * @param string $maptype 182 * Name of the map 183 * @param array $markers 184 * array of markers 185 * @param string $gpx 186 * GPX filename 187 * @param string $kml 188 * KML filename 189 * @param string $geojson 190 * @param string $mediaDir 191 * Directory to store/cache maps 192 * @param string $tileCacheBaseDir 193 * Directory to cache map tiles 194 * @param bool $autoZoomExtent 195 * Wheter or not to override zoom/lat/lon and zoom to the extent of gpx/kml and markers 196 * @param string $apikey 197 */ 198 public function __construct( 199 float $lat, 200 float $lon, 201 int $zoom, 202 int $width, 203 int $height, 204 string $maptype, 205 array $markers, 206 string $gpx, 207 string $kml, 208 string $geojson, 209 string $mediaDir, 210 string $tileCacheBaseDir, 211 bool $autoZoomExtent = true, 212 string $apikey = '' 213 ) { 214 $this->zoom = $zoom; 215 $this->lat = $lat; 216 $this->lon = $lon; 217 $this->width = $width; 218 $this->height = $height; 219 // validate + set maptype 220 $this->maptype = $this->tileDefaultSrc; 221 if(array_key_exists($maptype, $this->tileInfo)) { 222 $this->maptype = $maptype; 223 } 224 $this->markers = $markers; 225 $this->kmlFileName = $kml; 226 $this->gpxFileName = $gpx; 227 $this->geojsonFileName = $geojson; 228 $this->mediaBaseDir = $mediaDir; 229 $this->tileCacheBaseDir = $tileCacheBaseDir . '/olmaptiles'; 230 $this->useTileCache = $this->tileCacheBaseDir !== ''; 231 $this->mapCacheBaseDir = $mediaDir . '/olmapmaps'; 232 $this->autoZoomExtent = $autoZoomExtent; 233 $this->apikey = $apikey; 234 } 235 236 /** 237 * get the map, this may return a reference to a cached copy. 238 * 239 * @return string url relative to media dir 240 */ 241 public function getMap(): string { 242 try { 243 if($this->autoZoomExtent) { 244 $this->autoZoom(); 245 } 246 } catch(Exception $e) { 247 dbglog($e); 248 } 249 250 // use map cache, so check cache for map 251 if(!$this->checkMapCache()) { 252 // map is not in cache, needs to be build 253 $this->makeMap(); 254 $this->mkdirRecursive(dirname($this->mapCacheIDToFilename()), 0777); 255 imagepng($this->image, $this->mapCacheIDToFilename(), 9); 256 } 257 $doc = $this->mapCacheIDToFilename(); 258 // make url relative to media dir 259 return str_replace($this->mediaBaseDir, '', $doc); 260 } 261 262 /** 263 * Calculate the lat/lon/zoom values to make sure that all of the markers and gpx/kml are on the map. 264 * 265 * @param float $paddingFactor 266 * buffer constant to enlarge (>1.0) the zoom level 267 * @throws Exception if non-geometries are found in the collection 268 */ 269 private function autoZoom(float $paddingFactor = 1.0): void { 270 $geoms = array(); 271 $geoms [] = new Point ($this->lon, $this->lat); 272 if(!empty ($this->markers)) { 273 foreach($this->markers as $marker) { 274 $geoms [] = new Point ($marker ['lon'], $marker ['lat']); 275 } 276 } 277 if(file_exists($this->kmlFileName)) { 278 $g = geoPHP::load(file_get_contents($this->kmlFileName), 'kml'); 279 if($g !== false) { 280 $geoms [] = $g; 281 } 282 } 283 if(file_exists($this->gpxFileName)) { 284 $g = geoPHP::load(file_get_contents($this->gpxFileName), 'gpx'); 285 if($g !== false) { 286 $geoms [] = $g; 287 } 288 } 289 if(file_exists($this->geojsonFileName)) { 290 $g = geoPHP::load(file_get_contents($this->geojsonFileName), 'geojson'); 291 if($g !== false) { 292 $geoms [] = $g; 293 } 294 } 295 296 if(count($geoms) <= 1) { 297 dbglog($geoms, "StaticMap::autoZoom: Skip setting autozoom options"); 298 return; 299 } 300 301 $geom = new GeometryCollection ($geoms); 302 $centroid = $geom->centroid(); 303 $bbox = $geom->getBBox(); 304 305 // determine vertical resolution, this depends on the distance from the equator 306 // $vy00 = log(tan(M_PI*(0.25 + $centroid->getY()/360))); 307 $vy0 = log(tan(M_PI * (0.25 + $bbox ['miny'] / 360))); 308 $vy1 = log(tan(M_PI * (0.25 + $bbox ['maxy'] / 360))); 309 dbglog("StaticMap::autoZoom: vertical resolution: $vy0, $vy1"); 310 if ($vy1 - $vy0 === 0.0){ 311 $resolutionVertical = 0; 312 dbglog("StaticMap::autoZoom: using $resolutionVertical"); 313 } else { 314 $zoomFactorPowered = ($this->height / 2) / (40.7436654315252 * ($vy1 - $vy0)); 315 $resolutionVertical = 360 / ($zoomFactorPowered * $this->tileSize); 316 } 317 // determine horizontal resolution 318 $resolutionHorizontal = ($bbox ['maxx'] - $bbox ['minx']) / $this->width; 319 dbglog("StaticMap::autoZoom: using $resolutionHorizontal"); 320 $resolution = max($resolutionHorizontal, $resolutionVertical) * $paddingFactor; 321 $zoom = $this->zoom; 322 if ($resolution > 0){ 323 $zoom = log(360 / ($resolution * $this->tileSize), 2); 324 } 325 326 if(is_finite($zoom) && $zoom < 15 && $zoom > 2) { 327 $this->zoom = floor($zoom); 328 } 329 $this->lon = $centroid->getX(); 330 $this->lat = $centroid->getY(); 331 dbglog("StaticMap::autoZoom: Set autozoom options to: z: $this->zoom, lon: $this->lon, lat: $this->lat"); 332 } 333 334 public function checkMapCache(): bool { 335 // side effect: set the mapCacheID 336 $this->mapCacheID = md5($this->serializeParams()); 337 $filename = $this->mapCacheIDToFilename(); 338 return file_exists($filename); 339 } 340 341 public function serializeParams(): string { 342 return implode( 343 "&", array( 344 $this->zoom, 345 $this->lat, 346 $this->lon, 347 $this->width, 348 $this->height, 349 serialize($this->markers), 350 $this->maptype, 351 $this->kmlFileName, 352 $this->gpxFileName, 353 $this->geojsonFileName 354 ) 355 ); 356 } 357 358 public function mapCacheIDToFilename(): string { 359 if(!$this->mapCacheFile) { 360 $this->mapCacheFile = $this->mapCacheBaseDir . "/" . $this->maptype . "/" . $this->zoom . "/cache_" 361 . substr($this->mapCacheID, 0, 2) . "/" . substr($this->mapCacheID, 2, 2) 362 . "/" . substr($this->mapCacheID, 4); 363 } 364 return $this->mapCacheFile . "." . $this->mapCacheExtension; 365 } 366 367 /** 368 * make the map. 369 */ 370 public function makeMap(): void { 371 $this->initCoords(); 372 $this->createBaseMap(); 373 if(!empty ($this->markers)) { 374 $this->placeMarkers(); 375 } 376 if (file_exists($this->kmlFileName)) { 377 try { 378 $this->drawKML(); 379 } catch (exception $e) { 380 dbglog('failed to load KML file', $e); 381 } 382 } 383 if (file_exists($this->gpxFileName)) { 384 try { 385 $this->drawGPX(); 386 } catch (exception $e) { 387 dbglog('failed to load GPX file', $e); 388 } 389 } 390 if (file_exists($this->geojsonFileName)) { 391 try { 392 $this->drawGeojson(); 393 } catch (exception $e) { 394 dbglog('failed to load GeoJSON file', $e); 395 } 396 } 397 398 $this->drawCopyright(); 399 } 400 401 /** 402 */ 403 public function initCoords(): void { 404 $this->centerX = $this->lonToTile($this->lon, $this->zoom); 405 $this->centerY = $this->latToTile($this->lat, $this->zoom); 406 $this->offsetX = floor((floor($this->centerX) - $this->centerX) * $this->tileSize); 407 $this->offsetY = floor((floor($this->centerY) - $this->centerY) * $this->tileSize); 408 } 409 410 /** 411 * 412 * @param float $long 413 * @param int $zoom 414 * @return float|int 415 */ 416 public function lonToTile(float $long, int $zoom) { 417 return (($long + 180) / 360) * pow(2, $zoom); 418 } 419 420 /** 421 * 422 * @param float $lat 423 * @param int $zoom 424 * @return float|int 425 */ 426 public function latToTile(float $lat, int $zoom) { 427 return (1 - log(tan($lat * pi() / 180) + 1 / cos($lat * M_PI / 180)) / M_PI) / 2 * pow(2, $zoom); 428 } 429 430 /** 431 * make basemap image. 432 */ 433 public function createBaseMap(): void { 434 $this->image = imagecreatetruecolor($this->width, $this->height); 435 $startX = floor($this->centerX - ($this->width / $this->tileSize) / 2); 436 $startY = floor($this->centerY - ($this->height / $this->tileSize) / 2); 437 $endX = ceil($this->centerX + ($this->width / $this->tileSize) / 2); 438 $endY = ceil($this->centerY + ($this->height / $this->tileSize) / 2); 439 $this->offsetX = -floor(($this->centerX - floor($this->centerX)) * $this->tileSize); 440 $this->offsetY = -floor(($this->centerY - floor($this->centerY)) * $this->tileSize); 441 $this->offsetX += floor($this->width / 2); 442 $this->offsetY += floor($this->height / 2); 443 $this->offsetX += floor($startX - floor($this->centerX)) * $this->tileSize; 444 $this->offsetY += floor($startY - floor($this->centerY)) * $this->tileSize; 445 446 for($x = $startX; $x <= $endX; $x++) { 447 for($y = $startY; $y <= $endY; $y++) { 448 $url = str_replace( 449 array( 450 '{Z}', 451 '{X}', 452 '{Y}' 453 ), array( 454 $this->zoom, 455 $x, 456 $y 457 ), $this->tileInfo [$this->maptype] ['url'] 458 ); 459 460 $tileData = $this->fetchTile($url); 461 if($tileData) { 462 $tileImage = imagecreatefromstring($tileData); 463 } else { 464 $tileImage = imagecreate($this->tileSize, $this->tileSize); 465 $color = imagecolorallocate($tileImage, 255, 255, 255); 466 @imagestring($tileImage, 1, 127, 127, 'err', $color); 467 } 468 $destX = ($x - $startX) * $this->tileSize + $this->offsetX; 469 $destY = ($y - $startY) * $this->tileSize + $this->offsetY; 470 dbglog($this->tileSize, "imagecopy tile into image: $destX, $destY"); 471 imagecopy( 472 $this->image, $tileImage, $destX, $destY, 0, 0, $this->tileSize, 473 $this->tileSize 474 ); 475 } 476 } 477 } 478 479 /** 480 * Fetch a tile and (if configured) store it in the cache. 481 * @param string $url 482 * @return bool|string 483 * @todo refactor this to use dokuwiki\HTTP\HTTPClient or dokuwiki\HTTP\DokuHTTPClient 484 * for better proxy handling... 485 */ 486 public function fetchTile(string $url) { 487 if($this->useTileCache && ($cached = $this->checkTileCache($url))) 488 return $cached; 489 490 $_UA = 'Mozilla/4.0 (compatible; DokuWikiSpatial HTTP Client; ' . PHP_OS . ')'; 491 if(function_exists("curl_init")) { 492 // use cUrl 493 $ch = curl_init(); 494 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 495 curl_setopt($ch, CURLOPT_USERAGENT, $_UA); 496 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); 497 curl_setopt($ch, CURLOPT_URL, $url . $this->apikey); 498 dbglog("StaticMap::fetchTile: getting: $url using curl_exec"); 499 $tile = curl_exec($ch); 500 curl_close($ch); 501 } else { 502 // use file_get_contents 503 global $conf; 504 $opts = array( 505 'http' => array( 506 'method' => "GET", 507 'header' => "Accept-language: en\r\n" . "User-Agent: $_UA\r\n" . "accept: image/png\r\n", 508 'request_fulluri' => true 509 ) 510 ); 511 if(isset($conf['proxy']['host'], $conf['proxy']['port']) 512 && $conf['proxy']['host'] !== '' 513 && $conf['proxy']['port'] !== '') { 514 $opts['http'] += ['proxy' => "tcp://" . $conf['proxy']['host'] . ":" . $conf['proxy']['port']]; 515 } 516 517 $context = stream_context_create($opts); 518 dbglog("StaticMap::fetchTile: getting: $url . $this->apikey using file_get_contents and options $opts"); 519 $tile = file_get_contents($url . $this->apikey, false, $context); 520 } 521 if($tile && $this->useTileCache) { 522 $this->writeTileToCache($url, $tile); 523 } 524 return $tile; 525 } 526 527 /** 528 * 529 * @param string $url 530 * @return string|false 531 */ 532 public function checkTileCache(string $url) { 533 $filename = $this->tileUrlToFilename($url); 534 if(file_exists($filename)) { 535 return file_get_contents($filename); 536 } 537 return false; 538 } 539 540 /** 541 * 542 * @param string $url 543 * @return string 544 */ 545 public function tileUrlToFilename(string $url): string { 546 return $this->tileCacheBaseDir . "/" . substr($url, strpos($url, '/') + 1); 547 } 548 549 /** 550 * Write a tile into the cache. 551 * 552 * @param string $url 553 * @param mixed $data 554 */ 555 public function writeTileToCache($url, $data): void { 556 $filename = $this->tileUrlToFilename($url); 557 $this->mkdirRecursive(dirname($filename), 0777); 558 file_put_contents($filename, $data); 559 } 560 561 /** 562 * Recursively create the directory. 563 * 564 * @param string $pathname 565 * The directory path. 566 * @param int $mode 567 * File access mode. For more information on modes, read the details on the chmod manpage. 568 */ 569 public function mkdirRecursive(string $pathname, int $mode): bool { 570 is_dir(dirname($pathname)) || $this->mkdirRecursive(dirname($pathname), $mode); 571 return is_dir($pathname) || mkdir($pathname, $mode) || is_dir($pathname); 572 } 573 574 /** 575 * Place markers on the map and number them in the same order as they are listed in the html. 576 */ 577 public function placeMarkers(): void { 578 $count = 0; 579 $color = imagecolorallocate($this->image, 0, 0, 0); 580 $bgcolor = imagecolorallocate($this->image, 200, 200, 200); 581 $markerBaseDir = __DIR__ . '/icons'; 582 $markerImageOffsetX = 0; 583 $markerImageOffsetY = 0; 584 $markerShadowOffsetX = 0; 585 $markerShadowOffsetY = 0; 586 $markerShadowImg = null; 587 // loop thru marker array 588 foreach($this->markers as $marker) { 589 // set some local variables 590 $markerLat = $marker ['lat']; 591 $markerLon = $marker ['lon']; 592 $markerType = $marker ['type']; 593 // clear variables from previous loops 594 $markerFilename = ''; 595 $markerShadow = ''; 596 $matches = false; 597 // check for marker type, get settings from markerPrototypes 598 if($markerType) { 599 foreach($this->markerPrototypes as $markerPrototype) { 600 if(preg_match($markerPrototype ['regex'], $markerType, $matches)) { 601 $markerFilename = $matches [0] . $markerPrototype ['extension']; 602 if($markerPrototype ['offsetImage']) { 603 list ($markerImageOffsetX, $markerImageOffsetY) = explode( 604 ",", 605 $markerPrototype ['offsetImage'] 606 ); 607 } 608 $markerShadow = $markerPrototype ['shadow']; 609 if($markerShadow) { 610 list ($markerShadowOffsetX, $markerShadowOffsetY) = explode( 611 ",", 612 $markerPrototype ['offsetShadow'] 613 ); 614 } 615 } 616 } 617 } 618 // create img resource 619 if(file_exists($markerBaseDir . '/' . $markerFilename)) { 620 $markerImg = imagecreatefrompng($markerBaseDir . '/' . $markerFilename); 621 } else { 622 $markerImg = imagecreatefrompng($markerBaseDir . '/marker.png'); 623 } 624 // check for shadow + create shadow recource 625 if($markerShadow && file_exists($markerBaseDir . '/' . $markerShadow)) { 626 $markerShadowImg = imagecreatefrompng($markerBaseDir . '/' . $markerShadow); 627 } 628 // calc position 629 $destX = floor( 630 ($this->width / 2) - 631 $this->tileSize * ($this->centerX - $this->lonToTile($markerLon, $this->zoom)) 632 ); 633 $destY = floor( 634 ($this->height / 2) - 635 $this->tileSize * ($this->centerY - $this->latToTile($markerLat, $this->zoom)) 636 ); 637 // copy shadow on basemap 638 if($markerShadow && $markerShadowImg) { 639 imagecopy( 640 $this->image, 641 $markerShadowImg, 642 $destX + (int) $markerShadowOffsetX, 643 $destY + (int) $markerShadowOffsetY, 644 0, 645 0, 646 imagesx($markerShadowImg), 647 imagesy($markerShadowImg) 648 ); 649 } 650 // copy marker on basemap above shadow 651 imagecopy( 652 $this->image, 653 $markerImg, 654 $destX + (int) $markerImageOffsetX, 655 $destY + (int) $markerImageOffsetY, 656 0, 657 0, 658 imagesx($markerImg), 659 imagesy($markerImg) 660 ); 661 // add label 662 imagestring( 663 $this->image, 664 3, 665 $destX - imagesx($markerImg) + 1, 666 $destY + (int) $markerImageOffsetY + 1, 667 ++$count, 668 $bgcolor 669 ); 670 imagestring( 671 $this->image, 672 3, 673 $destX - imagesx($markerImg), 674 $destY + (int) $markerImageOffsetY, 675 $count, 676 $color 677 ); 678 } 679 } 680 681 /** 682 * Draw kml trace on the map. 683 * @throws exception when loading the KML fails 684 */ 685 public function drawKML(): void { 686 // TODO get colour from kml node (not currently supported in geoPHP) 687 $col = imagecolorallocatealpha($this->image, 255, 0, 0, .4 * 127); 688 $kmlgeom = geoPHP::load(file_get_contents($this->kmlFileName), 'kml'); 689 $this->drawGeometry($kmlgeom, $col); 690 } 691 692 /** 693 * Draw geometry or geometry collection on the map. 694 * 695 * @param Geometry $geom 696 * @param int $colour 697 * drawing colour 698 */ 699 private function drawGeometry(Geometry $geom, int $colour): void { 700 if(empty($geom)) { 701 return; 702 } 703 704 switch($geom->geometryType()) { 705 case 'GeometryCollection' : 706 // recursively draw part of the collection 707 for($i = 1; $i < $geom->numGeometries() + 1; $i++) { 708 $_geom = $geom->geometryN($i); 709 $this->drawGeometry($_geom, $colour); 710 } 711 break; 712 case 'MultiPolygon' : 713 case 'MultiLineString' : 714 case 'MultiPoint' : 715 // TODO implement / do nothing 716 break; 717 case 'Polygon' : 718 $this->drawPolygon($geom, $colour); 719 break; 720 case 'LineString' : 721 $this->drawLineString($geom, $colour); 722 break; 723 case 'Point' : 724 $this->drawPoint($geom, $colour); 725 break; 726 default : 727 // draw nothing 728 break; 729 } 730 } 731 732 /** 733 * Draw a polygon on the map. 734 * 735 * @param Polygon $polygon 736 * @param int $colour 737 * drawing colour 738 */ 739 private function drawPolygon($polygon, int $colour) { 740 // TODO implementation of drawing holes, 741 // maybe draw the polygon to an in-memory image and use imagecopy, draw polygon in col., draw holes in bgcol? 742 743 // print_r('Polygon:<br />'); 744 // print_r($polygon); 745 $extPoints = array(); 746 // extring is a linestring actually.. 747 $extRing = $polygon->exteriorRing(); 748 749 for($i = 1; $i < $extRing->numGeometries(); $i++) { 750 $p1 = $extRing->geometryN($i); 751 $x = floor( 752 ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom)) 753 ); 754 $y = floor( 755 ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom)) 756 ); 757 $extPoints [] = $x; 758 $extPoints [] = $y; 759 } 760 // print_r('points:('.($i-1).')<br />'); 761 // print_r($extPoints); 762 // imagepolygon ($this->image, $extPoints, $i-1, $colour ); 763 imagefilledpolygon($this->image, $extPoints, $i - 1, $colour); 764 } 765 766 /** 767 * Draw a line on the map. 768 * 769 * @param LineString $line 770 * @param int $colour 771 * drawing colour 772 */ 773 private function drawLineString($line, $colour) { 774 imagesetthickness($this->image, 2); 775 for($p = 1; $p < $line->numGeometries(); $p++) { 776 // get first pair of points 777 $p1 = $line->geometryN($p); 778 $p2 = $line->geometryN($p + 1); 779 // translate to paper space 780 $x1 = floor( 781 ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom)) 782 ); 783 $y1 = floor( 784 ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom)) 785 ); 786 $x2 = floor( 787 ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p2->x(), $this->zoom)) 788 ); 789 $y2 = floor( 790 ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p2->y(), $this->zoom)) 791 ); 792 // draw to image 793 imageline($this->image, $x1, $y1, $x2, $y2, $colour); 794 } 795 imagesetthickness($this->image, 1); 796 } 797 798 /** 799 * Draw a point on the map. 800 * 801 * @param Point $point 802 * @param int $colour 803 * drawing colour 804 */ 805 private function drawPoint($point, $colour) { 806 imagesetthickness($this->image, 2); 807 // translate to paper space 808 $cx = floor( 809 ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($point->x(), $this->zoom)) 810 ); 811 $cy = floor( 812 ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($point->y(), $this->zoom)) 813 ); 814 $r = 5; 815 // draw to image 816 // imageellipse($this->image, $cx, $cy,$r, $r, $colour); 817 imagefilledellipse($this->image, $cx, $cy, $r, $r, $colour); 818 // don't use imageellipse because the imagesetthickness function has 819 // no effect. So the better workaround is to use imagearc. 820 imagearc($this->image, $cx, $cy, $r, $r, 0, 359, $colour); 821 imagesetthickness($this->image, 1); 822 } 823 824 /** 825 * Draw gpx trace on the map. 826 * @throws exception when loading the GPX fails 827 */ 828 public function drawGPX() { 829 $col = imagecolorallocatealpha($this->image, 0, 0, 255, .4 * 127); 830 $gpxgeom = geoPHP::load(file_get_contents($this->gpxFileName), 'gpx'); 831 $this->drawGeometry($gpxgeom, $col); 832 } 833 834 /** 835 * Draw geojson on the map. 836 * @throws exception when loading the JSON fails 837 */ 838 public function drawGeojson() { 839 $col = imagecolorallocatealpha($this->image, 255, 0, 255, .4 * 127); 840 $gpxgeom = geoPHP::load(file_get_contents($this->geojsonFileName), 'json'); 841 $this->drawGeometry($gpxgeom, $col); 842 } 843 844 /** 845 * add copyright and origin notice and icons to the map. 846 */ 847 public function drawCopyright() { 848 $logoBaseDir = dirname(__FILE__) . '/' . 'logo/'; 849 $logoImg = imagecreatefrompng($logoBaseDir . $this->tileInfo ['openstreetmap'] ['logo']); 850 $textcolor = imagecolorallocate($this->image, 0, 0, 0); 851 $bgcolor = imagecolorallocate($this->image, 200, 200, 200); 852 853 imagecopy( 854 $this->image, 855 $logoImg, 856 0, 857 imagesy($this->image) - imagesy($logoImg), 858 0, 859 0, 860 imagesx($logoImg), 861 imagesy($logoImg) 862 ); 863 imagestring( 864 $this->image, 865 1, 866 imagesx($logoImg) + 2, 867 imagesy($this->image) - imagesy($logoImg) + 1, 868 $this->tileInfo ['openstreetmap'] ['txt'], 869 $bgcolor 870 ); 871 imagestring( 872 $this->image, 873 1, 874 imagesx($logoImg) + 1, 875 imagesy($this->image) - imagesy($logoImg), 876 $this->tileInfo ['openstreetmap'] ['txt'], 877 $textcolor 878 ); 879 880 // additional tile source info, ie. who created/hosted the tiles 881 $xIconOffset = 0; 882 if($this->maptype === 'openstreetmap') { 883 $mapAuthor = "(c) OpenStreetMap maps/CC BY-SA"; 884 } else { 885 $mapAuthor = $this->tileInfo [$this->maptype] ['txt']; 886 $iconImg = imagecreatefrompng($logoBaseDir . $this->tileInfo [$this->maptype] ['logo']); 887 $xIconOffset = imagesx($iconImg); 888 imagecopy( 889 $this->image, 890 $iconImg, imagesx($logoImg) + 1, 891 imagesy($this->image) - imagesy($iconImg), 892 0, 893 0, 894 imagesx($iconImg), imagesy($iconImg) 895 ); 896 } 897 imagestring( 898 $this->image, 899 1, imagesx($logoImg) + $xIconOffset + 4, 900 imagesy($this->image) - ceil(imagesy($logoImg) / 2) + 1, 901 $mapAuthor, 902 $bgcolor 903 ); 904 imagestring( 905 $this->image, 906 1, imagesx($logoImg) + $xIconOffset + 3, 907 imagesy($this->image) - ceil(imagesy($logoImg) / 2), 908 $mapAuthor, 909 $textcolor 910 ); 911 912 } 913} 914