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