xref: /plugin/openlayersmap/StaticMap.php (revision c977deac72b89fc7d75e4570621667358cb59a53)
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		$col = imagecolorallocatealpha($this->image, 0, 0, 255, .4*127);
347		$gpxgeom = geoPHP::load(file_get_contents($this->gpxFileName),'gpx');
348		$this->drawGeometry($gpxgeom, $col);
349	}
350
351	/**
352	 * Draw kml trace on the map.
353	 */
354	public function drawKML(){
355		// TODO get colour from kml node
356		$col = imagecolorallocatealpha($this->image, 255, 0, 0, .4*127);
357		//	if ($col === FALSE) {
358		//		$col = imagecolorallocate($this->image, 255, 0, 0);
359		//	}
360		$kmlgeom = geoPHP::load(file_get_contents($this->kmlFileName),'kml');
361		$this->drawGeometry($kmlgeom, $col);
362	}
363	/**
364	 * Draw geometry or geometry collection on the map.
365	 * @param Geometry $geom
366	 * @param int $colour drawing colour
367	 */
368	private function drawGeometry($geom, $colour){
369		switch ($geom->geometryType()) {
370			case 'GeometryCollection':
371				// recursively draw part of the collection
372				for ($i = 1; $i < $geom->numGeometries()+1; $i++) {
373					$_geom = $geom->geometryN($i);
374					$this->drawGeometry($_geom, $colour);
375				}
376				break;
377			case 'MultiPolygon':
378				// TODO implement / do nothing
379				break;
380			case 'MultiLineString':
381				// TODO implement / do nothing
382				break;
383			case 'MultiPoint':
384				// TODO implement / do nothing
385				break;
386			case 'Polygon':
387				$this->drawPolygon($geom, $colour);
388				break;
389			case 'LineString':
390				$this->drawLineString($geom, $colour);
391				break;
392			case 'Point':
393				$this->drawPoint($geom, $colour);
394				break;
395			default:
396				//do nothing
397				break;
398		}
399	}
400
401	/**
402	 * Draw a line on the map.
403	 * @param LineString $line
404	 * @param int $colour drawing colour
405	 */
406	private function drawLineString($line, $colour){
407		imagesetthickness($this->image,3);
408		for ($p = 1; $p < $line->numGeometries(); $p++) {
409			// get first pair of points
410			$p1 = $line->geometryN($p);
411			$p2 = $line->geometryN($p+1);
412			// translate to paper space
413			$x1 = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($p1->x(), $this->zoom)));
414			$y1 = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($p1->y(), $this->zoom)));
415			$x2 = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($p2->x(), $this->zoom)));
416			$y2 = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($p2->y(), $this->zoom)));
417			// draw to image
418			imageline ( $this->image, $x1, $y1, $x2, $y2, $colour);
419		}
420		imagesetthickness($this->image,1);
421	}
422
423	/**
424	 * Draw a point on the map.
425	 * @param Point $point
426	 * @param int $colour drawing colour
427	 */
428	private function drawPoint($point, $colour){
429		imagesetthickness($this->image,2);
430		// translate to paper space
431		$cx = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($point->x(), $this->zoom)));
432		$cy = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($point->y(), $this->zoom)));
433		// draw to image
434		// imageellipse($this->image, $cx, $cy,14 /*width*/, 14/*height*/, $colour);
435		imagefilledellipse ($this->image, $cx, $cy, 5, 5, $colour);
436		// We don't use imageellipse because the imagesetthickness function has
437		// no effect. So the better workaround is to use imagearc.
438		imagearc($this->image, $cx, $cy, 5, 5, 0, 359, $colour);
439		imagesetthickness($this->image,1);
440	}
441
442	/**
443	 * Draw a polygon on the map.
444	 * @param Polygon $polygon
445	 * @param int $colour drawing colour
446	 */
447	private function drawPolygon($polygon, $colour){
448		//TODO implementation of drawing holes,
449		// maybe draw the polygon to an in-memory image and use imagecopy, draw polygon in col., draw holes in bgcol?
450
451		//print_r('Polygon:<br />');
452		//print_r($polygon);
453
454		$extPoints = array();
455		// extring is a linestring actually..
456		$extRing = $polygon->exteriorRing();
457
458		for ($i = 1; $i < $extRing->numGeometries(); $i++) {
459			$p1 = $extRing->geometryN($i);
460			$x = floor(($this->width/2)-$this->tileSize*($this->centerX-$this->lonToTile($p1->x(), $this->zoom)));
461			$y = floor(($this->height/2)-$this->tileSize*($this->centerY-$this->latToTile($p1->y(), $this->zoom)));
462			$extPoints[]=$x;
463			$extPoints[]=$y;
464		}
465		//print_r('points:('.($i-1).')<br />');
466		//print_r($extPoints);
467		//imagepolygon ($this->image, $extPoints, $i-1, $colour );
468		imagefilledpolygon($this->image, $extPoints, $i-1, $colour );
469	}
470
471	/**
472	 * add copyright and origin notice and icons to the map.
473	 */
474	public function drawCopyright(){
475		$logoBaseDir = dirname(__FILE__).'/'.'logo/';
476		$logoImg = imagecreatefrompng($logoBaseDir.$this->tileInfo['openstreetmap']['logo']);
477		$textcolor = imagecolorallocate($this->image, 0, 0, 0);
478		$bgcolor = imagecolorallocate($this->image, 200, 200, 200);
479
480		imagecopy($this->image,
481				$logoImg,
482				0,
483				imagesy($this->image)-imagesy($logoImg),
484				0,
485				0,
486				imagesx($logoImg),
487				imagesy($logoImg)
488		);
489		imagestring ($this->image , 1 , imagesx($logoImg)+2 , imagesy($this->image)-imagesy($logoImg)+1 , $this->tileInfo['openstreetmap']['txt'],$bgcolor );
490		imagestring ($this->image , 1 , imagesx($logoImg)+1 , imagesy($this->image)-imagesy($logoImg) , $this->tileInfo['openstreetmap']['txt'] ,$textcolor );
491
492		// additional tile source info, ie. who created/hosted the tiles
493		if ($this->maptype!='openstreetmap') {
494			$iconImg = imagecreatefrompng($logoBaseDir.$this->tileInfo[$this->maptype]['logo']);
495			imagecopy($this->image,
496					$iconImg,
497					imagesx($logoImg)+1,
498					imagesy($this->image)-imagesy($iconImg),
499					0,
500					0,
501					imagesx($iconImg),
502					imagesy($iconImg)
503			);
504			imagestring ($this->image , 1 , imagesx($logoImg)+imagesx($iconImg)+4 , imagesy($this->image)-ceil(imagesy($logoImg)/2)+1 , $this->tileInfo[$this->maptype]['txt'],$bgcolor );
505			imagestring ($this->image , 1 , imagesx($logoImg)+imagesx($iconImg)+3 , imagesy($this->image)-ceil(imagesy($logoImg)/2) , $this->tileInfo[$this->maptype]['txt'] ,$textcolor );
506		}
507	}
508	/**
509	 * make the map.
510	 */
511	public function makeMap(){
512		$this->initCoords();
513		$this->createBaseMap();
514		if(count($this->markers))$this->placeMarkers();
515		if(file_exists($this->kmlFileName)) $this->drawKML();
516		if(file_exists($this->gpxFileName)) $this->drawGPX();
517		$this->drawCopyright();
518	}
519	/**
520	 * get the map, this may return a reference to a cached copy.
521	 * @return string url relative to media dir
522	 */
523	public function getMap(){
524		// use map cache, so check cache for map
525		if(!$this->checkMapCache()){
526			// map is not in cache, needs to be build
527			$this->makeMap();
528			$this->mkdir_recursive(dirname($this->mapCacheIDToFilename()),0777);
529			imagepng($this->image,$this->mapCacheIDToFilename(),9);
530		}
531		$this->doc =$this->mapCacheIDToFilename();
532		// make url relative to media dir
533		return str_replace($this->mediaBaseDir, '', $this->doc);
534	}
535}
536