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