Passed
Push — master ( c23d30...f204b8 )
by Mark
02:05
created

StaticMap.php (5 issues)

Labels
Severity
1
<?php
2
3
/*
4
 * Copyright (c) 2012-2023 Mark C. Prins <[email protected]>
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
 */
22
namespace dokuwiki\plugin\openlayersmap;
23
24
use geoPHP\Geometry\Geometry;
25
use geoPHP\Geometry\GeometryCollection;
26
use geoPHP\Geometry\LineString;
27
use geoPHP\Geometry\Point;
28
use geoPHP\Geometry\Polygon;
29
use geoPHP\geoPHP;
30
31
/**
32
 *
33
 * @author Mark C. Prins <[email protected]>
34
 * @author Gerhard Koch <gerhard.koch AT ymail.com>
35
 *
36
 */
37
class 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);
0 ignored issues
show
The function dbglog was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

173
            /** @scrutinizer ignore-call */ 
174
            dbglog($e);
Loading history...
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");
0 ignored issues
show
The function dbglog was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

224
            /** @scrutinizer ignore-call */ 
225
            dbglog($geoms, "StaticMap::autoZoom: Skip setting autozoom options");
Loading history...
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);
0 ignored issues
show
The function dbglog was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

301
                /** @scrutinizer ignore-call */ 
302
                dbglog('failed to load KML file', $e);
Loading history...
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");
0 ignored issues
show
The function dbglog was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

389
                /** @scrutinizer ignore-call */ 
390
                dbglog($this->tileSize, "imagecopy tile into image: $destX, $destY");
Loading history...
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");
0 ignored issues
show
The function dbglog was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

424
            /** @scrutinizer ignore-call */ 
425
            dbglog("StaticMap::fetchTile: getting: $url using curl_exec");
Loading history...
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 'Polygon':
643
                $this->drawPolygon($geom, $colour);
644
                break;
645
            case 'LineString':
646
                $this->drawLineString($geom, $colour);
647
                break;
648
            case 'Point':
649
                $this->drawPoint($geom, $colour);
650
                break;
651
            // TODO implement / do nothing
652
            case 'MultiPolygon':
653
            case 'MultiLineString':
654
            case 'MultiPoint':
655
            default:
656
                // draw nothing
657
                break;
658
        }
659
    }
660
661
    /**
662
     * Draw a polygon on the map.
663
     *
664
     * @param Polygon $polygon
665
     * @param int     $colour
666
     *            drawing colour
667
     */
668
    private function drawPolygon($polygon, int $colour)
669
    {
670
        // TODO implementation of drawing holes,
671
        // maybe draw the polygon to an in-memory image and use imagecopy, draw polygon in col., draw holes in bgcol?
672
673
        // print_r('Polygon:<br />');
674
        // print_r($polygon);
675
        $extPoints = [];
676
        // extring is a linestring actually..
677
        $extRing = $polygon->exteriorRing();
678
679
        for ($i = 1; $i < $extRing->numGeometries(); $i++) {
680
            $p1           = $extRing->geometryN($i);
681
            $x            = floor(
682
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom))
683
            );
684
            $y            = floor(
685
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom))
686
            );
687
            $extPoints [] = $x;
688
            $extPoints [] = $y;
689
        }
690
        // print_r('points:('.($i-1).')<br />');
691
        // print_r($extPoints);
692
        // imagepolygon ($this->image, $extPoints, $i-1, $colour );
693
        imagefilledpolygon($this->image, $extPoints, $i - 1, $colour);
694
    }
695
696
    /**
697
     * Draw a line on the map.
698
     *
699
     * @param LineString $line
700
     * @param int        $colour
701
     *            drawing colour
702
     */
703
    private function drawLineString($line, $colour)
704
    {
705
        imagesetthickness($this->image, 2);
706
        for ($p = 1; $p < $line->numGeometries(); $p++) {
707
            // get first pair of points
708
            $p1 = $line->geometryN($p);
709
            $p2 = $line->geometryN($p + 1);
710
            // translate to paper space
711
            $x1 = floor(
712
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom))
713
            );
714
            $y1 = floor(
715
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom))
716
            );
717
            $x2 = floor(
718
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p2->x(), $this->zoom))
719
            );
720
            $y2 = floor(
721
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p2->y(), $this->zoom))
722
            );
723
            // draw to image
724
            imageline($this->image, $x1, $y1, $x2, $y2, $colour);
725
        }
726
        imagesetthickness($this->image, 1);
727
    }
