Passed
Pull Request — master (#31)
by Mark
02:24
created

StaticMap::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 18
c 2
b 0
f 0
dl 0
loc 36
rs 9.6666
cc 2
nc 2
nop 14

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
/*
3
 * Copyright (c) 2012-2018 Mark C. Prins <[email protected]>
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
 */
21
22
use geoPHP\Geometry\Geometry;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Geometry was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use geoPHP\Geometry\GeometryCollection;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\GeometryCollection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use geoPHP\Geometry\LineString;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\LineString was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use geoPHP\Geometry\Point;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
use geoPHP\Geometry\Polygon;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Polygon was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use geoPHP\geoPHP;
0 ignored issues
show
Bug introduced by
The type geoPHP\geoPHP was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
29
// phpcs:disable PSR1.Files.SideEffects
30
// TODO resolve side effect
31
require_once __DIR__ . '/../geophp/vendor/autoload.php';
32
33
/**
34
 *
35
 * @author Mark C. Prins <[email protected]>
36
 * @author Gerhard Koch <gerhard.koch AT ymail.com>
37
 *
38
 */
39
class StaticMap {
40
41
    // the final output
42
    private $tileSize = 256;
43
    private $tileInfo = array(
44
        // OSM sources
45
        'openstreetmap' => array(
46
            'txt'  => '(c) OpenStreetMap data/ODbl',
47
            'logo' => 'osm_logo.png',
48
            'url'  => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png'
49
        ),
50
        // OpenTopoMap sources
51
        'opentopomap' => array(
52
            'txt'  => '(c) OpenStreetMap data/ODbl, SRTM | style: (c) OpenTopoMap',
53
            'logo' => 'osm_logo.png',
54
            'url'  => 'https:/tile.opentopomap.org/{Z}/{X}/{Y}.png'
55
        ),
56
        // OCM sources
57
        'cycle'         => array(
58
            'txt'  => '(c) Thunderforest maps',
59
            'logo' => 'tf_logo.png',
60
            'url'  => 'https://tile.thunderforest.com/cycle/{Z}/{X}/{Y}.png'
61
        ),
62
        'transport'     => array(
63
            'txt'  => '(c) Thunderforest maps',
64
            'logo' => 'tf_logo.png',
65
            'url'  => 'https://tile.thunderforest.com/transport/{Z}/{X}/{Y}.png'
66
        ),
67
        'landscape'     => array(
68
            'txt'  => '(c) Thunderforest maps',
69
            'logo' => 'tf_logo.png',
70
            'url'  => 'https://tile.thunderforest.com/landscape/{Z}/{X}/{Y}.png'
71
        ),
72
        'outdoors'      => array(
73
            'txt'  => '(c) Thunderforest maps',
74
            'logo' => 'tf_logo.png',
75
            'url'  => 'https://tile.thunderforest.com/outdoors/{Z}/{X}/{Y}.png'
76
        ),
77
        'toner-lite'    => array(
78
            'txt'  => 'Stamen tiles',
79
            'logo' => 'stamen.png',
80
            'url'  => 'https://stamen-tiles.a.ssl.fastly.net/toner/{Z}/{X}/{Y}.png'
81
        ),
82
        'terrain'       => array(
83
            'txt'  => 'Stamen tiles',
84
            'logo' => 'stamen.png',
85
            'url'  => 'https://stamen-tiles.a.ssl.fastly.net/terrain/{Z}/{X}/{Y}.jpg'
86
        )
87
        //,
88
        // 'piste'=>array(
89
        // 'txt'=>'OpenPisteMap tiles',
90
        // 'logo'=>'piste_logo.png',
91
        // 'url'=>''),
92
        // 'sea'=>array(
93
        // 'txt'=>'OpenSeaMap tiles',
94
        // 'logo'=>'sea_logo.png',
95
        // 'url'=>''),
96
        // H&B sources
97
        //          'hikeandbike' => array (
98
        //                  'txt' => 'Hike & Bike Map',
99
        //                  'logo' => 'hnb_logo.png',
100
        //                  //'url' => 'http://toolserver.org/tiles/hikebike/{Z}/{X}/{Y}.png'
101
        //                  //moved to: https://www.toolserver.org/tiles/hikebike/12/2105/1388.png
102
        //                  'url' => 'http://c.tiles.wmflabs.org/hikebike/{Z}/{X}/{Y}.png'
103
        //          )
104
    );
105
    private $tileDefaultSrc = 'openstreetmap';
106
107
    // set up markers
108
    private $markerPrototypes = array(
109
        // found at http://www.mapito.net/map-marker-icons.html
110
        // these are 17x19 px with a pointer at the bottom left
111
        'lightblue' => array(
112
            'regex'        => '/^lightblue([0-9]+)$/',
113
            'extension'    => '.png',
114
            'shadow'       => false,
115
            'offsetImage'  => '0,-19',
116
            'offsetShadow' => false
117
        ),
118
        // openlayers std markers are 21x25px with shadow
119
        'ol-marker' => array(
120
            'regex'        => '/^marker(|-blue|-gold|-green|-red)+$/',
121
            'extension'    => '.png',
122
            'shadow'       => 'marker_shadow.png',
123
            'offsetImage'  => '-10,-25',
124
            'offsetShadow' => '-1,-13'
125
        ),
126
        // these are 16x16 px
127
        'ww_icon'   => array(
128
            'regex'        => '/ww_\S+$/',
129
            'extension'    => '.png',
130
            'shadow'       => false,
131
            'offsetImage'  => '-8,-8',
132
            'offsetShadow' => false
133
        ),
134
        // assume these are 16x16 px
135
        'rest'      => array(
136
            'regex'        => '/^(?!lightblue([0-9]+)$)(?!(ww_\S+$))(?!marker(|-blue|-gold|-green|-red)+$)(.*)/',
137
            'extension'    => '.png',
138
            'shadow'       => 'marker_shadow.png',
139
            'offsetImage'  => '-8,-8',
140
            'offsetShadow' => '-1,-1'
141
        )
142
    );
143
    private $centerX;
144
    private $centerY;
145
    private $offsetX;
146
    private $offsetY;
147
    private $image;
148
    private $zoom;
149
    private $lat;
150
    private $lon;
151
    private $width;
152
    private $height;
153
    private $markers;
154
    private $maptype;
155
    private $kmlFileName;
156
    private $gpxFileName;
157
    private $geojsonFileName;
158
    private $autoZoomExtent;
159
    private $apikey;
160
    private $tileCacheBaseDir;
161
    private $mapCacheBaseDir;
162
    private $mediaBaseDir;
163
    private $useTileCache;
164
    private $mapCacheID = '';
165
    private $mapCacheFile = '';
166
    private $mapCacheExtension = 'png';
167
168
    /**
169
     * Constructor.
170
     *
171
     * @param float  $lat
172
     *            Latitude (x) of center of map
173
     * @param float  $lon
174
     *            Longitude (y) of center of map
175
     * @param int    $zoom
176
     *            Zoomlevel
177
     * @param int    $width
178
     *            Width in pixels
179
     * @param int    $height
180
     *            Height in pixels
181
     * @param string $maptype
182
     *            Name of the map
183
     * @param array  $markers
184
     *            array of markers
185
     * @param string $gpx
186
     *            GPX filename
187
     * @param string $kml
188
     *            KML filename
189
     * @param string $geojson
190
     * @param string $mediaDir
191
     *            Directory to store/cache maps
192
     * @param string $tileCacheBaseDir
193
     *            Directory to cache map tiles
194
     * @param bool   $autoZoomExtent
195
     *            Wheter or not to override zoom/lat/lon and zoom to the extent of gpx/kml and markers
196
     * @param string $apikey
197
     */
198
    public function __construct(
199
        float $lat,
200
        float $lon,
201
        int $zoom,
202
        int $width,
203
        int $height,
204
        string $maptype,
205
        array $markers,
206
        string $gpx,
207
        string $kml,
208
        string $geojson,
209
        string $mediaDir,
210
        string $tileCacheBaseDir,
211
        bool $autoZoomExtent = true,
212
        string $apikey = ''
213
    ) {
214
        $this->zoom   = $zoom;
215
        $this->lat    = $lat;
216
        $this->lon    = $lon;
217
        $this->width  = $width;
218
        $this->height = $height;
219
        // validate + set maptype
220
        $this->maptype = $this->tileDefaultSrc;
221
        if(array_key_exists($maptype, $this->tileInfo)) {
222
            $this->maptype = $maptype;
223
        }
224
        $this->markers          = $markers;
225
        $this->kmlFileName      = $kml;
226
        $this->gpxFileName      = $gpx;
227
        $this->geojsonFileName  = $geojson;
228
        $this->mediaBaseDir     = $mediaDir;
229
        $this->tileCacheBaseDir = $tileCacheBaseDir . '/olmaptiles';
230
        $this->useTileCache     = $this->tileCacheBaseDir !== '';
231
        $this->mapCacheBaseDir  = $mediaDir . '/olmapmaps';
232
        $this->autoZoomExtent   = $autoZoomExtent;
233
        $this->apikey           = $apikey;
234
    }
235
236
    /**
237
     * get the map, this may return a reference to a cached copy.
238
     *
239
     * @return string url relative to media dir
240
     */
241
    public function getMap(): string {
242
        try {
243
            if($this->autoZoomExtent) {
244
                $this->autoZoom();
245
            }
246
        } catch(Exception $e) {
247
            dbglog($e);
0 ignored issues
show
Bug introduced by
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

247
            /** @scrutinizer ignore-call */ 
248
            dbglog($e);
Loading history...
248
        }
249
250
        // use map cache, so check cache for map
251
        if(!$this->checkMapCache()) {
252
            // map is not in cache, needs to be build
253
            $this->makeMap();
254
            $this->mkdirRecursive(dirname($this->mapCacheIDToFilename()), 0777);
255
            imagepng($this->image, $this->mapCacheIDToFilename(), 9);
256
        }
257
        $doc = $this->mapCacheIDToFilename();
258
        // make url relative to media dir
259
        return str_replace($this->mediaBaseDir, '', $doc);
260
    }
261
262
    /**
263
     * Calculate the lat/lon/zoom values to make sure that all of the markers and gpx/kml are on the map.
264
     *
265
     * @param float $paddingFactor
266
     *            buffer constant to enlarge (>1.0) the zoom level
267
     * @throws Exception if non-geometries are found in the collection
268
     */
269
    private function autoZoom(float $paddingFactor = 1.0): void {
270
        $geoms    = array();
271
        $geoms [] = new Point ($this->lon, $this->lat);
272
        if(!empty ($this->markers)) {
273
            foreach($this->markers as $marker) {
274
                $geoms [] = new Point ($marker ['lon'], $marker ['lat']);
275
            }
276
        }
277
        if(file_exists($this->kmlFileName)) {
278
            $g = geoPHP::load(file_get_contents($this->kmlFileName), 'kml');
279
            if($g !== false) {
280
                $geoms [] = $g;
281
            }
282
        }
283
        if(file_exists($this->gpxFileName)) {
284
            $g = geoPHP::load(file_get_contents($this->gpxFileName), 'gpx');
285
            if($g !== false) {
286
                $geoms [] = $g;
287
            }
288
        }
289
        if(file_exists($this->geojsonFileName)) {
290
            $g = geoPHP::load(file_get_contents($this->geojsonFileName), 'geojson');
291
            if($g !== false) {
292
                $geoms [] = $g;
293
            }
294
        }
295
296
        if(count($geoms) <= 1) {
297
            dbglog($geoms, "StaticMap::autoZoom: Skip setting autozoom options");
0 ignored issues
show
Bug introduced by
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

297
            /** @scrutinizer ignore-call */ 
298
            dbglog($geoms, "StaticMap::autoZoom: Skip setting autozoom options");
Loading history...
298
            return;
299
        }
300
301
        $geom     = new GeometryCollection ($geoms);
302
        $centroid = $geom->centroid();
303
        $bbox     = $geom->getBBox();
304
305
        // determine vertical resolution, this depends on the distance from the equator
306
        // $vy00 = log(tan(M_PI*(0.25 + $centroid->getY()/360)));
307
        $vy0 = log(tan(M_PI * (0.25 + $bbox ['miny'] / 360)));
308
        $vy1 = log(tan(M_PI * (0.25 + $bbox ['maxy'] / 360)));
309
        dbglog("StaticMap::autoZoom: vertical resolution: $vy0, $vy1");
310
        if ($vy1 - $vy0 === 0.0){
311
            $resolutionVertical = 0;
312
            dbglog("StaticMap::autoZoom: using $resolutionVertical");
313
        } else {
314
            $zoomFactorPowered  = ($this->height / 2) / (40.7436654315252 * ($vy1 - $vy0));
315
            $resolutionVertical = 360 / ($zoomFactorPowered * $this->tileSize);
316
        }
317
        // determine horizontal resolution
318
        $resolutionHorizontal = ($bbox ['maxx'] - $bbox ['minx']) / $this->width;
319
        dbglog("StaticMap::autoZoom: using $resolutionHorizontal");
320
        $resolution           = max($resolutionHorizontal, $resolutionVertical) * $paddingFactor;
321
        $zoom                 = $this->zoom;
322
        if ($resolution > 0){
323
            $zoom             = log(360 / ($resolution * $this->tileSize), 2);
324
        }
325
326
        if(is_finite($zoom) && $zoom < 15 && $zoom > 2) {
327
            $this->zoom = floor($zoom);
328
        }
329
        $this->lon = $centroid->getX();
330
        $this->lat = $centroid->getY();
331
        dbglog("StaticMap::autoZoom: Set autozoom options to: z: $this->zoom, lon: $this->lon, lat: $this->lat");
332
    }
333
334
    public function checkMapCache(): bool {
335
        // side effect: set the mapCacheID
336
        $this->mapCacheID = md5($this->serializeParams());
337
        $filename         = $this->mapCacheIDToFilename();
338
        return file_exists($filename);
339
    }
340
341
    public function serializeParams(): string {
342
        return implode(
343
            "&", array(
344
                   $this->zoom,
345
                   $this->lat,
346
                   $this->lon,
347
                   $this->width,
348
                   $this->height,
349
                   serialize($this->markers),
350
                   $this->maptype,
351
                   $this->kmlFileName,
352
                   $this->gpxFileName,
353
                   $this->geojsonFileName
354
               )
355
        );
356
    }
357
358
    public function mapCacheIDToFilename(): string {
359
        if(!$this->mapCacheFile) {
360
            $this->mapCacheFile = $this->mapCacheBaseDir . "/" . $this->maptype . "/" . $this->zoom . "/cache_"
0 ignored issues
show
Bug introduced by
Are you sure $this->maptype of type mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

360
            $this->mapCacheFile = $this->mapCacheBaseDir . "/" . /** @scrutinizer ignore-type */ $this->maptype . "/" . $this->zoom . "/cache_"
Loading history...
361
                . substr($this->mapCacheID, 0, 2) . "/" . substr($this->mapCacheID, 2, 2)
362
                . "/" . substr($this->mapCacheID, 4);
363
        }
364
        return $this->mapCacheFile . "." . $this->mapCacheExtension;
365
    }
366
367
    /**
368
     * make the map.
369
     */
370
    public function makeMap(): void {
371
        $this->initCoords();
372
        $this->createBaseMap();
373
        if(!empty ($this->markers)) {
374
            $this->placeMarkers();
375
        }
376
        if (file_exists($this->kmlFileName)) {
377
            try {
378
                $this->drawKML();
379
            } catch (exception $e) {
380
                dbglog('failed to load KML file', $e);
0 ignored issues
show
Bug introduced by
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

380
                /** @scrutinizer ignore-call */ 
381
                dbglog('failed to load KML file', $e);
Loading history...
381
            }
382
        }
383
        if (file_exists($this->gpxFileName)) {
384
            try {
385
                $this->drawGPX();
386
            } catch (exception $e) {
387
                dbglog('failed to load GPX file', $e);
388
            }
389
        }
390
        if (file_exists($this->geojsonFileName)) {
391
            try {
392
                $this->drawGeojson();
393
            } catch (exception $e) {
394
                dbglog('failed to load GeoJSON file', $e);
395
            }
396
        }
397
398
        $this->drawCopyright();
399
    }
400
401
    /**
402
     */
403
    public function initCoords(): void {
404
        $this->centerX = $this->lonToTile($this->lon, $this->zoom);
405
        $this->centerY = $this->latToTile($this->lat, $this->zoom);
406
        $this->offsetX = floor((floor($this->centerX) - $this->centerX) * $this->tileSize);
407
        $this->offsetY = floor((floor($this->centerY) - $this->centerY) * $this->tileSize);
408
    }
409
410
    /**
411
     *
412
     * @param float $long
413
     * @param int   $zoom
414
     * @return float|int
415
     */
416
    public function lonToTile(float $long, int $zoom) {
417
        return (($long + 180) / 360) * pow(2, $zoom);
418
    }
419
420
    /**
421
     *
422
     * @param float $lat
423
     * @param int   $zoom
424
     * @return float|int
425
     */
426
    public function latToTile(float $lat, int $zoom) {
427
        return (1 - log(tan($lat * pi() / 180) + 1 / cos($lat * M_PI / 180)) / M_PI) / 2 * pow(2, $zoom);
428
    }
429
430
    /**
431
     * make basemap image.
432
     */
433
    public function createBaseMap(): void {
434
        $this->image   = imagecreatetruecolor($this->width, $this->height);
435
        $startX        = floor($this->centerX - ($this->width / $this->tileSize) / 2);
436
        $startY        = floor($this->centerY - ($this->height / $this->tileSize) / 2);
437
        $endX          = ceil($this->centerX + ($this->width / $this->tileSize) / 2);
438
        $endY          = ceil($this->centerY + ($this->height / $this->tileSize) / 2);
439
        $this->offsetX = -floor(($this->centerX - floor($this->centerX)) * $this->tileSize);
440
        $this->offsetY = -floor(($this->centerY - floor($this->centerY)) * $this->tileSize);
441
        $this->offsetX += floor($this->width / 2);
442
        $this->offsetY += floor($this->height / 2);
443
        $this->offsetX += floor($startX - floor($this->centerX)) * $this->tileSize;
444
        $this->offsetY += floor($startY - floor($this->centerY)) * $this->tileSize;
445
446
        for($x = $startX; $x <= $endX; $x++) {
447
            for($y = $startY; $y <= $endY; $y++) {
448
                $url = str_replace(
449
                    array(
450
                        '{Z}',
451
                        '{X}',
452
                        '{Y}'
453
                    ), array(
454
                        $this->zoom,
455
                        $x,
456
                        $y
457
                    ), $this->tileInfo [$this->maptype] ['url']
458
                );
459
460
                $tileData = $this->fetchTile($url);
461
                if($tileData) {
462
                    $tileImage = imagecreatefromstring($tileData);
0 ignored issues
show
Bug introduced by
It seems like $tileData can also be of type true; however, parameter $image of imagecreatefromstring() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

462
                    $tileImage = imagecreatefromstring(/** @scrutinizer ignore-type */ $tileData);
Loading history...
463
                } else {
464
                    $tileImage = imagecreate($this->tileSize, $this->tileSize);
465
                    $color     = imagecolorallocate($tileImage, 255, 255, 255);
466
                    @imagestring($tileImage, 1, 127, 127, 'err', $color);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for imagestring(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

466
                    /** @scrutinizer ignore-unhandled */ @imagestring($tileImage, 1, 127, 127, 'err', $color);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
467
                }
468
                $destX = ($x - $startX) * $this->tileSize + $this->offsetX;
469
                $destY = ($y - $startY) * $this->tileSize + $this->offsetY;
470
                dbglog($this->tileSize, "imagecopy tile into image: $destX, $destY");
0 ignored issues
show
Bug introduced by
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

470
                /** @scrutinizer ignore-call */ 
471
                dbglog($this->tileSize, "imagecopy tile into image: $destX, $destY");
Loading history...
471
                imagecopy(
472
                    $this->image, $tileImage, $destX, $destY, 0, 0, $this->tileSize,
0 ignored issues
show
Bug introduced by
$destX of type double is incompatible with the type integer expected by parameter $dst_x of imagecopy(). ( Ignorable by Annotation )

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

472
                    $this->image, $tileImage, /** @scrutinizer ignore-type */ $destX, $destY, 0, 0, $this->tileSize,
Loading history...
Bug introduced by
$destY of type double is incompatible with the type integer expected by parameter $dst_y of imagecopy(). ( Ignorable by Annotation )

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

472
                    $this->image, $tileImage, $destX, /** @scrutinizer ignore-type */ $destY, 0, 0, $this->tileSize,
Loading history...
473
                    $this->tileSize
474
                );
475
            }
476
        }
477
    }
478
479
    /**
480
     * Fetch a tile and (if configured) store it in the cache.
481
     * @param string $url
482
     * @return bool|string
483
     * @todo refactor this to use dokuwiki\HTTP\HTTPClient or dokuwiki\HTTP\DokuHTTPClient
484
     *          for better proxy handling...
485
     */
486
    public function fetchTile(string $url) {
487
        if($this->useTileCache && ($cached = $this->checkTileCache($url)))
488
            return $cached;
489
490
        $_UA = 'Mozilla/4.0 (compatible; DokuWikiSpatial HTTP Client; ' . PHP_OS . ')';
491
        if(function_exists("curl_init")) {
492
            // use cUrl
493
            $ch = curl_init();
494
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
495
            curl_setopt($ch, CURLOPT_USERAGENT, $_UA);
496
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
497
            curl_setopt($ch, CURLOPT_URL, $url . $this->apikey);
498
            dbglog("StaticMap::fetchTile: getting: $url using curl_exec");
0 ignored issues
show
Bug introduced by
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

498
            /** @scrutinizer ignore-call */ 
499
            dbglog("StaticMap::fetchTile: getting: $url using curl_exec");
Loading history...
499
            $tile = curl_exec($ch);
500
            curl_close($ch);
501
        } else {
502
            // use file_get_contents
503
            global $conf;
504
            $opts = array(
505
                'http' => array(
506
                    'method'          => "GET",
507
                    'header'          => "Accept-language: en\r\n" . "User-Agent: $_UA\r\n" . "accept: image/png\r\n",
508
                    'request_fulluri' => true
509
                )
510
            );
511
            if(isset($conf['proxy']['host'], $conf['proxy']['port'])
512
                && $conf['proxy']['host'] !== ''
513
                && $conf['proxy']['port'] !== '') {
514
                $opts['http'] += ['proxy' => "tcp://" . $conf['proxy']['host'] . ":" . $conf['proxy']['port']];
515
            }
516
517
            $context = stream_context_create($opts);
518
            dbglog("StaticMap::fetchTile: getting: $url . $this->apikey using file_get_contents and options $opts");
519
            $tile = file_get_contents($url . $this->apikey, false, $context);
520
        }
521
        if($tile && $this->useTileCache) {
522
            $this->writeTileToCache($url, $tile);
523
        }
524
        return $tile;
525
    }
526
527
    /**
528
     *
529
     * @param string $url
530
     * @return string|false
531
     */
532
    public function checkTileCache(string $url) {
533
        $filename = $this->tileUrlToFilename($url);
534
        if(file_exists($filename)) {
535
            return file_get_contents($filename);
536
        }
537
        return false;
538
    }
539
540
    /**
541
     *
542
     * @param string $url
543
     * @return string
544
     */
545
    public function tileUrlToFilename(string $url): string {
546
        return $this->tileCacheBaseDir . "/" . substr($url, strpos($url, '/') + 1);
547
    }
548
549
    /**
550
     * Write a tile into the cache.
551
     *
552
     * @param string $url
553
     * @param mixed  $data
554
     */
555
    public function writeTileToCache($url, $data): void {
556
        $filename = $this->tileUrlToFilename($url);
557
        $this->mkdirRecursive(dirname($filename), 0777);
558
        file_put_contents($filename, $data);
559
    }
560
561
    /**
562
     * Recursively create the directory.
563
     *
564
     * @param string $pathname
565
     *            The directory path.
566
     * @param int    $mode
567
     *            File access mode. For more information on modes, read the details on the chmod manpage.
568
     */
569
    public function mkdirRecursive(string $pathname, int $mode): bool {
570
        is_dir(dirname($pathname)) || $this->mkdirRecursive(dirname($pathname), $mode);
571
        return is_dir($pathname) || mkdir($pathname, $mode) || is_dir($pathname);
572
    }
573
574
    /**
575
     * Place markers on the map and number them in the same order as they are listed in the html.
576
     */
577
    public function placeMarkers(): void {
578
        $count         = 0;
579
        $color         = imagecolorallocate($this->image, 0, 0, 0);
580
        $bgcolor       = imagecolorallocate($this->image, 200, 200, 200);
581
        $markerBaseDir = __DIR__ . '/icons';
582
        $markerImageOffsetX  = 0;
583
        $markerImageOffsetY  = 0;
584
        $markerShadowOffsetX = 0;
585
        $markerShadowOffsetY = 0;
586
        $markerShadowImg     = null;
587
        // loop thru marker array
588
        foreach($this->markers as $marker) {
589
            // set some local variables
590
            $markerLat  = $marker ['lat'];
591
            $markerLon  = $marker ['lon'];
592
            $markerType = $marker ['type'];
593
            // clear variables from previous loops
594
            $markerFilename = '';
595
            $markerShadow   = '';
596
            $matches        = false;
597
            // check for marker type, get settings from markerPrototypes
598
            if($markerType) {
599
                foreach($this->markerPrototypes as $markerPrototype) {
600
                    if(preg_match($markerPrototype ['regex'], $markerType, $matches)) {
0 ignored issues
show
Bug introduced by
$matches of type false is incompatible with the type string[] expected by parameter $matches of preg_match(). ( Ignorable by Annotation )

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

600
                    if(preg_match($markerPrototype ['regex'], $markerType, /** @scrutinizer ignore-type */ $matches)) {
Loading history...
601
                        $markerFilename = $matches [0] . $markerPrototype ['extension'];
602
                        if($markerPrototype ['offsetImage']) {
603
                            list ($markerImageOffsetX, $markerImageOffsetY) = explode(
604
                                ",",
605
                                $markerPrototype ['offsetImage']
606
                            );
607
                        }
608
                        $markerShadow = $markerPrototype ['shadow'];
609
                        if($markerShadow) {
610
                            list ($markerShadowOffsetX, $markerShadowOffsetY) = explode(
611
                                ",",
612
                                $markerPrototype ['offsetShadow']
613
                            );
614
                        }
615
                    }
616
                }
617
            }
618
            // create img resource
619
            if(file_exists($markerBaseDir . '/' . $markerFilename)) {
620
                $markerImg = imagecreatefrompng($markerBaseDir . '/' . $markerFilename);
621
            } else {
622
                $markerImg = imagecreatefrompng($markerBaseDir . '/marker.png');
623
            }
624
            // check for shadow + create shadow recource
625
            if($markerShadow && file_exists($markerBaseDir . '/' . $markerShadow)) {
626
                $markerShadowImg = imagecreatefrompng($markerBaseDir . '/' . $markerShadow);
627
            }
628
            // calc position
629
            $destX = floor(
630
                ($this->width / 2) -
631
                $this->tileSize * ($this->centerX - $this->lonToTile($markerLon, $this->zoom))
632
            );
633
            $destY = floor(
634
                ($this->height / 2) -
635
                $this->tileSize * ($this->centerY - $this->latToTile($markerLat, $this->zoom))
636
            );
637
            // copy shadow on basemap
638
            if($markerShadow && $markerShadowImg) {
639
                imagecopy(
640
                    $this->image,
641
                    $markerShadowImg,
642
                    $destX + (int) $markerShadowOffsetX,
0 ignored issues
show
Bug introduced by
$destX + (int)$markerShadowOffsetX of type double is incompatible with the type integer expected by parameter $dst_x of imagecopy(). ( Ignorable by Annotation )

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

642
                    /** @scrutinizer ignore-type */ $destX + (int) $markerShadowOffsetX,
Loading history...
643
                    $destY + (int) $markerShadowOffsetY,
0 ignored issues
show
Bug introduced by
$destY + (int)$markerShadowOffsetY of type double is incompatible with the type integer expected by parameter $dst_y of imagecopy(). ( Ignorable by Annotation )

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

643
                    /** @scrutinizer ignore-type */ $destY + (int) $markerShadowOffsetY,
Loading history...
644
                    0,
645
                    0,
646
                    imagesx($markerShadowImg),
647
                    imagesy($markerShadowImg)
648
                );
649
            }
650
            // copy marker on basemap above shadow
651
            imagecopy(
652
                $this->image,
653
                $markerImg,
654
                $destX + (int) $markerImageOffsetX,
655
                $destY + (int) $markerImageOffsetY,
656
                0,
657
                0,
658
                imagesx($markerImg),
659
                imagesy($markerImg)
660
            );
661
            // add label
662
            imagestring(
663
                $this->image,
664
                3,
665
                $destX - imagesx($markerImg) + 1,
0 ignored issues
show
Bug introduced by
$destX - imagesx($markerImg) + 1 of type double is incompatible with the type integer expected by parameter $x of imagestring(). ( Ignorable by Annotation )

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

665
                /** @scrutinizer ignore-type */ $destX - imagesx($markerImg) + 1,
Loading history...
666
                $destY + (int) $markerImageOffsetY + 1,
0 ignored issues
show
Bug introduced by
$destY + (int)$markerImageOffsetY + 1 of type double is incompatible with the type integer expected by parameter $y of imagestring(). ( Ignorable by Annotation )

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

666
                /** @scrutinizer ignore-type */ $destY + (int) $markerImageOffsetY + 1,
Loading history...
667
                ++$count,
668
                $bgcolor
669
            );
670
            imagestring(
671
                $this->image,
672
                3,
673
                $destX - imagesx($markerImg),
674
                $destY + (int) $markerImageOffsetY,
675
                $count,
676
                $color
677
            );
678
        }
679
    }
680
681
    /**
682
     * Draw kml trace on the map.
683
     * @throws exception when loading the KML fails
684
     */
685
    public function drawKML(): void {
686
        // TODO get colour from kml node (not currently supported in geoPHP)
687
        $col     = imagecolorallocatealpha($this->image, 255, 0, 0, .4 * 127);
0 ignored issues
show
Bug introduced by
0.4 * 127 of type double is incompatible with the type integer expected by parameter $alpha of imagecolorallocatealpha(). ( Ignorable by Annotation )

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

687
        $col     = imagecolorallocatealpha($this->image, 255, 0, 0, /** @scrutinizer ignore-type */ .4 * 127);
Loading history...
688
        $kmlgeom = geoPHP::load(file_get_contents($this->kmlFileName), 'kml');
689
        $this->drawGeometry($kmlgeom, $col);
690
    }
691
692
    /**
693
     * Draw geometry or geometry collection on the map.
694
     *
695
     * @param Geometry $geom
696
     * @param int      $colour
697
     *            drawing colour
698
     */
699
    private function drawGeometry(Geometry $geom, int $colour): void {
700
        if(empty($geom)) {
701
            return;
702
        }
703
704
        switch($geom->geometryType()) {
705
            case 'GeometryCollection' :
706
                // recursively draw part of the collection
707
                for($i = 1; $i < $geom->numGeometries() + 1; $i++) {
708
                    $_geom = $geom->geometryN($i);
709
                    $this->drawGeometry($_geom, $colour);
710
                }
711
                break;
712
            case 'MultiPolygon' :
713
            case 'MultiLineString' :
714
            case 'MultiPoint' :
715
                // TODO implement / do nothing
716
                break;
717
            case 'Polygon' :
718
                $this->drawPolygon($geom, $colour);
719
                break;
720
            case 'LineString' :
721
                $this->drawLineString($geom, $colour);
722
                break;
723
            case 'Point' :
724
                $this->drawPoint($geom, $colour);
725
                break;
726
            default :
727
                // draw nothing
728
                break;
729
        }
730
    }
731
732
    /**
733
     * Draw a polygon on the map.
734
     *
735
     * @param Polygon $polygon
736
     * @param int     $colour
737
     *            drawing colour
738
     */
739
    private function drawPolygon($polygon, int $colour) {
740
        // TODO implementation of drawing holes,
741
        // maybe draw the polygon to an in-memory image and use imagecopy, draw polygon in col., draw holes in bgcol?
742
743
        // print_r('Polygon:<br />');
744
        // print_r($polygon);
745
        $extPoints = array();
746
        // extring is a linestring actually..
747
        $extRing = $polygon->exteriorRing();
748
749
        for($i = 1; $i < $extRing->numGeometries(); $i++) {
750
            $p1           = $extRing->geometryN($i);
751
            $x            = floor(
752
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom))
753
            );
754
            $y            = floor(
755
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom))
756
            );
757
            $extPoints [] = $x;
758
            $extPoints [] = $y;
759
        }
760
        // print_r('points:('.($i-1).')<br />');
761
        // print_r($extPoints);
762
        // imagepolygon ($this->image, $extPoints, $i-1, $colour );
763
        imagefilledpolygon($this->image, $extPoints, $i - 1, $colour);
764
    }
