1<?php 2 3/* 4 * Copyright (c) 2012 Mark C. Prins <mprins@users.sf.net> 5* 6* 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*/ 22include_once('geoPHP/geoPHP.inc'); 23/** 24 * @author Mark C. Prins <mprins@users.sf.net> 25 * @author Gerhard Koch <gerhard.koch AT ymail.com> 26 * 27 */ 28class StaticMap { 29 // these should probably not be changed 30 protected $tileSize = 256; 31 32 // the final output 33 var $doc = ''; 34 35 protected $tileInfo = array( 36 // OSM sources 37 'openstreetmap'=>array( 38 'txt'=>'(c) OpenStreetMap CC-BY-SA', 39 'logo'=>'osm_logo.png', 40 'url'=>'http://tile.openstreetmap.org/{Z}/{X}/{Y}.png'), 41 // cloudmade 42 'cloudmade' =>array( 43 'txt'=>'CloudMade tiles', 44 'logo'=>'cloudmade_logo.png', 45 'url'=> 'http://tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/2/256/{Z}/{X}/{Y}.png'), 46 'fresh' =>array( 47 'txt'=>'CloudMade tiles', 48 'logo'=>'cloudmade_logo.png', 49 'url'=> 'http://tile.cloudmade.com/BC9A493B41014CAABB98F0471D759707/997/256/{Z}/{X}/{Y}.png'), 50 // OCM sources 51 'cycle'=>array( 52 'txt'=>'OpenCycleMap tiles', 53 'logo'=>'cycle_logo.png', 54 'url'=>'http://tile.opencyclemap.org/cycle/{Z}/{X}/{Y}.png'), 55 'transport'=>array( 56 'txt'=>'OpenCycleMap tiles', 57 'logo'=>'cycle_logo.png', 58 'url'=>'http://tile2.opencyclemap.org/transport/{Z}/{X}/{Y}.png'), 59 'landscape'=>array( 60 'txt'=>'OpenCycleMap tiles', 61 'logo'=>'cycle_logo.png', 62 'url'=>'http://tile3.opencyclemap.org/landscape/{Z}/{X}/{Y}.png'), 63 // H&B sources 64 'hikeandbike'=>array( 65 'txt'=>'Hike & Bike Map', 66 'logo'=>'hnb_logo.png', 67 'url'=>'http://toolserver.org/tiles/hikebike/{Z}/{X}/{Y}.png'), 68 //'piste'=>array( 69 // 'txt'=>'OpenPisteMap tiles', 70 // 'logo'=>'piste_logo.png', 71 // 'url'=>''), 72 //'sea'=>array( 73 // 'txt'=>'OpenSeaMap tiles', 74 // 'logo'=>'sea_logo.png', 75 // 'url'=>''), 76 // MapQuest 77 'mapquest'=>array( 78 'txt'=>'MapQuest tiles', 79 'logo'=>'mq_logo.png', 80 'url'=>'http://otile3.mqcdn.com/tiles/1.0.0/osm/{Z}/{X}/{Y}.png') 81 ); 82 protected $tileDefaultSrc = 'openstreetmap'; 83 84 // set up markers 85 protected $markerPrototypes = array( 86 // found at http://www.mapito.net/map-marker-icons.html 87 // these are 17x19 px with a pointer at the bottom left 88 'lighblue' => array('regex'=>'/^lightblue([0-9]+)$/', 89 'extension'=>'.png', 90 'shadow'=>false, 91 'offsetImage'=>'0,-19', 92 'offsetShadow'=>false 93 ), 94 // openlayers std markers are 21x25px with shadow 95 'ol-marker'=> array('regex'=>'/^marker(|-blue|-gold|-green|-red)+$/', 96 'extension'=>'.png', 97 'shadow'=>'marker_shadow.png', 98 'offsetImage'=>'-10,-25', 99 'offsetShadow'=>'-1,-13' 100 ), 101 // these are 16x16 px 102 'ww_icon'=> array('regex'=>'/ww_\S+$/', 103 'extension'=>'.png', 104 'shadow'=>false, 105 'offsetImage'=>'-8,-8', 106 'offsetShadow'=>false 107 ), 108 // assume these are 16x16 px 109 'rest' => array('regex'=>'/^(?!lightblue([0-9]+)$)(?!(ww_\S+$))(?!marker(|-blue|-gold|-green|-red)+$)(.*)/', 110 'extension'=>'.png', 111 'shadow'=>'marker_shadow.png', 112 'offsetImage'=>'-8,-8', 113 'offsetShadow'=>'-1,-1' 114 ) 115 ); 116 protected $centerX, $centerY, $offsetX, $offsetY, $image; 117 protected $zoom, $lat, $lon, $width, $height, $markers, $maptype, $kmlFileName, $gpxFileName; 118 protected $tileCacheBaseDir, $mapCacheBaseDir, $mediaBaseDir; 119 protected $useTileCache = true; 120 protected $mapCacheID = ''; 121 protected $mapCacheFile = ''; 122 protected $mapCacheExtension = 'png'; 123 124 /** 125 * 126 * @param number $lat 127 * @param number $lon 128 * @param number $zoom 129 * @param number $width 130 * @param number $height 131 * @param string $maptype 132 * @param mixed $markers 133 * @param string $gpx 134 * @param string $kml 135 * @param string $mediaDir 136 * @param string $tileCacheBaseDir 137 */ 138 public function __construct($lat,$lon,$zoom,$width,$height,$maptype, $markers,$gpx,$kml,$mediaDir,$tileCacheBaseDir){ 139 $this->zoom = $zoom; 140 $this->lat = $lat; 141 $this->lon = $lon; 142 $this->width = $width; 143 $this->height = $height; 144 $this->markers = $markers; 145 $this->mediaBaseDir = $mediaDir; 146 // validate + set maptype 147 $this->maptype = $this->tileDefaultSrc; 148 if(array_key_exists($maptype,$this->tileInfo)) { 149 $this->maptype = $maptype; 150 } 151 152 $this->tileCacheBaseDir= $tileCacheBaseDir.'/olmaptiles'; 153 $this->useTileCache = $this->tileCacheBaseDir !==''; 154 $this->mapCacheBaseDir = $mediaDir.'/olmapmaps'; 155 156 $this->kmlFileName = $kml; 157 $this->gpxFileName = $gpx; 158 } 159 160 /** 161 * 162 * @param number $long 163 * @param number $zoom 164 */ 165 public function lonToTile($long, $zoom){ 166 return (($long + 180) / 360) * pow(2, $zoom); 167 } 168 /** 169 * 170 * @param number $lat 171 * @param number $zoom 172 * @return number 173 */ 174 public function latToTile($lat, $zoom){ 175 return (1 - log(tan($lat * pi()/180) + 1 / cos($lat* pi()/180)) / pi()) /2 * pow(2, $zoom); 176 } 177 /** 178 * 179 */ 180 public function initCoords(){ 181 $this->centerX = $this->lonToTile($this->lon, $this->zoom); 182 $this->centerY = $this->latToTile($this->lat, $this->zoom); 183 $this->offsetX = floor((floor($this->centerX)-$this->centerX)*$this->tileSize); 184 $this->offsetY = floor((floor($this->centerY)-$this->centerY)*$this->tileSize); 185 } 186 187 /** 188 * make basemap image. 189 */ 190 public function createBaseMap(){ 191 $this->image = imagecreatetruecolor($this->width, $this->height); 192 $startX = floor($this->centerX-($this->width/$this->tileSize)/2); 193 $startY = floor($this->centerY-($this->height/$this->tileSize)/2); 194 $endX = ceil($this->centerX+($this->width/$this->tileSize)/2); 195 $endY = ceil($this->centerY+($this->height/$this->tileSize)/2); 196 $this->offsetX = -floor(($this->centerX-floor($this->centerX))*$this->tileSize); 197 $this->offsetY = -floor(($this->centerY-floor($this->centerY))*$this->tileSize); 198 $this->offsetX += floor($this->width/2); 199 $this->offsetY += floor($this->height/2); 200 $this->offsetX += floor($startX-floor($this->centerX))*$this->tileSize; 201 $this->offsetY += floor($startY-floor($this->centerY))*$this->tileSize; 202 203 for($x=$startX; $x<=$endX; $x++){ 204 for($y=$startY; $y<=$endY; $y++){ 205 $url = str_replace(array('{Z}','{X}','{Y}'),array($this->zoom, $x, $y), $this->tileInfo[$this->maptype]['url']); 206 $tileData = $this->fetchTile($url); 207 if($tileData){ 208 $tileImage = imagecreatefromstring($tileData); 209 } else { 210 $tileImage = imagecreate($this->tileSize,$this->tileSize); 211 $color = imagecolorallocate($tileImage, 255, 255, 255); 212 @imagestring($tileImage,1,127,127,'err',$color); 213 } 214 $destX = ($x-$startX)*$this->tileSize+$this->offsetX; 215 $destY = ($y-$startY)*$this->tileSize+$this->offsetY; 216 imagecopy($this->image, $tileImage, $destX, $destY, 0, 0, $this->tileSize, $this->tileSize); 217 } 218 } 219 } 220 221 /** 222 * Place markers on the map and number them in the same order as they are listed in the html. 223 */ 224 public function placeMarkers(){ 225 $count=0; 226 $color=imagecolorallocate ($this->image,0,0,0 ); 227 $bgcolor=imagecolorallocate ($this->image,200,200,200 ); 228 $markerBaseDir = dirname(__FILE__).'/icons'; 229 // loop thru marker array 230 foreach($this->markers as $marker){ 231 // set some local variables 232 $markerLat = $marker['lat']; 233 $markerLon = $marker['lon']; 234 $markerType = $marker['type']; 235 // clear variables from previous loops 236 $markerFilename = ''; 237 $markerShadow = ''; 238 $matches = false; 239 // check for marker type, get settings from markerPrototypes 240 if($markerType){ 241 foreach($this->markerPrototypes as $markerPrototype){ 242 if(preg_match($markerPrototype['regex'],$markerType,$matches)){ 243 $markerFilename = $matches[0].$markerPrototype['extension']; 244 if($markerPrototype['offsetImage']){ 245 list($markerImageOffsetX, $markerImageOffsetY) = split(",",$markerPrototype['offsetImage']); 246 } 247 $markerShadow = $markerPrototype['shadow']; 248 if($markerShadow){ 249 list($markerShadowOffsetX, $markerShadowOffsetY) = split(",",$markerPrototype['offsetShadow']); 250 } 251 } 252 } 253 } 254 // create img resource 255 if(file_exists($markerBaseDir.'/'.$markerFilename)){ 256 $markerImg = imagecreatefrompng($markerBaseDir.'/'.$markerFilename); 257 } else { 258 $markerImg = imagecreatefrompng($markerBaseDir.'/marker.png'); 259 } 260 // check for shadow + create shadow recource 261 if($markerShadow && file_exists($markerBaseDir.'/'.$markerShadow)){ 262 $markerShadowImg = imagecreatefrompng($markerBaseDir.'/'.$markerShadow); 263 } 264 // calc position 265 $destX = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($markerLon, $this->zoom))); 266 $destY = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($markerLat, $this->zoom))); 267 // copy shadow on basemap 268 if($markerShadow && $markerShadowImg){ 269 imagecopy($this->image, $markerShadowImg, $destX+intval($markerShadowOffsetX), $destY+intval($markerShadowOffsetY), 270 0, 0, imagesx($markerShadowImg), imagesy($markerShadowImg)); 271 } 272 // copy marker on basemap above shadow 273 imagecopy($this->image, $markerImg, $destX+intval($markerImageOffsetX), $destY+intval($markerImageOffsetY), 274 0, 0, imagesx($markerImg), imagesy($markerImg)); 275 // add label 276 imagestring ($this->image , 3 , $destX-imagesx($markerImg)+1 , $destY+intval($markerImageOffsetY)+1 , ++$count , $bgcolor ); 277 imagestring ($this->image , 3 , $destX-imagesx($markerImg) , $destY+intval($markerImageOffsetY) , $count , $color ); 278 }; 279 } 280 /** 281 * 282 * @param string $url 283 * @return string 284 */ 285 public function tileUrlToFilename($url){ 286 return $this->tileCacheBaseDir."/".str_replace(array('http://'),'',$url); 287 } 288 /** 289 * 290 * @param string $url 291 */ 292 public function checkTileCache($url){ 293 $filename = $this->tileUrlToFilename($url); 294 if(file_exists($filename)){ 295 return file_get_contents($filename); 296 } 297 } 298 299 public function checkMapCache(){ 300 $this->mapCacheID = md5($this->serializeParams()); 301 $filename = $this->mapCacheIDToFilename(); 302 if(file_exists($filename)) return true; 303 } 304 305 public function serializeParams(){ 306 return join("&",array($this->zoom,$this->lat,$this->lon,$this->width,$this->height, serialize($this->markers),$this->maptype)); 307 } 308 309 public function mapCacheIDToFilename(){ 310 if(!$this->mapCacheFile){ 311 $this->mapCacheFile = $this->mapCacheBaseDir."/".$this->maptype."/".$this->zoom."/cache_".substr($this->mapCacheID,0,2)."/".substr($this->mapCacheID,2,2)."/".substr($this->mapCacheID,4); 312 } 313 return $this->mapCacheFile.".".$this->mapCacheExtension; 314 } 315 316 public function mkdir_recursive($pathname, $mode){ 317 is_dir(dirname($pathname)) || $this->mkdir_recursive(dirname($pathname), $mode); 318 return is_dir($pathname) || @mkdir($pathname, $mode); 319 } 320 321 public function writeTileToCache($url, $data){ 322 $filename = $this->tileUrlToFilename($url); 323 $this->mkdir_recursive(dirname($filename),0777); 324 file_put_contents($filename, $data); 325 } 326 327 public function fetchTile($url){ 328 if($this->useTileCache && ($cached = $this->checkTileCache($url))) return $cached; 329 $ch = curl_init(); 330 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 331 curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; DokuWikiSpatial HTTP Client; '.PHP_OS.')'); 332 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); 333 curl_setopt($ch, CURLOPT_URL, $url); 334 $tile = curl_exec($ch); 335 curl_close($ch); 336 if($tile && $this->useTileCache){ 337 $this->writeTileToCache($url,$tile); 338 } 339 return $tile; 340 } 341 342 /** 343 * Draw gpx trace on the map. 344 */ 345 public function drawGPX(){ 346 $gpxgeom = geoPHP::load(file_get_contents($this->gpxFileName),'gpx'); 347 $col = imagecolorallocate($this->image, 0, 0, 255); 348 349 for ($i = 1; $i < $gpxgeom->numGeometries()+1; $i++) { 350 $geom = $gpxgeom->geometryN($i); 351 // can be Point or LineString 352 switch ($geom->geometryType()) { 353 case 'LineString': 354 $this->drawLineString($geom, $col); 355 break; 356 case 'Point': 357 $this->drawPoint($geom, $col); 358 break; 359 default: 360 //do nothing 361 break; 362 } 363 } 364 } 365 366 /** 367 * Draw kml trace on the map. 368 */ 369 public function drawKML(){ 370 //TODO implementation 371 $kmlgeom = geoPHP::load(file_get_contents($this->kmlFileName),'kml'); 372 // TODO get colour from kml node 373 $col = imagecolorallocate($this->image, 255, 0, 0); 374 375 for ($i = 1; $i < $kmlgeom->numGeometries()+1; $i++) { 376 $geom = $kmlgeom->geometryN($i); 377 switch ($geom->geometryType()) { 378 case 'LineString': 379 $this->drawLineString($geom, $col); 380 break; 381 case 'Point': 382 $this->drawPoint($geom, $col); 383 break; 384 case 'Polygon': 385 break; 386 default: 387 //do nothing 388 break; 389 } 390 } 391 } 392 393 private function drawLineString($line, $colour){ 394 for ($p = 1; $p < $line->numGeometries(); $p++) { 395 // get first pair of points 396 $p1 = $line->geometryN($p); 397 $p2 = $line->geometryN($p+1); 398 // translate to paper space 399 $x1 = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($p1->x(), $this->zoom))); 400 $y1 = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($p1->y(), $this->zoom))); 401 $x2 = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($p2->x(), $this->zoom))); 402 $y2 = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($p2->y(), $this->zoom))); 403 // draw to image 404 imageline ( $this->image , $x1 , $y1 , $x2 , $y2 , $colour ); 405 } 406 } 407 408 private function drawPoint($point, $colour){ 409 // translate to paper space 410 $cx = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($point->x(), $this->zoom))); 411 $cy = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($point->y(), $this->zoom))); 412 // draw to image 413 imageellipse ( $this->image , $cx , $cy , 14 /*width*/ , 14 /*height*/ , $colour ); 414 } 415 416 /** 417 * add copyright and origin notice and icons to the map. 418 */ 419 public function drawCopyright(){ 420 $logoBaseDir = dirname(__FILE__).'/'.'logo/'; 421 $logoImg = imagecreatefrompng($logoBaseDir.$this->tileInfo['openstreetmap']['logo']); 422 $textcolor = imagecolorallocate($this->image, 0, 0, 0); 423 $bgcolor = imagecolorallocate($this->image, 200, 200, 200); 424 425 imagecopy($this->image, 426 $logoImg, 427 0, 428 imagesy($this->image)-imagesy($logoImg), 429 0, 430 0, 431 imagesx($logoImg), 432 imagesy($logoImg) 433 ); 434 imagestring ($this->image , 1 , imagesx($logoImg)+2 , imagesy($this->image)-imagesy($logoImg)+1 , $this->tileInfo['openstreetmap']['txt'],$bgcolor ); 435 imagestring ($this->image , 1 , imagesx($logoImg)+1 , imagesy($this->image)-imagesy($logoImg) , $this->tileInfo['openstreetmap']['txt'] ,$textcolor ); 436 437 // additional tile source info, ie. who created/hosted the tiles 438 if ($this->maptype!='openstreetmap') { 439 $iconImg = imagecreatefrompng($logoBaseDir.$this->tileInfo[$this->maptype]['logo']); 440 imagecopy($this->image, 441 $iconImg, 442 imagesx($logoImg)+1, 443 imagesy($this->image)-imagesy($iconImg), 444 0, 445 0, 446 imagesx($iconImg), 447 imagesy($iconImg) 448 ); 449 imagestring ($this->image , 1 , imagesx($logoImg)+imagesx($iconImg)+4 , imagesy($this->image)-ceil(imagesy($logoImg)/2)+1 , $this->tileInfo[$this->maptype]['txt'],$bgcolor ); 450 imagestring ($this->image , 1 , imagesx($logoImg)+imagesx($iconImg)+3 , imagesy($this->image)-ceil(imagesy($logoImg)/2) , $this->tileInfo[$this->maptype]['txt'] ,$textcolor ); 451 } 452 } 453 /** 454 * make the map. 455 */ 456 public function makeMap(){ 457 $this->initCoords(); 458 $this->createBaseMap(); 459 if(count($this->markers))$this->placeMarkers(); 460 if(file_exists($this->kmlFileName)) $this->drawKML(); 461 if(file_exists($this->gpxFileName)) $this->drawGPX(); 462 $this->drawCopyright(); 463 } 464 /** 465 * get the map, this may return a reference to a cached copy. 466 * @return string url relative to media dir 467 */ 468 public function getMap(){ 469 // use map cache, so check cache for map 470 if(!$this->checkMapCache()){ 471 // map is not in cache, needs to be build 472 $this->makeMap(); 473 $this->mkdir_recursive(dirname($this->mapCacheIDToFilename()),0777); 474 imagepng($this->image,$this->mapCacheIDToFilename(),9); 475 } 476 $this->doc =$this->mapCacheIDToFilename(); 477 // make url relative to media dir 478 return str_replace($this->mediaBaseDir, '', $this->doc); 479 } 480} 481