728
729
    /**
730
     * Draw a point on the map.
731
     *
732
     * @param Point $point
733
     * @param int   $colour
734
     *            drawing colour
735
     */
736
    private function drawPoint($point, $colour)
737
    {
738
        imagesetthickness($this->image, 2);
739
        // translate to paper space
740
        $cx = floor(
741
            ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($point->x(), $this->zoom))
742
        );
743
        $cy = floor(
744
            ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($point->y(), $this->zoom))
745
        );
746
        $r  = 5;
747
        // draw to image
748
        // imageellipse($this->image, $cx, $cy,$r, $r, $colour);
749
        imagefilledellipse($this->image, $cx, $cy, $r, $r, $colour);
750
        // don't use imageellipse because the imagesetthickness function has
751
        // no effect. So the better workaround is to use imagearc.
752
        imagearc($this->image, $cx, $cy, $r, $r, 0, 359, $colour);
753
        imagesetthickness($this->image, 1);
754
    }
755
756
    /**
757
     * Draw gpx trace on the map.
758
     * @throws exception when loading the GPX fails
759
     */
760
    public function drawGPX()
761
    {
762
        $col     = imagecolorallocatealpha($this->image, 0, 0, 255, .4 * 127);
763
        $gpxgeom = geoPHP::load(file_get_contents($this->gpxFileName), 'gpx');
764
        $this->drawGeometry($gpxgeom, $col);
765
    }
766
767
    /**
768
     * Draw geojson on the map.
769
     * @throws exception when loading the JSON fails
770
     */
771
    public function drawGeojson()
772
    {
773
        $col     = imagecolorallocatealpha($this->image, 255, 0, 255, .4 * 127);
774
        $gpxgeom = geoPHP::load(file_get_contents($this->geojsonFileName), 'json');
775
        $this->drawGeometry($gpxgeom, $col);
776
    }
777
778
    /**
779
     * add copyright and origin notice and icons to the map.
780
     */
781
    public function drawCopyright()
782
    {
783
        $logoBaseDir = __DIR__ . '/' . 'logo/';
784
        $logoImg     = imagecreatefrompng($logoBaseDir . $this->tileInfo ['openstreetmap'] ['logo']);
785
        $textcolor   = imagecolorallocate($this->image, 0, 0, 0);
786
        $bgcolor     = imagecolorallocate($this->image, 200, 200, 200);
787
788
        imagecopy(
789
            $this->image,
790
            $logoImg,
791
            0,
792
            imagesy($this->image) - imagesy($logoImg),
793
            0,
794
            0,
795
            imagesx($logoImg),
796
            imagesy($logoImg)
797
        );
798
        imagestring(
799
            $this->image,
800
            1,
801
            imagesx($logoImg) + 2,
802
            imagesy($this->image) - imagesy($logoImg) + 1,
803
            $this->tileInfo ['openstreetmap'] ['txt'],
804
            $bgcolor
805
        );
806
        imagestring(
807
            $this->image,
808
            1,
809
            imagesx($logoImg) + 1,
810
            imagesy($this->image) - imagesy($logoImg),
811
            $this->tileInfo ['openstreetmap'] ['txt'],
812
            $textcolor
813
        );
814
815
        // additional tile source info, ie. who created/hosted the tiles
816
        $xIconOffset = 0;
817
        if ($this->maptype === 'openstreetmap') {
818
            $mapAuthor = "(c) OpenStreetMap maps/CC BY-SA";
819
        } else {
820
            $mapAuthor   = $this->tileInfo [$this->maptype] ['txt'];
821
            $iconImg     = imagecreatefrompng($logoBaseDir . $this->tileInfo [$this->maptype] ['logo']);
822
            $xIconOffset = imagesx($iconImg);
823
            imagecopy(
824
                $this->image,
825
                $iconImg,
826
                imagesx($logoImg) + 1,
827
                imagesy($this->image) - imagesy($iconImg),
828
                0,
829
                0,
830
                imagesx($iconImg),
831
                imagesy($iconImg)
832
            );
833
        }
834
        imagestring(
835
            $this->image,
836
            1,
837
            imagesx($logoImg) + $xIconOffset + 4,
838
            imagesy($this->image) - ceil(imagesy($logoImg) / 2) + 1,
839
            $mapAuthor,
840
            $bgcolor
841
        );
842
        imagestring(
843
            $this->image,
844
            1,
845
            imagesx($logoImg) + $xIconOffset + 3,
846
            imagesy($this->image) - ceil(imagesy($logoImg) / 2),
847
            $mapAuthor,
848
            $textcolor
849
        );
850
    }
851
}
852