765
766
    /**
767
     * Draw a line on the map.
768
     *
769
     * @param LineString $line
770
     * @param int        $colour
771
     *            drawing colour
772
     */
773
    private function drawLineString($line, $colour) {
774
        imagesetthickness($this->image, 2);
775
        for($p = 1; $p < $line->numGeometries(); $p++) {
776
            // get first pair of points
777
            $p1 = $line->geometryN($p);
778
            $p2 = $line->geometryN($p + 1);
779
            // translate to paper space
780
            $x1 = floor(
781
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p1->x(), $this->zoom))
782
            );
783
            $y1 = floor(
784
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p1->y(), $this->zoom))
785
            );
786
            $x2 = floor(
787
                ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($p2->x(), $this->zoom))
788
            );
789
            $y2 = floor(
790
                ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($p2->y(), $this->zoom))
791
            );
792
            // draw to image
793
            imageline($this->image, $x1, $y1, $x2, $y2, $colour);
0 ignored issues
show
Bug introduced by
$y1 of type double is incompatible with the type integer expected by parameter $y1 of imageline(). ( Ignorable by Annotation )

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

793
            imageline($this->image, $x1, /** @scrutinizer ignore-type */ $y1, $x2, $y2, $colour);
Loading history...
Bug introduced by
$x1 of type double is incompatible with the type integer expected by parameter $x1 of imageline(). ( Ignorable by Annotation )

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

