]> git.street.me.uk Git - andy/gpx.git/commitdiff
Improve metadata
authorAndy Street <andy@street.me.uk>
Thu, 13 Dec 2018 22:21:41 +0000 (22:21 +0000)
committerAndy Street <andy@street.me.uk>
Thu, 13 Dec 2018 22:21:41 +0000 (22:21 +0000)
  * Change arrays to linked list
  * Add support for metadata extensions

docs/samples/full-1.1.gpx
src/libgpx/gpx.php
src/libgpx/gpxreader.php
src/libgpx/gpxwriter.php
src/libgpx/typeddoublylinkedlist.php [new file with mode: 0644]
src/libgpx/xmlreader.php

index d266731e8e9f7f9792022c7410b584a3b053e4de..3d8c9f2e20311ba3f7feef4f6187fadd376e6dfa 100644 (file)
@@ -1,5 +1,5 @@
 <?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
@@ -22,5 +22,8 @@
     <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
index 87e0b9055b77b147a20470407c82067878607f19..4404d942bd5b9bc07a70dac080d3977dab0518d0 100644 (file)
@@ -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;
   }
 
   /**
index accf6e0e22592f8f561d8edc57d2df8b04b44ec4..0168f243707d7402fb50bdb573bd260f7378cf31 100644 (file)
@@ -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"',
index 49b5feaca78ccd74dab119267754bfc0e299756d..b4fd51f484a7a838a6e5ce2cb582e99ccc5ffac3 100644 (file)
@@ -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(); // </metadata>
+    }
   }
 
   /**
diff --git a/src/libgpx/typeddoublylinkedlist.php b/src/libgpx/typeddoublylinkedlist.php
new file mode 100644 (file)
index 0000000..b01ba0b
--- /dev/null
@@ -0,0 +1,167 @@
+<?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);
+  }
+
+}
index 3348650b473883a80fa7736a56e291c960b059d5..f3f282945e14e8d2cfbc8d074d6ed2e6cad7180d 100644 (file)
@@ -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;
+  }
+
 }