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