793
            imageline($this->image, /** @scrutinizer ignore-type */ $x1, $y1, $x2, $y2, $colour);
Loading history...
Bug introduced by
$y2 of type double is incompatible with the type integer expected by parameter $y2 of imageline(). ( Ignorable by Annotation )

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

793
            imageline($this->image, $x1, $y1, $x2, /** @scrutinizer ignore-type */ $y2, $colour);
Loading history...
Bug introduced by
$x2 of type double is incompatible with the type integer expected by parameter $x2 of imageline(). ( Ignorable by Annotation )

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

793
            imageline($this->image, $x1, $y1, /** @scrutinizer ignore-type */ $x2, $y2, $colour);
Loading history...
794
        }
795
        imagesetthickness($this->image, 1);
796
    }
797
798
    /**
799
     * Draw a point on the map.
800
     *
801
     * @param Point $point
802
     * @param int   $colour
803
     *            drawing colour
804
     */
805
    private function drawPoint($point, $colour) {
806
        imagesetthickness($this->image, 2);
807
        // translate to paper space
808
        $cx = floor(
809
            ($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($point->x(), $this->zoom))
810
        );
811
        $cy = floor(
812
            ($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($point->y(), $this->zoom))
813
        );
814
        $r  = 5;
815
        // draw to image
816
        // imageellipse($this->image, $cx, $cy,$r, $r, $colour);
817
        imagefilledellipse($this->image, $cx, $cy, $r, $r, $colour);
0 ignored issues
show
Bug introduced by
$cx of type double is incompatible with the type integer expected by parameter $cx of imagefilledellipse(). ( Ignorable by Annotation )

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

817
        imagefilledellipse($this->image, /** @scrutinizer ignore-type */ $cx, $cy, $r, $r, $colour);
Loading history...
Bug introduced by
$cy of type double is incompatible with the type integer expected by parameter $cy of imagefilledellipse(). ( Ignorable by Annotation )

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

817
        imagefilledellipse($this->image, $cx, /** @scrutinizer ignore-type */ $cy, $r, $r, $colour);
Loading history...
818
        // don't use imageellipse because the imagesetthickness function has
819
        // no effect. So the better workaround is to use imagearc.
820
        imagearc($this->image, $cx, $cy, $r, $r, 0, 359, $colour);
0 ignored issues
show
Bug introduced by
$cy of type double is incompatible with the type integer expected by parameter $cy of imagearc(). ( Ignorable by Annotation )

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

820
        imagearc($this->image, $cx, /** @scrutinizer ignore-type */ $cy, $r, $r, 0, 359, $colour);
Loading history...
Bug introduced by
$cx of type double is incompatible with the type integer expected by parameter $cx of imagearc(). ( Ignorable by Annotation )

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

820
        imagearc($this->image, /** @scrutinizer ignore-type */ $cx, $cy, $r, $r, 0, 359, $colour);
Loading history...
821
        imagesetthickness($this->image, 1);
822
    }
