Passed
Pull Request — master (#4)
by Mark
01:50
created

GPX::parseRoutes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 4
nop 1
dl 0
loc 20
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
namespace geoPHP\Adapter;
4
5
use DOMDocument;
6
use DOMElement;
7
use DOMXPath;
8
use geoPHP\Geometry\Collection;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Collection 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...
9
use geoPHP\geoPHP;
10
use geoPHP\Geometry\Geometry;
11
use geoPHP\Geometry\GeometryCollection;
12
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...
13
use geoPHP\Geometry\LineString;
14
use geoPHP\Geometry\MultiLineString;
15
16
/*
17
 * Copyright (c) Patrick Hayes
18
 *
19
 * This code is open-source and licenced under the Modified BSD License.
20
 * For the full copyright and license information, please view the LICENSE
21
 * file that was distributed with this source code.
22
 */
23
24
/**
25
 * PHP Geometry/GPX encoder/decoder
26
 */
27
class GPX implements GeoAdapter
28
{
29
30
    protected $nss = ''; // Name-space string. eg 'georss:'
31
32
    /**
33
     * @var GpxTypes
34
     */
35
    protected $gpxTypes;
36
37
    /**
38
     * @var DOMXPath
39
     */
40
    protected $xpath;
41
42
    protected $parseGarminRpt = false;
43
44
    protected $trackFromRoute = null;
45
46
    /**
47
     * Read GPX string into geometry object
48
     *
49
     * @param string $gpx A GPX string
50
     * @param array|null $allowedElements Which elements can be read from each GPX type
51
     *                   If not specified, every element defined in the GPX specification can be read
52
     *                   Can be overwritten with an associative array, with type name in keys.
53
     *                   eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null]
54
     * @return Geometry|GeometryCollection
55
     * @throws \Exception If GPX is not a valid XML
56
     */
57
    public function read($gpx, $allowedElements = null)
58
    {
59
        $this->gpxTypes = new GpxTypes($allowedElements);
60
61
        //libxml_use_internal_errors(true); // why?
62
63
        // Load into DOMDocument
64
        $xmlObject = new DOMDocument('1.0', 'UTF-8');
65
        $xmlObject->preserveWhiteSpace = false;
66
        @$xmlObject->loadXML($gpx);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for loadXML(). 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

66
        /** @scrutinizer ignore-unhandled */ @$xmlObject->loadXML($gpx);

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...
67
        if ($xmlObject === false) {
0 ignored issues
show
introduced by
The condition $xmlObject === false is always false.
Loading history...
68
            throw new \Exception("Invalid GPX: " . $gpx);
69
        }
70
71
        $this->parseGarminRpt = strpos($gpx, 'gpxx:rpt') > 0;
72
73
        // Initialize XPath parser if needed (currently only for Garmin extensions)
74
        if ($this->parseGarminRpt) {
75
            $this->xpath = new DOMXPath($xmlObject);
76
            $this->xpath->registerNamespace('gpx', 'http://www.topografix.com/GPX/1/1');
77
            $this->xpath->registerNamespace('gpxx', 'http://www.garmin.com/xmlschemas/GpxExtensions/v3');
78
        }
79
80
        try {
81
            $geom = $this->geomFromXML($xmlObject);
82
            if ($geom->isEmpty()) {
83
                /* Geometry was empty but maybe because its tags was not lower cased.
84
                   We try to lower-case tags and try to run again, but just once.
85
                */
86
                $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
87
                $caller = isset($backtrace[1]['function']) ? $backtrace[1]['function'] : null;
88
                if ($caller && $caller !== __FUNCTION__) {
89
                    $gpx = preg_replace_callback(
90
                        "/(<\/?\w+)(.*?>)/",
91
                        function ($m) {
92
                            return strtolower($m[1]) . $m[2];
93
                        },
94
                        $gpx
95
                    );
96
                    $geom = $this->read($gpx, $allowedElements);
97
                }
98
            }
99
        } catch (\Exception $e) {
100
            throw new \Exception("Cannot Read Geometry From GPX: " . $gpx);
101
        }
102
103
        return $geom;
104
    }
105
106
    /**
107
     * Parses the GPX XML and returns a geometry
108
     * @param DOMDocument $xmlObject
109
     * @return GeometryCollection|Geometry Returns the geometry representation of the GPX (@see geoPHP::buildGeometry)
110
     */
111
    protected function geomFromXML($xmlObject)
112
    {
113
        /** @var Geometry[] $geometries */
114
        $geometries = array_merge(
115
            $this->parseWaypoints($xmlObject),
116
            $this->parseTracks($xmlObject),
117
            $this->parseRoutes($xmlObject)
118
        );
119
120
        if (isset($this->trackFromRoute)) {
121
            $trackFromRoute = new LineString($this->trackFromRoute);
122
            $trackFromRoute->setData('gpxType', 'track');
123
            $trackFromRoute->setData('type', 'planned route');
124
            $geometries[] = $trackFromRoute;
125
        }
126
127
        $geometry = geoPHP::buildGeometry($geometries);
128
        if (in_array('metadata', $this->gpxTypes->get('gpxType')) && $xmlObject->getElementsByTagName('metadata')->length === 1) {
129
            $metadata = self::parseNodeProperties(
130
                $xmlObject->getElementsByTagName('metadata')->item(0),
131
                $this->gpxTypes->get('metadataType')
132
            );
133
            if ($geometry->getData() !== null && $metadata !== null) {
134
                $geometry = new GeometryCollection([$geometry]);
135
            }
136
            $geometry->setData($metadata);
137
        }
138
139
        return $geometry;
140
    }
141
142
    protected function childElements($xml, $nodeName = '')
143
    {
144
        $children = [];
145
        foreach ($xml->childNodes as $child) {
146
            if ($child->nodeName == $nodeName) {
147
                $children[] = $child;
148
            }
149
        }
150
        return $children;
151
    }
152
153
    /**
154
     * @param DOMElement $node
155
     * @return Point
156
     */
157
    protected function parsePoint($node)
158
    {
159
        $lat = $node->attributes->getNamedItem("lat")->nodeValue;
0 ignored issues
show
Bug introduced by
The method getNamedItem() does not exist on null. ( Ignorable by Annotation )

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

159
        $lat = $node->attributes->/** @scrutinizer ignore-call */ getNamedItem("lat")->nodeValue;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
160
        $lon = $node->attributes->getNamedItem("lon")->nodeValue;
161
        $elevation = null;
162
        $ele = $node->getElementsByTagName('ele');
163
        if ($ele->length) {
164
            $elevation = $ele->item(0)->nodeValue;
165
        }
166
        $point = new Point($lon, $lat, $elevation);
167
        $point->setData($this->parseNodeProperties($node, $this->gpxTypes->get($node->nodeName . 'Type')));
168
        if ($node->nodeName === 'rtept' && $this->parseGarminRpt) {
169
            foreach ($this->xpath->query('.//gpx:extensions/gpxx:RoutePointExtension/gpxx:rpt', $node) as $element) {
170
                $this->trackFromRoute[] = $this->parsePoint($element);
171
            }
172
        }
173
        return $point;
174
    }
175
176
    /**
177
     * @param DOMDocument $xmlObject
178
     * @return Point[]
179
     */
180
    protected function parseWaypoints($xmlObject)
181
    {
182
        if (!in_array('wpt', $this->gpxTypes->get('gpxType'))) {
183
            return [];
184
        }
185
        $points = [];
186
        $wptElements = $xmlObject->getElementsByTagName('wpt');
187
        foreach ($wptElements as $wpt) {
188
            $point = $this->parsePoint($wpt);
189
            $point->setData('gpxType', 'waypoint');
190
            $points[] = $point;
191
        }
192
        return $points;
193
    }
194
195
    /**
196
     * @param DOMDocument $xmlObject
197
     * @return LineString[]
198
     */
199
    protected function parseTracks($xmlObject)
200
    {
201
        if (!in_array('trk', $this->gpxTypes->get('gpxType'))) {
202
            return [];
203
        }
204
        $tracks = [];
205
        $trkElements = $xmlObject->getElementsByTagName('trk');
206
        foreach ($trkElements as $trk) {
207
            $segments = [];
208
            /** @noinspection SpellCheckingInspection */
209
            foreach ($this->childElements($trk, 'trkseg') as $trkseg) {
210
                $points = [];
211
                /** @noinspection SpellCheckingInspection */
212
                foreach ($this->childElements($trkseg, 'trkpt') as $trkpt) {
213
                    $points[] = $this->parsePoint($trkpt);
214
                }
215
                // Avoids creating invalid LineString
216
                $segments[] = new LineString(count($points) <> 1 ? $points : []);
217
            }
218
            $track = count($segments) === 0
219
                    ? new LineString()
220
                    : (count($segments) === 1
221
                            ? $segments[0]
222
                            : new MultiLineString($segments));
223
            $track->setData($this->parseNodeProperties($trk, $this->gpxTypes->get('trkType')));
224
            $track->setData('gpxType', 'track');
225
            $tracks[] = $track;
226
        }
227
        return $tracks;
228
    }
229
230
    /**
231
     * @param DOMDocument $xmlObject
232
     * @return LineString[]
233
     */
234
    protected function parseRoutes($xmlObject)
235
    {
236
        if (!in_array('rte', $this->gpxTypes->get('gpxType'))) {
237
            return [];
238
        }
239
        $lines = [];
240
        $rteElements = $xmlObject->getElementsByTagName('rte');
241
        foreach ($rteElements as $rte) {
242
            $components = [];
243
            /** @noinspection SpellCheckingInspection */
244
            foreach ($this->childElements($rte, 'rtept') as $routePoint) {
245
                /** @noinspection SpellCheckingInspection */
246
                $components[] = $this->parsePoint($routePoint);
247
            }
248
            $line = new LineString($components);
249
            $line->setData($this->parseNodeProperties($rte, $this->gpxTypes->get('rteType')));
250
            $line->setData('gpxType', 'route');
251
            $lines[] = $line;
252
        }
253
        return $lines;
254
    }
255
256
    /**
257
     * Parses a DOMNode and returns its content in a multidimensional associative array
258
     * eg: <wpt><name>Test</name><link href="example.com"><text>Example</text></link></wpt>
259
     * to: ['name' => 'Test', 'link' => ['text'] => 'Example', '@attributes' => ['href' => 'example.com']]
260
     *
261
     * @param \DOMNode $node
262
     * @param string[]|null $tagList
263
     * @return array|string
264
     */
265
    protected static function parseNodeProperties($node, $tagList = null)
266
    {
267
        if ($node->nodeType === XML_TEXT_NODE) {
268
            return $node->nodeValue;
269
        }
270
        $result = [];
271
        foreach ($node->childNodes as $childNode) {
272
            /** @var \DOMNode $childNode */
273
            if ($childNode->hasChildNodes()) {
274
                if ($tagList === null || in_array($childNode->nodeName, $tagList ?: [])) {
275
                    if ($node->firstChild->nodeName == $node->lastChild->nodeName && $node->childNodes->length > 1) {
276
                        $result[$childNode->nodeName][] = self::parseNodeProperties($childNode);
277
                    } else {
278
                        $result[$childNode->nodeName] = self::parseNodeProperties($childNode);
279
                    }
280
                }
281
            } elseif ($childNode->nodeType === 1 && in_array($childNode->nodeName, $tagList ?: [])) {
282
                $result[$childNode->nodeName] = self::parseNodeProperties($childNode);
283
            } elseif ($childNode->nodeType === 3) {
284
                $result = $childNode->nodeValue;
285
            }
286
        }
287
        if ($node->hasAttributes()) {
288
            if (is_string($result)) {
289
                // As of the GPX specification text node cannot have attributes, thus this never happens
290
                $result = ['#text' => $result];
291
            }
292
            $attributes = [];
293
            foreach ($node->attributes as $attribute) {
294
                if ($attribute->name !== 'lat' && $attribute->name !== 'lon' && trim($attribute->value) !== '') {
295
                    $attributes[$attribute->name] = trim($attribute->value);
296
                }
297
            }
298
            if (count($attributes)) {
299
                $result['@attributes'] = $attributes;
300
            }
301
        }
302
        return $result;
303
    }
304
305
306
    /**
307
     * Serialize geometries into a GPX string.
308
     *
309
     * @param Geometry|GeometryCollection $geometry
310
     * @param string|null $namespace
311
     * @param array|null $allowedElements Which elements can be added to each GPX type
312
     *                   If not specified, every element defined in the GPX specification can be added
313
     *                   Can be overwritten with an associative array, with type name in keys.
314
     *                   eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null]
315
     * @return string The GPX string representation of the input geometries
316
     */
317
    public function write(Geometry $geometry, $namespace = null, $allowedElements = null)
318
    {
319
        if ($namespace) {
320
            $this->nss = $namespace . ':';
321
        }
322
        $this->gpxTypes = new GpxTypes($allowedElements);
323
324
        return
325
        '<?xml version="1.0" encoding="UTF-8"?>
326
<' . $this->nss . 'gpx creator="geoPHP" version="1.1"
327
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
328
  xmlns="http://www.topografix.com/GPX/1/1"
329
  xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" >
330
331
' . $this->geometryToGPX($geometry) .
332
        '</' . $this->nss . 'gpx>
333
';
334
    }
335
336
    /**
337
     * @param Geometry|Collection $geometry
338
     * @return string
339
     */
340
    protected function geometryToGPX($geometry)
341
    {
342
        switch ($geometry->geometryType()) {
343
            case Geometry::POINT:
344
                /** @var Point $geometry */
345
                return $this->pointToGPX($geometry);
346
            case Geometry::LINE_STRING:
347
            case Geometry::MULTI_LINE_STRING:
348
                /** @var LineString $geometry */
349
                return $this->linestringToGPX($geometry);
350
            case Geometry::POLYGON:
351
            case Geometry::MULTI_POINT:
352
            case Geometry::MULTI_POLYGON:
353
            case Geometry::GEOMETRY_COLLECTION:
354
                return $this->collectionToGPX($geometry);
355
        }
356
        return '';
357
    }
358
359
    /**
360
     * @param Point $geom
361
     * @param string $tag Can be "wpt", "trkpt" or "rtept"
362
     * @return string
363
     */
364
    private function pointToGPX($geom, $tag = 'wpt')
365
    {
366
        if ($geom->isEmpty() || ($tag === 'wpt' && !in_array($tag, $this->gpxTypes->get('gpxType')))) {
367
            return '';
368
        }
369
        $indent = $tag === 'trkpt' ? "\t\t" : ($tag === 'rtept' ? "\t" : '');
370
371
        if ($geom->hasZ() || $geom->getData() !== null) {
372
            $node = $indent . "<" . $this->nss . $tag . " lat=\"" . $geom->getY() . "\" lon=\"" . $geom->getX() . "\">\n";
373
            if ($geom->hasZ()) {
374
                $geom->setData('ele', $geom->z());
375
            }
376
            $node .= self::processGeometryData($geom, $this->gpxTypes->get($tag . 'Type'), $indent . "\t") .
377
                    $indent . "</" . $this->nss . $tag . ">\n";
378
            if ($geom->hasZ()) {
379
                $geom->setData('ele', null);
380
            }
381
            return $node;
382
        }
383
        return $indent . "<" . $this->nss . $tag . " lat=\"" . $geom->getY() . "\" lon=\"" . $geom->getX() . "\" />\n";
384
    }
385
386
    /**
387
     * Writes a LineString or MultiLineString to the GPX
388
     *
389
     * The (Multi)LineString will be included in a <trk></trk> block
390
     * The LineString or each LineString of the MultiLineString will be in <trkseg> </trkseg> inside the <trk>
391
     *
392
     * @param LineString|MultiLineString $geom
393
     * @return string
394
     */
395
    private function linestringToGPX($geom)
396
    {
397
        $isTrack = $geom->getData('gpxType') === 'route' ? false : true;
398
        if ($geom->isEmpty() || !in_array($isTrack ? 'trk' : 'rte', $this->gpxTypes->get('gpxType'))) {
399
            return '';
400
        }
401
402
        if ($isTrack) { // write as <trk>
403
404
            /** @noinspection SpellCheckingInspection */
405
            $gpx = "<" . $this->nss . "trk>\n" . self::processGeometryData($geom, $this->gpxTypes->get('trkType'));
406
            $components = $geom->geometryType() === 'LineString' ? [$geom] : $geom->getComponents();
407
            foreach ($components as $lineString) {
408
                $gpx .= "\t<" . $this->nss . "trkseg>\n";
409
                foreach ($lineString->getPoints() as $point) {
410
                    $gpx .= $this->pointToGPX($point, 'trkpt');
411
                }
412
                $gpx .= "\t</" . $this->nss . "trkseg>\n";
413
            }
414
            /** @noinspection SpellCheckingInspection */
415
            $gpx .= "</" . $this->nss . "trk>\n";
416
        } else {    // write as <rte>
417
418
            /** @noinspection SpellCheckingInspection */
419
            $gpx = "<" . $this->nss . "rte>\n" . self::processGeometryData($geom, $this->gpxTypes->get('rteType'));
420
            foreach ($geom->getPoints() as $point) {
421
                $gpx .= $this->pointToGPX($point, 'rtept');
422
            }
423
            /** @noinspection SpellCheckingInspection */
424
            $gpx .= "</" . $this->nss . "rte>\n";
425
        }
426
427
        return $gpx;
428
    }
429
430
    /**
431
     * @param Collection $geometry
432
     * @return string
433
     */
434
    public function collectionToGPX($geometry)
435
    {
436
        $metadata = self::processGeometryData($geometry, $this->gpxTypes->get('metadataType'));
437
        $metadata = empty($metadata) || !in_array('metadataType', $this->gpxTypes->get('gpxType'))
438
                ? ''
439
                : "<metadata>\n{$metadata}</metadata>\n\n";
440
        $wayPoints = $routes = $tracks = "";
441
442
        foreach ($geometry->getComponents() as $component) {
443
            if (strpos($component->geometryType(), 'Point') !== false) {
444
                $wayPoints .= $this->geometryToGPX($component);
445
            }
446
            if (strpos($component->geometryType(), 'LineString') !== false && $component->getData('gpxType') === 'route') {
447
                $routes .= $this->geometryToGPX($component);
448
            }
449
            if (strpos($component->geometryType(), 'LineString') !== false && $component->getData('gpxType') !== 'route') {
450
                $tracks .= $this->geometryToGPX($component);
451
            }
452
            if (strpos($component->geometryType(), 'Point') === false && strpos($component->geometryType(), 'LineString') === false) {
453
                return $this->geometryToGPX($component);
454
            }
455
        }
456
457
        return $metadata . $wayPoints . $routes . $tracks;
458
    }
459
460
    /**
461
     * @param Geometry $geometry
462
     * @param string[] $tagList Allowed tags
463
     * @param string $indent
464
     * @return string
465
     */
466
    protected static function processGeometryData($geometry, $tagList, $indent = "\t")
467
    {
468
        $tags = '';
469
        if ($geometry->getData() !== null) {
470
            foreach ($tagList as $tagName) {
471
                if ($geometry->hasDataProperty($tagName)) {
472
                    $tags .= self::createNodes($tagName, $geometry->getData($tagName), $indent) . "\n";
473
                }
474
            }
475
        }
476
        return $tags;
477
    }
478
479
    /**
480
     * @param string $tagName
481
     * @param string|array $value
482
     * @param string $indent
483
     * @return string
484
     */
485
    protected static function createNodes($tagName, $value, $indent)
486
    {
487
        $attributes = '';
488
        if (!is_array($value)) {
489
            $returnValue = $value;
490
        } else {
491
            $returnValue = '';
492
            if (array_key_exists('@attributes', $value)) {
493
                $attributes = '';
494
                foreach ($value['@attributes'] as $attributeName => $attributeValue) {
495
                    $attributes .= ' ' . $attributeName . '="' . $attributeValue . '"';
496
                }
497
                unset($value['@attributes']);
498
            }
499
            foreach ($value as $subKey => $subValue) {
500
                $returnValue .= "\n" . self::createNodes($subKey, $subValue, $indent . "\t") . "\n" . $indent;
501
            }
502
        }
503
        return $indent . "<{$tagName}{$attributes}>{$returnValue}</{$tagName}>";
504
    }
505
}
506