]>
Commit | Line | Data |
---|---|---|
88564339 AS |
1 | <?php |
2 | /** | |
3 | * gpxreader.php | |
4 | * | |
5 | * Copyright 2018 Andy Street <andy@street.me.uk> | |
6 | * | |
7 | * This program is free software; you can redistribute it and/or modify | |
8 | * it under the terms of the GNU General Public License as published by | |
9 | * the Free Software Foundation; either version 2 of the License, or | |
10 | * (at your option) any later version. | |
11 | * | |
12 | * This program is distributed in the hope that it will be useful, | |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | * GNU General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with this program; if not, write to the Free Software | |
19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | |
20 | * MA 02110-1301, USA. | |
21 | * | |
22 | * | |
23 | */ | |
24 | ||
25 | namespace libgpx; | |
26 | ||
27 | use \DateTime; | |
28 | use \DateTimeZone; | |
f528d248 | 29 | use \DomainException; |
88564339 AS |
30 | use \Exception; |
31 | use \InvalidArgumentException; | |
32 | use \RuntimeException; | |
33 | ||
34 | /** | |
35 | * Read GPX files. | |
36 | * | |
37 | * @author Andy Street <andy@street.me.uk> | |
38 | */ | |
39 | class GPXReader | |
40 | { | |
41 | ||
42 | /** | |
43 | * The namespace of the XML currently being parsed. | |
44 | * | |
45 | * @var string | |
46 | */ | |
47 | protected $ns; | |
48 | ||
49 | /** | |
50 | * The GPX version of the XML currently being parsed. | |
51 | * | |
52 | * @var string | |
53 | */ | |
54 | protected $version; | |
55 | ||
56 | /** | |
57 | * The reader currently reading the XML. | |
58 | * | |
59 | * @var XMLReader | |
60 | */ | |
61 | protected $xml; | |
62 | ||
63 | /** | |
64 | * Read GPX from a string. | |
65 | * | |
66 | * @param string $xml The GPX XML data. | |
67 | * @return GPX The GPX data. | |
68 | * @throws RuntimeException If the XML could not be read. | |
69 | * @throws MalformedGPXException If the GPX file is invalid. | |
70 | */ | |
71 | public function readString(string $xml) | |
72 | { | |
73 | try { | |
74 | $this->xml = new XMLReader(); | |
75 | if (!$this->xml->XML($xml, null, LIBXML_NOERROR | LIBXML_NOWARNING)) { | |
76 | throw new RuntimeException('Unable to read XML from string.'); | |
77 | } | |
78 | return $this->read(); | |
79 | } finally { | |
80 | $this->xml = null; | |
81 | } | |
82 | } | |
83 | ||
84 | /** | |
85 | * Read GPX from a file. | |
86 | * | |
87 | * This function is an alias of `GPXReader::readURI()`. | |
88 | * | |
89 | * @param string $filename The name of the file containing GPX XML data. | |
90 | * @return GPX The GPX data. | |
91 | * @throws RuntimeException If the XML could not be read. | |
92 | * @throws MalformedGPXException If the GPX file is invalid. | |
93 | */ | |
94 | public function readFile(string $filename) | |
95 | { | |
96 | return $this->readURI($filename); | |
97 | } | |
98 | ||
99 | /** | |
100 | * Read GPX from a URI. | |
101 | * | |
102 | * @param string $uri The location of the file containing GPX XML data. | |
103 | * @return GPX The GPX data. | |
104 | * @throws RuntimeException If the XML could not be read. | |
105 | * @throws MalformedGPXException If the GPX file is invalid. | |
106 | */ | |
107 | public function readURI(string $uri) | |
108 | { | |
109 | try { | |
110 | $this->xml = new XMLReader(); | |
111 | if (!$this->xml->open($uri, null, LIBXML_NOERROR | LIBXML_NOWARNING)) { | |
112 | throw new RuntimeException(sprintf('Unable to read XML from: %s', $uri)); | |
113 | } | |
114 | return $this->read(); | |
115 | } finally { | |
116 | $this->xml = null; | |
117 | } | |
118 | } | |
119 | ||
120 | /** | |
121 | * Read GPX data from XML. | |
122 | * | |
123 | * @param XMLReader $xml Where to read the GPX data from. | |
124 | * @return GPX The data that was read. | |
125 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
126 | */ | |
127 | protected function read() | |
128 | { | |
129 | $xml = $this->xml; | |
130 | try { | |
131 | // Fast forward to the root element. | |
132 | while ($xml->nodeType !== XMLReader::ELEMENT) { | |
133 | $xml->read(); | |
134 | } | |
135 | ||
136 | // Detect file version | |
137 | switch ($xml->namespaceURI) { | |
138 | case libgpx::NAMESPACE_GPX_1_0: | |
139 | $this->version = '1.0'; | |
140 | $this->ns = libgpx::NAMESPACE_GPX_1_0; | |
141 | break; | |
142 | case libgpx::NAMESPACE_GPX_1_1: | |
143 | $this->version = '1.1'; | |
144 | $this->ns = libgpx::NAMESPACE_GPX_1_1; | |
145 | break; | |
146 | default: | |
147 | throw new MalformedGPXException('Unknown or unsupported file format.'); | |
148 | } | |
149 | ||
150 | // Read GPX element. | |
151 | if ($xml->localName == 'gpx') | |
152 | return $this->readGPX(); | |
153 | else | |
154 | throw new MalformedGPXException('Root element must be "gpx".'); | |
155 | ||
156 | } finally { | |
157 | $this->version = null; | |
158 | $this->ns = null; | |
159 | } | |
160 | } | |
161 | ||
162 | /** | |
163 | * Read a gpx type. | |
164 | * | |
165 | * @return GPX The GPX data. | |
166 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
167 | */ | |
168 | protected function readGPX() | |
169 | { | |
170 | // Sanity check - ensure version attribute and schema declaration match. | |
171 | if ($this->xml->getAttribute('version') != $this->version) | |
172 | throw new MalformedGPXException( | |
173 | 'GPX version attribute does not match namespace declaration.' | |
174 | ); | |
175 | ||
176 | $result = new GPX(); | |
177 | $result->setCreator($this->xml->getAttribute('creator')); | |
178 | ||
179 | $struct = [ | |
f528d248 AS |
180 | 'elements' => [ |
181 | 'wpt' => function ($gpx) { | |
182 | $gpx->getWaypoints()[] = $this->readWpt(); | |
f17d2566 AS |
183 | }, |
184 | 'rte' => function ($gpx) { | |
185 | $gpx->getRoutes()[] = $this->readRte(); | |
35c309cc AS |
186 | }, |
187 | 'trk' => function ($gpx) { | |
188 | $gpx->getTracks()[] = $this->readTrk(); | |
f528d248 AS |
189 | } |
190 | ] | |
88564339 AS |
191 | ]; |
192 | if ($this->version == '1.1') { | |
193 | $struct['elements']['metadata'] = function ($gpx) { | |
194 | $this->readMetadata($gpx); | |
195 | }; | |
196 | } else { | |
197 | $struct['elements']['name'] = function ($gpx) { | |
198 | $gpx->setName($this->readXSDString()); | |
199 | }; | |
200 | $struct['elements']['desc'] = function ($gpx) { | |
201 | $gpx->setDesc($this->readXSDString()); | |
202 | }; | |
203 | $struct['elements']['author'] = function ($gpx) { | |
204 | $author = $gpx->getAuthor(); | |
205 | if ($author === null) { | |
206 | $author = new Person(); | |
207 | $gpx->setAuthor($author); | |
208 | } | |
209 | $author->setName($this->readXSDString()); | |
210 | }; | |
211 | $struct['elements']['email'] = function ($gpx) { | |
212 | $author = $gpx->getAuthor(); | |
213 | if ($author === null) { | |
214 | $author = new Person(); | |
215 | $gpx->setAuthor($author); | |
216 | } | |
217 | $author->setEmail($this->readXSDString()); | |
218 | }; | |
219 | $struct['elements']['url'] = function ($gpx, &$state) { | |
220 | $href = $this->readXSDString(); | |
221 | $links = $gpx->getLinks(); | |
f528d248 | 222 | if ($links->isEmpty()) { |
88564339 AS |
223 | $link = new Link($href); |
224 | if (isset($state['urlname'])) { | |
225 | $link->setText($state['urlname']); | |
226 | unset($state['urlname']); | |
227 | } | |
7c465a82 AS |
228 | $links[] = $link; |
229 | } else { | |
230 | $links->bottom()->setHref($href); | |
88564339 AS |
231 | } |
232 | }; | |
233 | $struct['elements']['urlname'] = function ($gpx, &$state) { | |
234 | $text = $this->readXSDString(); | |
235 | $links = $gpx->getLinks(); | |
f528d248 | 236 | if ($links->isEmpty()) { |
88564339 | 237 | $state['urlname'] = $text; |
7c465a82 AS |
238 | } else { |
239 | $links->bottom()->setText($text); | |
88564339 AS |
240 | } |
241 | }; | |
242 | $struct['elements']['time'] = function ($gpx) { | |
243 | $gpx->setTime($this->string2DateTime($this->readXSDString())); | |
244 | }; | |
245 | $struct['elements']['keywords'] = function ($gpx) { | |
7c465a82 AS |
246 | $keywords = $gpx->getKeywords(); |
247 | $words = explode(',', $this->readXSDString()); | |
248 | foreach ($words as $word) | |
249 | $keywords[] = trim($word); | |
88564339 AS |
250 | }; |
251 | $struct['elements']['bounds'] = false; | |
252 | } | |
253 | ||
254 | $this->readStruct($struct, $result); | |
255 | return $result; | |
256 | } | |
257 | ||
258 | /** | |
259 | * Read a metadata type. | |
260 | * | |
261 | * Note: This is GPX 1.1 only as GPX 1.0 does not have a metadata type. | |
262 | * | |
263 | * @param GPX $gpx The gpx object to populate. | |
264 | * @return void | |
265 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
266 | */ | |
267 | protected function readMetadata(GPX $gpx) | |
268 | { | |
269 | $struct = [ | |
270 | 'elements' => [ | |
271 | 'name' => function ($gpx) { | |
272 | $gpx->setName($this->readXSDString()); | |
273 | }, | |
274 | 'desc' => function ($gpx) { | |
275 | $gpx->setDesc($this->readXSDString()); | |
276 | }, | |
277 | 'author' => function ($gpx) { | |
278 | $gpx->setAuthor($this->readPerson()); | |
279 | }, | |
280 | 'copyright' => function ($gpx) { | |
281 | $gpx->setCopyright($this->readCopyright()); | |
282 | }, | |
283 | 'link' => function ($gpx) { | |
7c465a82 AS |
284 | $links = $gpx->getLinks(); |
285 | $links[] = $this->readLink(); | |
88564339 AS |
286 | }, |
287 | 'time' => function ($gpx) { | |
288 | $gpx->setTime($this->string2DateTime($this->readXSDString())); | |
289 | }, | |
290 | 'keywords' => function ($gpx) { | |
7c465a82 AS |
291 | $keywords = $gpx->getKeywords(); |
292 | $words = explode(',', $this->readXSDString()); | |
293 | foreach ($words as $word) | |
294 | $keywords[] = trim($word); | |
88564339 | 295 | }, |
7c465a82 AS |
296 | 'bounds' => false, // Not required as it is calculated internally. |
297 | 'extensions' => function ($gpx) { | |
298 | $this->readExtension($gpx->getMetadataExtensions()); | |
299 | } | |
88564339 AS |
300 | ] |
301 | ]; | |
302 | $this->readStruct($struct, $gpx); | |
303 | } | |
304 | ||
305 | /** | |
306 | * Read a person type. | |
307 | * | |
308 | * @return Person The person data. | |
309 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
310 | */ | |
311 | protected function readPerson() | |
312 | { | |
313 | $struct = [ | |
314 | 'elements' => [ | |
315 | 'name' => function ($person) { | |
316 | $person->setName($this->readXSDString()); | |
317 | }, | |
318 | 'email' => function ($person) { | |
319 | $id = $this->xml->getAttribute('id'); | |
320 | if ($id === null) | |
321 | throw new MalformedGPXException( | |
322 | 'Missing required attribute "id" in "email"' | |
323 | ); | |
324 | $domain = $this->xml->getAttribute('domain'); | |
325 | if ($domain === null) | |
326 | throw new MalformedGPXException( | |
327 | 'Missing required attribute "domain" in "email"' | |
328 | ); | |
329 | $person->setEmail($id . '@' . $domain); | |
330 | $this->xml->read(); | |
331 | }, | |
332 | 'link' => function ($person) { | |
333 | $person->setLink($this->readLink()); | |
334 | } | |
335 | ] | |
336 | ]; | |
337 | $person = new Person(); | |
338 | $this->readStruct($struct, $person); | |
339 | return $person; | |
340 | } | |
341 | ||
342 | /** | |
343 | * Read a link type. | |
344 | * | |
345 | * @return Link The link data. | |
346 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
347 | */ | |
348 | protected function readLink() | |
349 | { | |
350 | $href = $this->xml->getAttribute('href'); | |
351 | if ($href === null) | |
352 | throw new MalformedGPXException( | |
353 | 'Missing required attribute "href" in "link"' | |
354 | ); | |
355 | $struct = [ | |
356 | 'elements' => [ | |
357 | 'text' => function ($link) { | |
358 | $link->setText($this->readXSDString()); | |
359 | }, | |
360 | 'type' => function ($link) { | |
361 | $link->setType($this->readXSDString()); | |
362 | } | |
363 | ] | |
364 | ]; | |
365 | $link = new Link($href); | |
366 | $this->readStruct($struct, $link); | |
367 | return $link; | |
368 | } | |
369 | ||
370 | /** | |
371 | * Read a copyright type. | |
372 | * | |
373 | * @return Copyright The copyright data. | |
374 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
375 | */ | |
376 | protected function readCopyright() | |
377 | { | |
378 | $author = $this->xml->getAttribute('author'); | |
379 | if ($author === null) | |
380 | throw new MalformedGPXException( | |
381 | 'Missing required attribute "author" in "copyright"' | |
382 | ); | |
383 | $struct = [ | |
384 | 'elements' => [ | |
385 | 'year' => function ($copyright) { | |
386 | try { | |
387 | $year = $this->readXSDString(); | |
388 | $copyright->setYear($year); | |
389 | } catch (InvalidArgumentException $e) { | |
390 | throw new MalformedGPXException( | |
391 | sprintf('"%s" is not a valid year', $year) | |
392 | ); | |
393 | } | |
394 | }, | |
395 | 'license' => function ($copyright) { | |
396 | $copyright->setLicense($this->readXSDString()); | |
397 | } | |
398 | ] | |
399 | ]; | |
400 | $copyright = new Copyright($author); | |
401 | $this->readStruct($struct, $copyright); | |
402 | return $copyright; | |
403 | } | |
404 | ||
7c465a82 AS |
405 | /** |
406 | * Read an extensions type. | |
407 | * | |
408 | * This is suitable for GPX 1.1 only as private elements are mixed into other | |
409 | * elements in GPX 1.0. | |
410 | * | |
411 | * @param TypedDoublyLinkedList $list The list to fill with extension XML. | |
412 | * @return void | |
413 | * @throws MalformedGPXException If the GPX file was invalid. | |
414 | */ | |
415 | protected function readExtension(TypedDoublyLinkedList $list) | |
416 | { | |
417 | $struct = [ | |
418 | 'extensions' => function ($list) { | |
419 | $list[] = $this->xml->readOuterXml(); | |
420 | $this->xml->next(); | |
421 | } | |
422 | ]; | |
423 | $this->readStruct($struct, $list); | |
424 | } | |
425 | ||
f528d248 AS |
426 | /** |
427 | * Read a wpt type. | |
428 | * | |
429 | * @see https://www.topografix.com/GPX/1/1/#type_wptType | |
430 | * | |
431 | * @return Point | |
432 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
433 | */ | |
434 | protected function readWpt() | |
435 | { | |
436 | try { | |
437 | $result = new Point( | |
438 | $this->string2float($this->xml->getAttribute('lat')), | |
439 | $this->string2float($this->xml->getAttribute('lon')) | |
440 | ); | |
09a36623 AS |
441 | $struct = $this->getDataTypeStruct(); |
442 | $struct['elements']['ele'] = function ($point) { | |
443 | $point->setEle($this->string2float($this->readXSDString())); | |
444 | }; | |
445 | $struct['elements']['time'] = function ($point) { | |
446 | $point->setTime($this->string2DateTime($this->readXSDString())); | |
447 | }; | |
448 | $struct['elements']['magvar'] = function ($point) { | |
449 | $point->setMagvar($this->string2float($this->readXSDString())); | |
450 | }; | |
451 | $struct['elements']['geoidheight'] = function ($point) { | |
452 | $point->setGeoidHeight($this->string2float($this->readXSDString())); | |
453 | }; | |
454 | $struct['elements']['sym'] = function ($point) { | |
455 | $point->setSymbol($this->readXSDString()); | |
456 | }; | |
457 | // "type" only appears in "wpt" in GPX 1.0 | |
458 | if ($this->version == '1.0') | |
459 | $struct['elements']['type'] = function ($point) { | |
460 | $point->setType($this->readXSDString()); | |
f528d248 | 461 | }; |
09a36623 AS |
462 | $struct['elements']['fix'] = function ($point) { |
463 | $point->setFix($this->readXSDString()); | |
464 | }; | |
465 | $struct['elements']['sat'] = function ($point) { | |
466 | $point->setSatellites($this->string2int($this->readXSDString())); | |
467 | }; | |
468 | $struct['elements']['hdop'] = function ($point) { | |
469 | $point->setHdop($this->string2float($this->readXSDString())); | |
470 | }; | |
471 | $struct['elements']['vdop'] = function ($point) { | |
472 | $point->setVdop($this->string2float($this->readXSDString())); | |
473 | }; | |
474 | $struct['elements']['pdop'] = function ($point) { | |
475 | $point->setPdop($this->string2float($this->readXSDString())); | |
476 | }; | |
477 | $struct['elements']['ageofdgpsdata'] = function ($point) { | |
478 | $point->setAgeOfDGPSData($this->string2float($this->readXSDString())); | |
479 | }; | |
480 | $struct['elements']['dgpsid'] = function ($point) { | |
481 | $point->setDGPSId($this->string2int($this->readXSDString())); | |
482 | }; | |
f528d248 | 483 | $this->readStruct($struct, $result); |
f17d2566 AS |
484 | } catch (DomainException $e) { |
485 | throw new MalformedGPXException( | |
486 | $e->getMessage(), $e->getCode(), $e | |
487 | ); | |
488 | } | |
489 | return $result; | |
490 | } | |
491 | ||
492 | /** | |
493 | * Read a rte type. | |
494 | * | |
495 | * @see https://www.topografix.com/GPX/1/1/#type_rteType | |
496 | * | |
497 | * @return Route | |
498 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
499 | */ | |
500 | protected function readRte() | |
501 | { | |
502 | try { | |
503 | $result = new Route(); | |
09a36623 AS |
504 | $struct = $this->getDataTypeStruct(); |
505 | $struct['elements']['number'] = function ($route) { | |
506 | $route->setNumber($this->string2int($this->readXSDString())); | |
507 | }; | |
508 | $struct['elements']['rtept'] = function ($route) { | |
509 | $route->getPoints()[] = $this->readWpt(); | |
510 | }; | |
f17d2566 | 511 | $this->readStruct($struct, $result); |
f528d248 AS |
512 | } catch (DomainException $e) { |
513 | throw new MalformedGPXException( | |
514 | $e->getMessage(), $e->getCode(), $e | |
515 | ); | |
516 | } | |
517 | return $result; | |
518 | } | |
519 | ||
35c309cc AS |
520 | /** |
521 | * Read a trk type. | |
522 | * | |
523 | * @see https://www.topografix.com/GPX/1/1/#type_trkType | |
524 | * | |
525 | * @return Track | |
526 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
527 | */ | |
528 | protected function readTrk() | |
529 | { | |
530 | try { | |
531 | $result = new Track(); | |
532 | $struct = $this->getDataTypeStruct(); | |
533 | $struct['elements']['number'] = function ($track) { | |
534 | $track->setNumber($this->string2int($this->readXSDString())); | |
535 | }; | |
536 | $struct['elements']['trkseg'] = function ($track) { | |
537 | $track->getSegments()[] = $this->readTrkseg(); | |
538 | }; | |
539 | $this->readStruct($struct, $result); | |
540 | } catch (DomainException $e) { | |
541 | throw new MalformedGPXException( | |
542 | $e->getMessage(), $e->getCode(), $e | |
543 | ); | |
544 | } | |
545 | return $result; | |
546 | } | |
547 | ||
548 | /** | |
549 | * Read a trkseg type. | |
550 | * | |
551 | * @see https://www.topografix.com/GPX/1/1/#type_trksegType | |
552 | * | |
553 | * @return TrackSegment | |
554 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
555 | */ | |
556 | protected function readTrkseg() | |
557 | { | |
558 | $result = new TrackSegment(); | |
559 | $struct = [ | |
560 | 'elements' => [ | |
561 | 'trkpt' => function ($segment) { | |
562 | $segment->getPoints()[] = $this->readWpt(); | |
563 | } | |
564 | ] | |
565 | ]; | |
566 | if ($this->version == '1.1') | |
567 | $struct['elements']['extensions'] = function ($segment) { | |
568 | $this->readExtension($segment->getExtensions()); | |
569 | }; | |
570 | $this->readStruct($struct, $result); | |
571 | return $result; | |
572 | } | |
573 | ||
88564339 AS |
574 | /** |
575 | * Read a data structure from XML. | |
576 | * | |
577 | * This is a generalized function for parsing XML data structures and | |
578 | * populating values based on data retrieved. The main control element is the | |
579 | * `$struct` array which holds a list of callable functions which are used to | |
580 | * modify the supplied object. The structure of the array is: | |
581 | * | |
582 | * [ | |
7c465a82 | 583 | * 'extensions' => function($object, &$state) {...}, |
88564339 | 584 | * 'elements' => [ |
7c465a82 | 585 | * 'nodename' => function($object, &$state) {...} |
88564339 AS |
586 | * ] |
587 | * ] | |
588 | * | |
7c465a82 AS |
589 | * The following keys are defined: |
590 | * | |
591 | * * `extensions` - Call the supplied anonymous function if any XML element | |
592 | * is encountered in a namespace other than the document | |
593 | * namespace. | |
594 | * * `elements` - Call the supplied anonymous function if an XML element | |
595 | * is encountered in the document namespace with a matching | |
596 | * node name. | |
597 | * | |
88564339 AS |
598 | * The anonymous function parameters are as follows: |
599 | * | |
600 | * * `$object` - The object to be populated (as passed to this function) | |
601 | * * `$state` - An array that can be used to store data temporarily while | |
602 | * the structure is being written. This is useful if a value | |
603 | * that is being read is split across several XML elements. | |
604 | * | |
605 | * @param array $struct a data structure as defined above. | |
606 | * @param object $object The object to populate with data. | |
607 | * @return void | |
608 | * @throws MalformedGPXException If the GPX file was invalid or not supported. | |
609 | */ | |
610 | protected function readStruct($struct, $object) | |
611 | { | |
612 | $xml = $this->xml; | |
613 | if ($xml->isEmptyElement) { | |
614 | $xml->read(); | |
615 | return; | |
616 | } | |
617 | $element = $xml->localName; | |
618 | $xml->read(); | |
619 | $state = []; | |
620 | while (true) { | |
621 | if ( | |
622 | $xml->nodeType == XMLReader::END_ELEMENT | |
623 | && $xml->namespaceURI == $this->ns | |
624 | && $xml->localName == $element | |
625 | ) return; | |
626 | if ($xml->nodeType != XMLReader::ELEMENT) { | |
627 | $xml->read(); | |
628 | continue; | |
629 | } | |
630 | if ( | |
631 | $xml->namespaceURI == $this->ns | |
632 | && isset($struct['elements'][$xml->localName]) | |
633 | ) { | |
634 | if (is_callable($struct['elements'][$xml->localName])) { | |
635 | $struct['elements'][$xml->localName]($object, $state); | |
636 | } else { | |
637 | $xml->read(); | |
638 | } | |
639 | continue; | |
640 | } | |
7c465a82 AS |
641 | if ( |
642 | $xml->namespaceURI != $this->ns | |
643 | && isset($struct['extensions']) | |
644 | && is_callable($struct['extensions']) | |
645 | ) { | |
646 | $struct['extensions']($object, $state); | |
647 | continue; | |
648 | } | |
88564339 AS |
649 | throw new MalformedGPXException( |
650 | sprintf( | |
651 | 'Unknown element "%s" in element "%s"', | |
652 | $xml->localName, | |
653 | $element | |
654 | ) | |
655 | ); | |
656 | } | |
657 | } | |
658 | ||
09a36623 AS |
659 | /** |
660 | * Generate a data structure for reading DataType elements. | |
661 | * | |
662 | * @return array | |
663 | */ | |
664 | protected function getDataTypeStruct() | |
665 | { | |
666 | $result = [ | |
667 | 'elements' => [ | |
668 | 'name' => function ($type) { | |
669 | $type->setName($this->readXSDString()); | |
670 | }, | |
671 | 'cmt' => function ($type) { | |
672 | $type->setComment($this->readXSDString()); | |
673 | }, | |
674 | 'desc' => function ($type) { | |
675 | $type->setDescription($this->readXSDString()); | |
676 | }, | |
677 | 'src' => function ($type) { | |
678 | $type->setSource($this->readXSDString()); | |
679 | } | |
680 | ] | |
681 | ]; | |
682 | if ($this->version == '1.1') { | |
683 | $result['elements']['link'] = function ($type) { | |
684 | $type->getLinks()[] = $this->readLink(); | |
685 | }; | |
686 | $result['elements']['type'] = function ($type) { | |
687 | $type->setType($this->readXSDString()); | |
688 | }; | |
689 | $result['elements']['extensions'] = function ($type) { | |
690 | $this->readExtension($type->getExtensions()); | |
691 | }; | |
692 | } else { | |
693 | $result['elements']['url'] = function ($type, &$state) { | |
694 | $href = $this->readXSDString(); | |
695 | $links = $type->getLinks(); | |
696 | if ($links->isEmpty()) { | |
697 | $link = new Link($href); | |
698 | if (isset($state['urlname'])) { | |
699 | $link->setText($state['urlname']); | |
700 | unset($state['urlname']); | |
701 | } | |
702 | $links[] = $link; | |
703 | } else { | |
704 | $links->bottom()->setHref($href); | |
705 | } | |
706 | }; | |
707 | $result['elements']['urlname'] = function ($type, &$state) { | |
708 | $text = $this->readXSDString(); | |
709 | $links = $type->getLinks(); | |
710 | if ($links->isEmpty()) { | |
711 | $state['urlname'] = $text; | |
712 | } else { | |
713 | $links->bottom()->setText($text); | |
714 | } | |
715 | }; | |
716 | $result['extensions'] = function ($type) { | |
717 | $type->getExtensions()[] = $this->xml->readOuterXML(); | |
718 | $this->xml->next(); | |
719 | }; | |
720 | } | |
721 | return $result; | |
722 | } | |
723 | ||
88564339 AS |
724 | /** |
725 | * Read an `xsd:string` type from XML. | |
726 | * | |
727 | * @return string The text string. | |
728 | * @throws MalformedGPXException If the element has non-text children. | |
729 | */ | |
730 | protected function readXSDString() | |
731 | { | |
732 | $xml = $this->xml; | |
733 | $result = ''; | |
734 | if ($xml->nodeType != XMLReader::ELEMENT || $xml->isEmptyElement) | |
735 | return $result; | |
736 | $element = $xml->name; | |
737 | while ($xml->read()) { | |
738 | switch ($xml->nodeType) { | |
739 | case XMLReader::TEXT: | |
740 | case XMLReader::CDATA: | |
741 | $result .= $xml->value; | |
742 | break; | |
743 | case XMLReader::END_ELEMENT: | |
744 | $xml->read(); | |
745 | break 2; | |
746 | default: | |
747 | throw new MalformedGPXException( | |
748 | sprintf('Element "%s" contains non-text data.', $element) | |
749 | ); | |
750 | } | |
751 | ||
752 | } | |
753 | return $result; | |
754 | } | |
755 | ||
756 | /** | |
757 | * Convert a string to DateTime. | |
758 | * | |
759 | * @param string $timestamp The timestamp to convert. | |
760 | * @return DateTime The parsed DateTime. | |
761 | * @throws MalformedGPXException If the timestamp could not be parsed. | |
762 | */ | |
763 | protected function string2DateTime(string $timestamp) | |
764 | { | |
765 | try { | |
766 | $result = new DateTime($timestamp, new DateTimeZone('Z')); | |
767 | } catch (Exception $e) { | |
768 | throw new MalformedGPXException( | |
769 | sprintf('Unknown datetime format "%s"', $timestamp) | |
770 | ); | |
771 | } | |
772 | return $result; | |
773 | } | |
774 | ||
f528d248 AS |
775 | /** |
776 | * Convert a string to a float. | |
777 | * | |
778 | * @param string $value The string value to convert. | |
779 | * @return float | |
780 | * @throws MalformedGPXException If the value is not numeric. | |
781 | */ | |
782 | protected function string2float(string $value) | |
783 | { | |
784 | if (!is_numeric($value)) | |
785 | throw new MalformedGPXException( | |
786 | sprintf('Expected decimal value but got "%s"', $value) | |
787 | ); | |
788 | return floatval($value); | |
789 | } | |
790 | ||
791 | /** | |
792 | * Convert a string to a int. | |
793 | * | |
794 | * @param string $value The string value to convert. | |
795 | * @return int | |
796 | * @throws MalformedGPXException If the value is not numeric. | |
797 | */ | |
798 | protected function string2int(string $value) | |
799 | { | |
800 | if (!is_numeric($value)) | |
801 | throw new MalformedGPXException( | |
802 | sprintf('Expected decimal value but got "%s"', $value) | |
803 | ); | |
804 | return intval($value); | |
805 | } | |
806 | ||
88564339 | 807 | } |