<?xml version="1.0" encoding="UTF-8"?>\r
-<gpx version="1.1" creator="libgpx" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<gpx version="1.1" creator="libgpx" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:custom="https://www.example.com/schema/custom">
<metadata>\r
<name>full-1.1</name>\r
<desc>This is a sample file containing all parts of the GPX 1.1 specification supported by libgpx.</desc>\r
<time>2018-11-25T10:07:47Z</time>\r
<keywords>Example, GPX</keywords>\r
<bounds minlat="-1.234" minlon="-1.234" maxlat="1.234" maxlon="1.234"/>\r
+ <extensions>\r
+ <custom:checkedBy>Jane Doe</custom:checkedBy>\r
+ </extensions>\r
</metadata>\r
</gpx>\r
/**
* Links to other resources that describe this GPX file.
*
- * @var Link[]
+ * @var TypedDoublyLinkedList (Link)
*/
- protected $links = [];
+ protected $links;
/**
* The creation time of the file.
/**
* A list of keywords classifying this GPX file.
*
- * @var string[]
+ * @var TypedDoublyLinkedList (string)
*/
- protected $keywords = [];
+ protected $keywords;
+
+ /**
+ * A list of XML snippets describing unsupported metadata.
+ *
+ * @var TypedDoublyLinkedList (string)
+ */
+ protected $metadataExtensions;
+
+ /**
+ * Create a new instance.
+ */
+ public function __construct()
+ {
+ $this->links = new TypedDoublyLinkedList('libgpx\Link');
+ $this->keywords = new TypedDoublyLinkedList('string');
+ $this->metadataExtensions = new TypedDoublyLinkedList('string');
+ }
+
/**
* Fetch the name of the program that created the GPX file.
/**
* Fetch links to other resources.
*
- * @return Link[]
+ * @return TypedDoublyLinkedList
*/
public function getLinks()
{
return $this->links;
}
- /**
- * Add a link to an related resource.
- *
- * @param Link $link The link to add.
- * @return void
- */
- public function addLink(Link $link)
- {
- if (!in_array($link, $this->links, true))
- $this->links[] = $link;
- }
-
- /**
- * Remove an existing link from the list.
- *
- * @param Link $link The link to remove.
- * @return void
- */
- public function removeLink(Link $link)
- {
- $key = array_search($link, $this->links, true);
- if ($key !== null)
- unset($this->links[$key]);
- }
-
/**
* Fetch the time the GPX file was created.
*
/**
* Fetch a list of keywords for this GPX file.
*
- * @return string[] The keywords
+ * @return TypedDoublyLinkedList The keywords
*/
public function getKeywords()
{
}
/**
- * Add a keyword for this GPX file.
+ * Fetch a list of XML snippets describing unsupported metadata.
*
- * @param string $keyword The keyword to add.
- * @return void
- */
- public function addKeyword(string $keyword)
- {
- if (!in_array($keyword, $this->keywords))
- $this->keywords[] = $keyword;
- }
-
- /**
- * Remove an existing keyword from the list.
- *
- * @param string $keyword The keyword to remove.
- * @return void
+ * @return TypedDoublyLinkedList The keywords
*/
- public function removeKeyword(string $keyword)
+ public function getMetadataExtensions()
{
- $key = array_search($keyword, $this->keywords);
- if ($key !== null)
- unset($this->keywords[$key]);
+ return $this->metadataExtensions;
}
/**
$struct['elements']['url'] = function ($gpx, &$state) {
$href = $this->readXSDString();
$links = $gpx->getLinks();
- if (isset($links[0])) {
- $links[0]->setHref($href);
- } else {
+ if ($links->isEmpty) {
$link = new Link($href);
if (isset($state['urlname'])) {
$link->setText($state['urlname']);
unset($state['urlname']);
}
- $gpx->addLink($link);
+ $links[] = $link;
+ } else {
+ $links->bottom()->setHref($href);
}
};
$struct['elements']['urlname'] = function ($gpx, &$state) {
$text = $this->readXSDString();
$links = $gpx->getLinks();
- if (isset($links[0])) {
- $links[0]->setText($text);
- } else {
+ if ($links->isEmpty) {
$state['urlname'] = $text;
+ } else {
+ $links->bottom()->setText($text);
}
};
$struct['elements']['time'] = function ($gpx) {
$gpx->setTime($this->string2DateTime($this->readXSDString()));
};
$struct['elements']['keywords'] = function ($gpx) {
- $keywords = explode(',', $this->readXSDString());
- foreach ($keywords as $keyword)
- $gpx->addKeyword(trim($keyword));
+ $keywords = $gpx->getKeywords();
+ $words = explode(',', $this->readXSDString());
+ foreach ($words as $word)
+ $keywords[] = trim($word);
};
$struct['elements']['bounds'] = false;
}
$gpx->setCopyright($this->readCopyright());
},
'link' => function ($gpx) {
- $gpx->addLink($this->readLink());
+ $links = $gpx->getLinks();
+ $links[] = $this->readLink();
},
'time' => function ($gpx) {
$gpx->setTime($this->string2DateTime($this->readXSDString()));
},
'keywords' => function ($gpx) {
- $keywords = explode(',', $this->readXSDString());
- foreach ($keywords as $keyword)
- $gpx->addKeyword(trim($keyword));
+ $keywords = $gpx->getKeywords();
+ $words = explode(',', $this->readXSDString());
+ foreach ($words as $word)
+ $keywords[] = trim($word);
},
- 'bounds' => false
+ 'bounds' => false, // Not required as it is calculated internally.
+ 'extensions' => function ($gpx) {
+ $this->readExtension($gpx->getMetadataExtensions());
+ }
]
];
$this->readStruct($struct, $gpx);
return $copyright;
}
+ /**
+ * Read an extensions type.
+ *
+ * This is suitable for GPX 1.1 only as private elements are mixed into other
+ * elements in GPX 1.0.
+ *
+ * @param TypedDoublyLinkedList $list The list to fill with extension XML.
+ * @return void
+ * @throws MalformedGPXException If the GPX file was invalid.
+ */
+ protected function readExtension(TypedDoublyLinkedList $list)
+ {
+ $struct = [
+ 'extensions' => function ($list) {
+ $list[] = $this->xml->readOuterXml();
+ $this->xml->next();
+ }
+ ];
+ $this->readStruct($struct, $list);
+ }
+
/**
* Read a data structure from XML.
*
* modify the supplied object. The structure of the array is:
*
* [
+ * 'extensions' => function($object, &$state) {...},
* 'elements' => [
- * 'tagname' => function($object, &$state) {...}
+ * 'nodename' => function($object, &$state) {...}
* ]
* ]
*
+ * The following keys are defined:
+ *
+ * * `extensions` - Call the supplied anonymous function if any XML element
+ * is encountered in a namespace other than the document
+ * namespace.
+ * * `elements` - Call the supplied anonymous function if an XML element
+ * is encountered in the document namespace with a matching
+ * node name.
+ *
* The anonymous function parameters are as follows:
*
* * `$object` - The object to be populated (as passed to this function)
}
continue;
}
+ if (
+ $xml->namespaceURI != $this->ns
+ && isset($struct['extensions'])
+ && is_callable($struct['extensions'])
+ ) {
+ $struct['extensions']($object, $state);
+ continue;
+ }
throw new MalformedGPXException(
sprintf(
'Unknown element "%s" in element "%s"',
}
$xml->writeElement('time', $this->createTimestamp(new DateTime()));
if (!empty($gpx->getKeywords()))
- $xml->writeElement('keywords', implode(', ', $gpx->getKeywords()));
+ $xml->writeElement(
+ 'keywords',
+ implode(', ', $gpx->getKeywords()->toArray())
+ );
$bounds = $gpx->getBounds();
if ($bounds instanceof Bounds) {
$xml->startElement('bounds');
$xml->writeAttribute('maxlon', $bounds->getMaxLon());
$xml->endElement();
}
- if ($this->format == '1.1')
- $xml->endElement();
+ if ($this->format == '1.1') {
+ $extensions = $gpx->getMetadataExtensions();
+ if (!$extensions->isEmpty()) {
+ $xml->startElement('extensions');
+ foreach ($extensions as $extension) {
+ $xml->writeRaw($extension);
+ }
+ $xml->endElement();
+ }
+ $xml->endElement(); // </metadata>
+ }
}
/**
--- /dev/null
+<?php
+/**
+ * typeddoublylinkedlist.php
+ *
+ * Copyright 2018 Andy Street <andy@street.me.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *
+ *
+ */
+
+namespace libgpx;
+
+use \InvalidArgumentException;
+use \SplDoublyLinkedList;
+
+/**
+ * A doubly linked list that only allows items of one type.
+ *
+ * @author Andy Street <andy@street.me.uk>
+ */
+class TypedDoublyLinkedList extends SplDoublyLinkedList
+{
+
+ /**
+ * The variable type.
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * Create a new instance.
+ *
+ * @param string $type A basic type (as returned by `gettype` or the name of
+ * a class.
+ */
+ public function __construct(string $type)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * Get the type (or class name) of a variable.
+ *
+ * @param mixed $var The variable.
+ * @return string
+ */
+ protected function getType($var)
+ {
+ $type = gettype($var);
+ if ($type == 'object')
+ $type = get_class($var);
+ return $type;
+ }
+
+ /**
+ * Check if a value is contained within the list.
+ *
+ * @param mixed $value The value to search for.
+ * @return boolean If $value is contained in the list.
+ */
+ public function contains($value)
+ {
+ foreach ($this as $val)
+ if ($val === $value)
+ return true;
+ return false;
+ }
+
+ /**
+ * Convert linked list to an array.
+ *
+ * @return array
+ */
+ public function toArray()
+ {
+ $result = [];
+ foreach ($this as $key => $value)
+ $result[$key] = $value;
+ return $result;
+ }
+
+ /**
+ * Add/insert a new value at the specified index.
+ *
+ * @param mixed $index The index where the new value is to be inserted.
+ * @param mixed $newval The new value for the index.
+ * @return void
+ * @throws InvalidArgumentException If the type of `$newval` is not permitted.
+ */
+ public function add($index, $newval)
+ {
+ $type = $this->getType($newval);
+ if ($type != $this->type)
+ throw new InvalidArgumentException(
+ sprintf('Expecting type "%s" but got "%s"', $this->type, $type)
+ );
+ return parent::add($index, $newval);
+ }
+
+ /**
+ * Sets the value at the specified $index to $newval
+ *
+ * @param mixed $index The index being set.
+ * @param mixed $newval The new value for the index.
+ * @return void
+ * @throws InvalidArgumentException If the type of `$newval` is not permitted.
+ */
+ public function offsetSet($index, $newval)
+ {
+ $type = $this->getType($newval);
+ if ($type != $this->type)
+ throw new InvalidArgumentException(
+ sprintf('Expecting type "%s" but got "%s"', $this->type, $type)
+ );
+ return parent::offsetSet($index, $newval);
+ }
+
+ /**
+ * Pushes an element at the end of the doubly linked list.
+ *
+ * @param mixed $value The value to push.
+ * @return void
+ * @throws InvalidArgumentException If the type of `$value` is not permitted.
+ */
+ public function push($value)
+ {
+ $type = $this->getType($value);
+ if ($type != $this->type)
+ throw new InvalidArgumentException(
+ sprintf('Expecting type "%s" but got "%s"', $this->type, $type)
+ );
+ return parent::push($value);
+ }
+
+ /**
+ * Prepends the doubly linked list with an element.
+ *
+ * @param mixed $value The value to unshift.
+ * @return void
+ * @throws InvalidArgumentException If the type of `$value` is not permitted.
+ */
+ public function unshift($value)
+ {
+ $type = $this->getType($value);
+ if ($type != $this->type)
+ throw new InvalidArgumentException(
+ sprintf('Expecting type "%s" but got "%s"', $this->type, $type)
+ );
+ return parent::unshift($value);
+ }
+
+}
return $result;
}
+ /**
+ * Move to next node in document.
+ *
+ * When the standard implementation of `read` returns false it could either
+ * be because the end of the document has been reached or because an error
+ * has occurred. This implementation makes it easier to distinguish between
+ * the two by throwing and exception when an error occurs.
+ *
+ * @see \XMLReader::next()
+ *
+ * @return boolean Returns true on success or false on failure.
+ * @throws LibXMLException If an error of sufficient magnitude is detected.
+ */
+ public function next() {
+ libxml_clear_errors();
+ $result = parent::next();
+ $error = libxml_get_last_error();
+ if ($error instanceof LibXMLError && $error->level >= $this->errorLevel)
+ throw new LibXMLException($error);
+ return $result;
+ }
+
}