5 * Copyright 2018 Andy Street <andy@street.me.uk>
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.
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.
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,
30 use \InvalidArgumentException;
31 use \RuntimeException;
36 * @author Andy Street <andy@street.me.uk>
42 * The namespace of the XML currently being parsed.
49 * The GPX version of the XML currently being parsed.
56 * The reader currently reading the XML.
63 * Read GPX from a string.
65 * @param string $xml The GPX XML data.
66 * @return GPX The GPX data.
67 * @throws RuntimeException If the XML could not be read.
68 * @throws MalformedGPXException If the GPX file is invalid.
70 public function readString(string $xml)
73 $this->xml = new XMLReader();
74 if (!$this->xml->XML($xml, null, LIBXML_NOERROR | LIBXML_NOWARNING)) {
75 throw new RuntimeException('Unable to read XML from string.');
84 * Read GPX from a file.
86 * This function is an alias of `GPXReader::readURI()`.
88 * @param string $filename The name of the file containing GPX XML data.
89 * @return GPX The GPX data.
90 * @throws RuntimeException If the XML could not be read.
91 * @throws MalformedGPXException If the GPX file is invalid.
93 public function readFile(string $filename)
95 return $this->readURI($filename);
99 * Read GPX from a URI.
101 * @param string $uri The location of the file containing GPX XML data.
102 * @return GPX The GPX data.
103 * @throws RuntimeException If the XML could not be read.
104 * @throws MalformedGPXException If the GPX file is invalid.
106 public function readURI(string $uri)
109 $this->xml = new XMLReader();
110 if (!$this->xml->open($uri, null, LIBXML_NOERROR | LIBXML_NOWARNING)) {
111 throw new RuntimeException(sprintf('Unable to read XML from: %s', $uri));
113 return $this->read();
120 * Read GPX data from XML.
122 * @param XMLReader $xml Where to read the GPX data from.
123 * @return GPX The data that was read.
124 * @throws MalformedGPXException If the GPX file was invalid or not supported.
126 protected function read()
130 // Fast forward to the root element.
131 while ($xml->nodeType !== XMLReader::ELEMENT) {
135 // Detect file version
136 switch ($xml->namespaceURI) {
137 case libgpx::NAMESPACE_GPX_1_0:
138 $this->version = '1.0';
139 $this->ns = libgpx::NAMESPACE_GPX_1_0;
141 case libgpx::NAMESPACE_GPX_1_1:
142 $this->version = '1.1';
143 $this->ns = libgpx::NAMESPACE_GPX_1_1;
146 throw new MalformedGPXException('Unknown or unsupported file format.');
150 if ($xml->localName == 'gpx')
151 return $this->readGPX();
153 throw new MalformedGPXException('Root element must be "gpx".');
156 $this->version = null;
164 * @return GPX The GPX data.
165 * @throws MalformedGPXException If the GPX file was invalid or not supported.
167 protected function readGPX()
169 // Sanity check - ensure version attribute and schema declaration match.
170 if ($this->xml->getAttribute('version') != $this->version)
171 throw new MalformedGPXException(
172 'GPX version attribute does not match namespace declaration.'
176 $result->setCreator($this->xml->getAttribute('creator'));
181 if ($this->version == '1.1') {
182 $struct['elements']['metadata'] = function ($gpx) {
183 $this->readMetadata($gpx);
186 $struct['elements']['name'] = function ($gpx) {
187 $gpx->setName($this->readXSDString());
189 $struct['elements']['desc'] = function ($gpx) {
190 $gpx->setDesc($this->readXSDString());
192 $struct['elements']['author'] = function ($gpx) {
193 $author = $gpx->getAuthor();
194 if ($author === null) {
195 $author = new Person();
196 $gpx->setAuthor($author);
198 $author->setName($this->readXSDString());
200 $struct['elements']['email'] = function ($gpx) {
201 $author = $gpx->getAuthor();
202 if ($author === null) {
203 $author = new Person();
204 $gpx->setAuthor($author);
206 $author->setEmail($this->readXSDString());
208 $struct['elements']['url'] = function ($gpx, &$state) {
209 $href = $this->readXSDString();
210 $links = $gpx->getLinks();
211 if (isset($links[0])) {
212 $links[0]->setHref($href);
214 $link = new Link($href);
215 if (isset($state['urlname'])) {
216 $link->setText($state['urlname']);
217 unset($state['urlname']);
219 $gpx->addLink($link);
222 $struct['elements']['urlname'] = function ($gpx, &$state) {
223 $text = $this->readXSDString();
224 $links = $gpx->getLinks();
225 if (isset($links[0])) {
226 $links[0]->setText($text);
228 $state['urlname'] = $text;
231 $struct['elements']['time'] = function ($gpx) {
232 $gpx->setTime($this->string2DateTime($this->readXSDString()));
234 $struct['elements']['keywords'] = function ($gpx) {
235 $keywords = explode(',', $this->readXSDString());
236 foreach ($keywords as $keyword)
237 $gpx->addKeyword(trim($keyword));
239 $struct['elements']['bounds'] = false;
242 $this->readStruct($struct, $result);
247 * Read a metadata type.
249 * Note: This is GPX 1.1 only as GPX 1.0 does not have a metadata type.
251 * @param GPX $gpx The gpx object to populate.
253 * @throws MalformedGPXException If the GPX file was invalid or not supported.
255 protected function readMetadata(GPX $gpx)
259 'name' => function ($gpx) {
260 $gpx->setName($this->readXSDString());
262 'desc' => function ($gpx) {
263 $gpx->setDesc($this->readXSDString());
265 'author' => function ($gpx) {
266 $gpx->setAuthor($this->readPerson());
268 'copyright' => function ($gpx) {
269 $gpx->setCopyright($this->readCopyright());
271 'link' => function ($gpx) {
272 $gpx->addLink($this->readLink());
274 'time' => function ($gpx) {
275 $gpx->setTime($this->string2DateTime($this->readXSDString()));
277 'keywords' => function ($gpx) {
278 $keywords = explode(',', $this->readXSDString());
279 foreach ($keywords as $keyword)
280 $gpx->addKeyword(trim($keyword));
285 $this->readStruct($struct, $gpx);
289 * Read a person type.
291 * @return Person The person data.
292 * @throws MalformedGPXException If the GPX file was invalid or not supported.
294 protected function readPerson()
298 'name' => function ($person) {
299 $person->setName($this->readXSDString());
301 'email' => function ($person) {
302 $id = $this->xml->getAttribute('id');
304 throw new MalformedGPXException(
305 'Missing required attribute "id" in "email"'
307 $domain = $this->xml->getAttribute('domain');
308 if ($domain === null)
309 throw new MalformedGPXException(
310 'Missing required attribute "domain" in "email"'
312 $person->setEmail($id . '@' . $domain);
315 'link' => function ($person) {
316 $person->setLink($this->readLink());
320 $person = new Person();
321 $this->readStruct($struct, $person);
328 * @return Link The link data.
329 * @throws MalformedGPXException If the GPX file was invalid or not supported.
331 protected function readLink()
333 $href = $this->xml->getAttribute('href');
335 throw new MalformedGPXException(
336 'Missing required attribute "href" in "link"'
340 'text' => function ($link) {
341 $link->setText($this->readXSDString());
343 'type' => function ($link) {
344 $link->setType($this->readXSDString());
348 $link = new Link($href);
349 $this->readStruct($struct, $link);
354 * Read a copyright type.
356 * @return Copyright The copyright data.
357 * @throws MalformedGPXException If the GPX file was invalid or not supported.
359 protected function readCopyright()
361 $author = $this->xml->getAttribute('author');
362 if ($author === null)
363 throw new MalformedGPXException(
364 'Missing required attribute "author" in "copyright"'
368 'year' => function ($copyright) {
370 $year = $this->readXSDString();
371 $copyright->setYear($year);
372 } catch (InvalidArgumentException $e) {
373 throw new MalformedGPXException(
374 sprintf('"%s" is not a valid year', $year)
378 'license' => function ($copyright) {
379 $copyright->setLicense($this->readXSDString());
383 $copyright = new Copyright($author);
384 $this->readStruct($struct, $copyright);
389 * Read a data structure from XML.
391 * This is a generalized function for parsing XML data structures and
392 * populating values based on data retrieved. The main control element is the
393 * `$struct` array which holds a list of callable functions which are used to
394 * modify the supplied object. The structure of the array is:
398 * 'tagname' => function($object, &$state) {...}
402 * The anonymous function parameters are as follows:
404 * * `$object` - The object to be populated (as passed to this function)
405 * * `$state` - An array that can be used to store data temporarily while
406 * the structure is being written. This is useful if a value
407 * that is being read is split across several XML elements.
409 * @param array $struct a data structure as defined above.
410 * @param object $object The object to populate with data.
412 * @throws MalformedGPXException If the GPX file was invalid or not supported.
414 protected function readStruct($struct, $object)
417 if ($xml->isEmptyElement) {
421 $element = $xml->localName;
426 $xml->nodeType == XMLReader::END_ELEMENT
427 && $xml->namespaceURI == $this->ns
428 && $xml->localName == $element
430 if ($xml->nodeType != XMLReader::ELEMENT) {
435 $xml->namespaceURI == $this->ns
436 && isset($struct['elements'][$xml->localName])
438 if (is_callable($struct['elements'][$xml->localName])) {
439 $struct['elements'][$xml->localName]($object, $state);
445 throw new MalformedGPXException(
447 'Unknown element "%s" in element "%s"',
456 * Read an `xsd:string` type from XML.
458 * @return string The text string.
459 * @throws MalformedGPXException If the element has non-text children.
461 protected function readXSDString()
465 if ($xml->nodeType != XMLReader::ELEMENT || $xml->isEmptyElement)
467 $element = $xml->name;
468 while ($xml->read()) {
469 switch ($xml->nodeType) {
470 case XMLReader::TEXT:
471 case XMLReader::CDATA:
472 $result .= $xml->value;
474 case XMLReader::END_ELEMENT:
478 throw new MalformedGPXException(
479 sprintf('Element "%s" contains non-text data.', $element)
488 * Convert a string to DateTime.
490 * @param string $timestamp The timestamp to convert.
491 * @return DateTime The parsed DateTime.
492 * @throws MalformedGPXException If the timestamp could not be parsed.
494 protected function string2DateTime(string $timestamp)
497 $result = new DateTime($timestamp, new DateTimeZone('Z'));
498 } catch (Exception $e) {
499 throw new MalformedGPXException(
500 sprintf('Unknown datetime format "%s"', $timestamp)