GPX::parsePoint()   B
last analyzed

Complexity

Conditions 9
Paths 12

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 18
c 3
b 0
f 0
dl 0
loc 28
rs 8.0555
cc 9
nc 12
nop 1
1
<?php
2
namespace geoPHP\Adapter;
3
4
use geoPHP\Geometry\Collection;
5
use geoPHP\geoPHP;
6
use geoPHP\Geometry\Geometry;
7
use geoPHP\Geometry\GeometryCollection;
8
use geoPHP\Geometry\Point;
9
use geoPHP\Geometry\LineString;
10
use geoPHP\Geometry\MultiLineString;
11
12
/*
13
 * Copyright (c) Patrick Hayes
14
 *
15
 * This code is open-source and licenced under the Modified BSD License.
16
 * For the full copyright and license information, please view the LICENSE
17
 * file that was distributed with this source code.
18
 */
19
20
/**
21
 * PHP Geometry/GPX encoder/decoder
22
 */
23
class GPX implements GeoAdapter
24
{
25
    use GPXWriter;
26
27
    /**
28
     * @var GpxTypes
29
     */
30
    protected $gpxTypes;
31
32
    /**
33
     * @var \DOMXPath
34
     */
35
    protected $xpath;
36
    
37
    /**
38
     * @var bool
39
     */
40
    protected $parseGarminRpt = false;
41
    
42
    /**
43
     * @var Point[]
44
     */
45
    protected $trackFromRoute;
46
    
47
    /**
48
     * @var bool add elevation-data to every coordinate
49
     */
50
    public $withElevation = false;
51
52
    /**
53
     * Read GPX string into geometry object
54
     *
55
     * @param string $gpx A GPX string
56
     * @param array<array> $allowedElements Which elements can be read from each GPX type
57
     *              If not specified, every element defined in the GPX specification can be read
58
     *              Can be overwritten with an associative array, with type name in keys.
59
     *              eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null]
60
     *
61
     * @return Geometry|GeometryCollection
62
     * @throws \Exception If GPX is not a valid XML
63
     */
64
    public function read(string $gpx, array $allowedElements = []): Geometry
65
    {
66
        // Converts XML tags to lower-case (DOMDocument functions are case sensitive)
67
        $gpx = preg_replace_callback("/(<\/?\w+)(.*?>)/", function ($m) {
68
            return strtolower($m[1]) . $m[2];
69
        }, $gpx);
70
71
        $this->gpxTypes = new GpxTypes($allowedElements);
72
73
        //libxml_use_internal_errors(true); // why?
74
        // Load into DOMDocument
75
        $xmlObject = new \DOMDocument('1.0', 'UTF-8');
76
        $xmlObject->preserveWhiteSpace = false;
77
        
78
        if ($xmlObject->loadXML($gpx) === false) {
79
            throw new \Exception("Invalid GPX: " . $gpx);
80
        }
81
82
        $this->parseGarminRpt = strpos($gpx, 'gpxx:rpt') > 0;
83
84
        // Initialize XPath parser if needed (currently only for Garmin extensions)
85
        if ($this->parseGarminRpt) {
86
            $this->xpath = new \DOMXPath($xmlObject);
87
            $this->xpath->registerNamespace('gpx', 'http://www.topografix.com/GPX/1/1');
88
            $this->xpath->registerNamespace('gpxx', 'http://www.garmin.com/xmlschemas/GpxExtensions/v3');
89
        }
90
91
        try {
92
            $geom = $this->geomFromXML($xmlObject);
93
        } catch (\Exception $e) {
94
            throw new \Exception("Cannot Read Geometry From GPX: " . $gpx . '<br>' . $e->getMessage());
95
        }
96
97
        return $geom;
98
    }
99
100
    /**
101
     * Parses the GPX XML and returns a geometry
102
     * @param \DOMDocument $xmlObject
103
     * @return GeometryCollection|Geometry Returns the geometry representation of the GPX (@see geoPHP::buildGeometry)
104
     */
105
    protected function geomFromXML($xmlObject): Geometry
106
    {
107
        /** @var Geometry[] $geometries */
108
        $geometries = array_merge(
109
            $this->parseWaypoints($xmlObject),
110
            $this->parseTracks($xmlObject),
111
            $this->parseRoutes($xmlObject)
112
        );
113
114
        if (isset($this->trackFromRoute)) {
115
            $trackFromRoute = new LineString($this->trackFromRoute);
116
            $trackFromRoute->setData('gpxType', 'track');
117
            $trackFromRoute->setData('type', 'planned route');
118
            $geometries[] = $trackFromRoute;
119
        }
120
121
        $geometry = geoPHP::buildGeometry($geometries);
122
        if (in_array('metadata', $this->gpxTypes->get('gpxType')) && $xmlObject->getElementsByTagName('metadata')->length === 1) {
123
            $metadata = $this->parseNodeProperties(
124
                $xmlObject->getElementsByTagName('metadata')->item(0),
125
                $this->gpxTypes->get('metadataType')
126
            );
127
            if ($geometry->getData() !== null && $metadata !== null) {
128
                $geometry = new GeometryCollection([$geometry]);
129
            }
130
            $geometry->setData($metadata);
131
        }
132
        
133
        return geoPHP::geometryReduce($geometry);
134
    }
135
136
    /**
137
     * @param \DOMNode $xml
138
     * @param string $nodeName
139
     * @return array<\DOMElement>
140
     */
141
    protected function childElements(\DOMNode $xml, string $nodeName = ''): array
142
    {
143
        $children = [];
144
        foreach ($xml->childNodes as $child) {
145
            if ($child->nodeName == $nodeName) {
146
                $children[] = $child;
147
            }
148
        }
149
        return $children;
150
    }
151
152
    /**
153
     * @param \DOMElement $node
154
     * @return Point
155
     */
156
    protected function parsePoint(\DOMElement $node): Point
157
    {
158
        $lat = null;
159
        $lon = null;
160
        
161
        if ($node->attributes !== null) {
162
            $lat = $node->attributes->getNamedItem("lat")->nodeValue;
163
            $lon = $node->attributes->getNamedItem("lon")->nodeValue;
164
        }
165
        
166
        $ele = $node->getElementsByTagName('ele');
167
        
168
        if ($this->withElevation && $ele->length) {
169
            $elevation = $ele->item(0)->nodeValue;
170
            $point = new Point($lon, $lat, $elevation <> 0 ? $elevation : null);
171
        } else {
172
            $point = new Point($lon, $lat);
173
        }
174
        $point->setData($this->parseNodeProperties($node, $this->gpxTypes->get($node->nodeName . 'Type')));
175
        if ($node->nodeName === 'rtept' && $this->parseGarminRpt) {
176
            $rpts = $this->xpath->query('.//gpx:extensions/gpxx:RoutePointExtension/gpxx:rpt', $node);
177
            if ($rpts !== false) {
178
                foreach ($rpts as $element) {
179
                    $this->trackFromRoute[] = $this->parsePoint($element);
180
                }
181
            }
182
        }
183
        return $point;
184
    }
185
186
    /**
187
     * @param \DOMDocument $xmlObject
188
     * @return Point[]
189
     */
190
    protected function parseWaypoints($xmlObject): array
191
    {
192
        if (!in_array('wpt', $this->gpxTypes->get('gpxType'))) {
193
            return [];
194
        }
195
        $points = [];
196
        $wptElements = $xmlObject->getElementsByTagName('wpt');
197
        foreach ($wptElements as $wpt) {
198
            $point = $this->parsePoint($wpt);
199
            $point->setData('gpxType', 'waypoint');
200
            $points[] = $point;
201
        }
202
        return $points;
203
    }
204
205
    /**
206
     * @param \DOMDocument $xmlObject
207
     * @return LineString[]|MultiLineString[]
208
     */
209
    protected function parseTracks($xmlObject): array
210
    {
211
        if (!in_array('trk', $this->gpxTypes->get('gpxType'))) {
212
            return [];
213
        }
214
        $tracks = [];
215
        $trkElements = $xmlObject->getElementsByTagName('trk');
216
        foreach ($trkElements as $trk) {
217
            $segments = [];
218
            /** @noinspection SpellCheckingInspection */
219
            foreach ($this->childElements($trk, 'trkseg') as $trkseg) {
220
                $points = [];
221
                /** @noinspection SpellCheckingInspection */
222
                foreach ($this->childElements($trkseg, 'trkpt') as $trkpt) {
223
                    $points[] = $this->parsePoint($trkpt);
224
                }
225
                // Avoids creating invalid LineString
226
                if (count($points) > 1) {
227
                    $segments[] = new LineString($points);
228
                }
229
            }
230
            if (!empty($segments)) {
231
                $track = count($segments) === 1 ? $segments[0] : new MultiLineString($segments);
232
                $track->setData($this->parseNodeProperties($trk, $this->gpxTypes->get('trkType')));
233
                $track->setData('gpxType', 'track');
234
                $tracks[] = $track;
235
            }
236
        }
237
238
        return $tracks;
239
    }
240
241
    /**
242
     * @param \DOMDocument $xmlObject
243
     * @return LineString[]
244
     */
245
    protected function parseRoutes($xmlObject): array
246
    {
247
        if (!in_array('rte', $this->gpxTypes->get('gpxType'))) {
248
            return [];
249
        }
250
        $lines = [];
251
        $rteElements = $xmlObject->getElementsByTagName('rte');
252
        foreach ($rteElements as $rte) {
253
            $points = [];
254
            /** @noinspection SpellCheckingInspection */
255
            foreach ($this->childElements($rte, 'rtept') as $routePoint) {
256
                /** @noinspection SpellCheckingInspection */
257
                $points[] = $this->parsePoint($routePoint);
258
            }
259
            $line = new LineString($points);
260
            $line->setData($this->parseNodeProperties($rte, $this->gpxTypes->get('rteType')));
261
            $line->setData('gpxType', 'route');
262
            $lines[] = $line;
263
        }
264
        return $lines;
265
    }
266
267
    /**
268
     * Parses a DOMNode and returns its content in a multidimensional associative array
269
     * eg: <wpt><name>Test</name><link href="example.com"><text>Example</text></link></wpt>
270
     * to: ['name' => 'Test', 'link' => ['text'] => 'Example', '@attributes' => ['href' => 'example.com']]
271
     *
272
     * @param \DOMNode $node
273
     * @param string[]|null $tagList
274
     * @return array<string>|string
275
     */
276
    protected function parseNodeProperties(\DOMNode $node, $tagList = null)
277
    {
278
        if ($node->nodeType === XML_TEXT_NODE) {
279
            return $node->nodeValue;
280
        }
281
        
282
        $result = [];
283
        
284
        // add/parse properties from childs to result
285
        if ($node->hasChildNodes()) {
286
            $this->addChildNodeProperties($result, $node, $tagList);
287
        }
288
        
289
        // add attributes to result
290
        if ($node->hasAttributes()) {
291
            $this->addNodeAttributes($result, $node);
292
        }
293
        
294
        return $result;
295
    }
296
    
297
    /**
298
     *
299
     * @param array<string, mixed>|string $result
300
     * @param \DOMNode $node
301
     * @param string[]|null $tagList
302
     * @return void
303
     */
304
    private function addChildNodeProperties(&$result, \DOMNode $node, $tagList)
305
    {
306
        foreach ($node->childNodes as $childNode) {
307
            /** @var \DOMNode $childNode */
308
            if ($childNode->hasChildNodes()) {
309
                if ($tagList === null || in_array($childNode->nodeName, $tagList ?: [])) {
310
                    if ($node->firstChild->nodeName == $node->lastChild->nodeName && $node->childNodes->length > 1) {
311
                        $result[$childNode->nodeName][] = $this->parseNodeProperties($childNode);
312
                    } else {
313
                        $result[$childNode->nodeName] = $this->parseNodeProperties($childNode);
314
                    }
315
                }
316
            } elseif ($childNode->nodeType === 1 && in_array($childNode->nodeName, $tagList ?: [])) {
317
                // node is a DOMElement
318
                $result[$childNode->nodeName] = $this->parseNodeProperties($childNode);
319
            } elseif ($childNode->nodeType === 3) {
320
                // node is a DOMText
321
                $result = $childNode->nodeValue;
322
            }
323
        }
324
    }
325
    
326
    /**
327
     *
328
     * @param array<string, mixed>|string $result
329
     * @param \DOMNode $node
330
     * @return void
331
     */
332
    private function addNodeAttributes(&$result, \DOMNode $node)
333
    {
334
        if (is_string($result)) {
335
            // As of the GPX specification text node cannot have attributes, thus this never happens
336
            $result = ['#text' => $result];
337
        }
338
        $attributes = [];
339
        foreach ($node->attributes as $attribute) {
340
            if ($attribute->name !== 'lat' && $attribute->name !== 'lon' && trim($attribute->value) !== '') {
341
                $attributes[$attribute->name] = trim($attribute->value);
342
            }
343
        }
344
        if (!empty($attributes)) {
345
            $result['@attributes'] = $attributes;
346
        }
347
    }
348
}
349