823
824
    /**
825
     * Draw gpx trace on the map.
826
     * @throws exception when loading the GPX fails
827
     */
828
    public function drawGPX() {
829
        $col     = imagecolorallocatealpha($this->image, 0, 0, 255, .4 * 127);
0 ignored issues
show
Bug introduced by
0.4 * 127 of type double is incompatible with the type integer expected by parameter $alpha of imagecolorallocatealpha(). ( Ignorable by Annotation )

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

829
        $col     = imagecolorallocatealpha($this->image, 0, 0, 255, /** @scrutinizer ignore-type */ .4 * 127);
Loading history...
830
        $gpxgeom = geoPHP::load(file_get_contents($this->gpxFileName), 'gpx');
831
        $this->drawGeometry($gpxgeom, $col);
832
    }
833
834
    /**
835
     * Draw geojson on the map.
836
     * @throws exception when loading the JSON fails
837
     */
838
    public function drawGeojson() {
839
        $col     = imagecolorallocatealpha($this->image, 255, 0, 255, .4 * 127);
0 ignored issues
show
Bug introduced by
0.4 * 127 of type double is incompatible with the type integer expected by parameter $alpha of imagecolorallocatealpha(). ( Ignorable by Annotation )

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

839
        $col     = imagecolorallocatealpha($this->image, 255, 0, 255, /** @scrutinizer ignore-type */ .4 * 127);
Loading history...
840
        $gpxgeom = geoPHP::load(file_get_contents($this->geojsonFileName), 'json');
841
        $this->drawGeometry($gpxgeom, $col);
842
    }
