From 7c465a827b43727c98ea76ba16de5085b91d7dfe Mon Sep 17 00:00:00 2001 From: Andy Street Date: Thu, 13 Dec 2018 22:21:41 +0000 Subject: [PATCH] Improve metadata * Change arrays to linked list * Add support for metadata extensions --- docs/samples/full-1.1.gpx | 5 +- src/libgpx/gpx.php | 78 +++++-------- src/libgpx/gpxreader.php | 77 +++++++++--- src/libgpx/gpxwriter.php | 18 ++- src/libgpx/typeddoublylinkedlist.php | 167 +++++++++++++++++++++++++++ src/libgpx/xmlreader.php | 22 ++++ 6 files changed, 297 insertions(+), 70 deletions(-) create mode 100644 src/libgpx/typeddoublylinkedlist.php diff --git a/docs/samples/full-1.1.gpx b/docs/samples/full-1.1.gpx index d266731..3d8c9f2 100644 --- a/docs/samples/full-1.1.gpx +++ b/docs/samples/full-1.1.gpx @@ -1,5 +1,5 @@ - + full-1.1 This is a sample file containing all parts of the GPX 1.1 specification supported by libgpx. @@ -22,5 +22,8 @@ Example, GPX + + Jane Doe + diff --git a/src/libgpx/gpx.php b/src/libgpx/gpx.php index 87e0b90..4404d94 100644 --- a/src/libgpx/gpx.php +++ b/src/libgpx/gpx.php @@ -74,9 +74,9 @@ class GPX implements Geographic /** * Links to other resources that describe this GPX file. * - * @var Link[] + * @var TypedDoublyLinkedList (Link) */ - protected $links = []; + protected $links; /** * The creation time of the file. @@ -88,9 +88,27 @@ class GPX implements Geographic /** * 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. @@ -200,38 +218,13 @@ class GPX implements Geographic /** * 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. * @@ -255,7 +248,7 @@ class GPX implements Geographic /** * Fetch a list of keywords for this GPX file. * - * @return string[] The keywords + * @return TypedDoublyLinkedList The keywords */ public function getKeywords() { @@ -263,28 +256,13 @@ class GPX implements Geographic } /** - * 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; } /** diff --git a/src/libgpx/gpxreader.php b/src/libgpx/gpxreader.php index accf6e0..0168f24 100644 --- a/src/libgpx/gpxreader.php +++ b/src/libgpx/gpxreader.php @@ -208,33 +208,34 @@ class GPXReader $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; } @@ -269,17 +270,22 @@ class GPXReader $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); @@ -385,6 +391,27 @@ class GPXReader 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. * @@ -394,11 +421,21 @@ class GPXReader * 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) @@ -442,6 +479,14 @@ class GPXReader } 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"', diff --git a/src/libgpx/gpxwriter.php b/src/libgpx/gpxwriter.php index 49b5fea..b4fd51f 100644 --- a/src/libgpx/gpxwriter.php +++ b/src/libgpx/gpxwriter.php @@ -270,7 +270,10 @@ class GPXWriter } $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'); @@ -280,8 +283,17 @@ class GPXWriter $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(); // + } } /** diff --git a/src/libgpx/typeddoublylinkedlist.php b/src/libgpx/typeddoublylinkedlist.php new file mode 100644 index 0000000..b01ba0b --- /dev/null +++ b/src/libgpx/typeddoublylinkedlist.php @@ -0,0 +1,167 @@ + + * + * 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 + */ +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); + } + +} diff --git a/src/libgpx/xmlreader.php b/src/libgpx/xmlreader.php index 3348650..f3f2829 100644 --- a/src/libgpx/xmlreader.php +++ b/src/libgpx/xmlreader.php @@ -85,4 +85,26 @@ class XMLReader extends \XMLReader 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; + } + } -- 2.39.5