843
844
    /**
845
     * add copyright and origin notice and icons to the map.
846
     */
847
    public function drawCopyright() {
848
        $logoBaseDir = dirname(__FILE__) . '/' . 'logo/';
849
        $logoImg     = imagecreatefrompng($logoBaseDir . $this->tileInfo ['openstreetmap'] ['logo']);
850
        $textcolor   = imagecolorallocate($this->image, 0, 0, 0);
851
        $bgcolor     = imagecolorallocate($this->image, 200, 200, 200);
852
853
        imagecopy(
854
            $this->image,
855
            $logoImg,
856
            0,
857
            imagesy($this->image) - imagesy($logoImg),
858
            0,
859
            0,
860
            imagesx($logoImg),
861
            imagesy($logoImg)
862
        );
863
        imagestring(
864
            $this->image,
865
            1,
866
            imagesx($logoImg) + 2,
867
            imagesy($this->image) - imagesy($logoImg) + 1,
868
            $this->tileInfo ['openstreetmap'] ['txt'],
869
            $bgcolor
870
        );
871
        imagestring(
872
            $this->image,
873
            1,
874
            imagesx($logoImg) + 1,
875
            imagesy($this->image) - imagesy($logoImg),
876
            $this->tileInfo ['openstreetmap'] ['txt'],
877
            $textcolor
878
        );
879
880
        // additional tile source info, ie. who created/hosted the tiles
881
        $xIconOffset = 0;
882
        if($this->maptype === 'openstreetmap') {
883
            $mapAuthor = "(c) OpenStreetMap maps/CC BY-SA";
884
        } else {
885
            $mapAuthor   = $this->tileInfo [$this->maptype] ['txt'];
886
            $iconImg     = imagecreatefrompng($logoBaseDir . $this->tileInfo [$this->maptype] ['logo']);
887
            $xIconOffset = imagesx($iconImg);
888
            imagecopy(
889
                $this->image,
890
                $iconImg, imagesx($logoImg) + 1,
891
                imagesy($this->image) - imagesy($iconImg),
892
                0,
893
                0,
894
                imagesx($iconImg), imagesy($iconImg)
895
            );
896
        }
897
        imagestring(
898
            $this->image,
899
            1, imagesx($logoImg) + $xIconOffset + 4,
900
            imagesy($this->image) - ceil(imagesy($logoImg) / 2) + 1,
0 ignored issues
show
Bug introduced by
imagesy($this->image) - ...gesy($logoImg) / 2) + 1 of type double is incompatible with the type integer expected by parameter $y of imagestring(). ( Ignorable by Annotation )

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

900
            /** @scrutinizer ignore-type */ imagesy($this->image) - ceil(imagesy($logoImg) / 2) + 1,
Loading history...
901
            $mapAuthor,
902
            $bgcolor
903
        );
904
        imagestring(
905
            $this->image,
906
            1, imagesx($logoImg) + $xIconOffset + 3,
907
            imagesy($this->image) - ceil(imagesy($logoImg) / 2),
908
            $mapAuthor,
909
            $textcolor
910
        );
911
912
    }
913
}
914