]> git.street.me.uk Git - andy/gpx.git/commitdiff
Rewrite GPXReader class
authorAndy Street <andy@street.me.uk>
Tue, 8 Jan 2019 18:22:22 +0000 (18:22 +0000)
committerAndy Street <andy@street.me.uk>
Tue, 8 Jan 2019 18:22:22 +0000 (18:22 +0000)
Rewrite using XSD schemas to improve preformance, validation and
code readability.

docs/samples/full-1.1.gpx
src/libgpx/gpx-1-0.xsd [new file with mode: 0644]
src/libgpx/gpx-1-1.xsd [new file with mode: 0644]
src/libgpx/gpxreader.php
src/libgpx/peekstream.php [new file with mode: 0644]

index a061ed8b30e9f92959b2327ead7ddab8c583820e..f240cf73221abea651ba0d9ca1ed70befd41c0e4 100644 (file)
-<?xml version="1.0" encoding="UTF-8"?>\r
+<?xml version="1.0" encoding="UTF-8"?>
 <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
-    <author>\r
-      <name>John Doe</name>\r
-      <email id="john.doe" domain="example.com"/>\r
-      <link href="https://www.example.com/~john.doe/">\r
-        <text>John Doe's Homepage</text>\r
-        <type>text/html</type>\r
-      </link>\r
-    </author>\r
-    <copyright author="John Doe">\r
-      <year>2018</year>\r
-      <license>https://creativecommons.org/publicdomain/zero/1.0/</license>\r
-    </copyright>\r
-    <link href="https://www.example.com/">\r
-      <text>Example Inc.</text>\r
-      <type>text/html</type>\r
-    </link>\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
-  <wpt lat="50.750130" lon="-1.072391">\r
-    <ele>10.3</ele>\r
-    <time>2018-11-25T10:07:47Z</time>\r
-    <magvar>12.3</magvar>\r
-    <geoidheight>10</geoidheight>\r
-    <name>Horse Sands Fort</name>\r
-    <cmt>Fort</cmt>\r
-    <desc>A fort in the Solent</desc>\r
-    <src>Guess</src>\r
-    <link href="https://www.example.com/fort">\r
-      <text>More Information</text>\r
-      <type>text/html</type>\r
-    </link>\r
-    <sym>Flag, Blue</sym>\r
-    <type>Nautical</type>\r
-    <fix>3d</fix>\r
-    <sat>6</sat>\r
-    <hdop>1</hdop>\r
-    <vdop>2</vdop>\r
-    <pdop>3</pdop>\r
-    <ageofdgpsdata>86400</ageofdgpsdata>\r
-    <dgpsid>13</dgpsid>\r
-    <extensions>\r
-      <custom:checkedBy>Jane Doe</custom:checkedBy>\r
-    </extensions>\r
-  </wpt>\r
-  <rte>\r
-    <name>Sample Route</name>\r
-    <cmt>Sample</cmt>\r
-    <desc>A Sample route</desc>\r
-    <src>Imagination</src>\r
-    <link href="https://www.example.com/route">\r
-      <text>Sample Route</text>\r
-      <type>text/html</type>\r
-    </link>\r
-    <number>1</number>\r
-    <type>A type</type>\r
-    <extensions>\r
-      <custom:checkedBy>Jane Doe</custom:checkedBy>\r
-    </extensions>\r
-    <rtept lat="0.0" lon="0.0"/>\r
-    <rtept lat="1.0" lon="1.0"/>\r
-  </rte>\r
-  <trk>\r
-    <name>Sample Track</name>\r
-    <cmt>track</cmt>\r
-    <desc>A sample track</desc>\r
-    <src>Imagination</src>\r
-    <link href="https://www.example.com/track">\r
-      <text>Sample Route</text>\r
-      <type>text/html</type>\r
-    </link>\r
-    <number>1</number>\r
-    <type>A type</type>\r
-    <extensions>\r
-      <custom:checkedBy>Jane Doe</custom:checkedBy>\r
-    </extensions>\r
-    <trkseg>\r
-      <extensions>\r
-        <custom:checkedBy>Jane Doe</custom:checkedBy>\r
-      </extensions>\r
-      <trkpt lat="0.0" lon="0.0"/>\r
-      <trkpt lat="1.0" lon="1.0"/>\r
-    </trkseg>\r
-    <trkseg>\r
-      <trkpt lat="2.0" lon="2.0"/>\r
-      <trkpt lat="3.0" lon="3.0"/>\r
-    </trkseg>\r
-  </trk>\r
-  <extensions>\r
-    <custom:checkedBy>Jane Doe</custom:checkedBy>\r
-  </extensions>\r
-</gpx>\r
+  <metadata>
+    <name>full-1.1</name>
+    <desc>This is a sample file containing all parts of the GPX 1.1 specification supported by libgpx.</desc>
+    <author>
+      <name>John Doe</name>
+      <email id="john.doe" domain="example.com"/>
+      <link href="https://www.example.com/~john.doe/">
+        <text>John Doe's Homepage</text>
+        <type>text/html</type>
+      </link>
+    </author>
+    <copyright author="John Doe">
+      <year>2018</year>
+      <license>https://creativecommons.org/publicdomain/zero/1.0/</license>
+    </copyright>
+    <link href="https://www.example.com/">
+      <text>Example Inc.</text>
+      <type>text/html</type>
+    </link>
+    <time>2018-11-25T10:07:47Z</time>
+    <keywords>Example, GPX</keywords>
+    <bounds minlat="-1.234" minlon="-1.234" maxlat="1.234" maxlon="1.234"/>
+    <extensions>
+      <custom:checkedBy>Jane Doe</custom:checkedBy>
+    </extensions>
+  </metadata>
+  <wpt lat="50.750130" lon="-1.072391">
+    <ele>10.3</ele>
+    <time>2018-11-25T10:07:47Z</time>
+    <magvar>12.3</magvar>
+    <geoidheight>10</geoidheight>
+    <name>Horse Sands Fort</name>
+    <cmt>Fort</cmt>
+    <desc>A fort in the Solent</desc>
+    <src>Guess</src>
+    <link href="https://www.example.com/fort">
+      <text>More Information</text>
+      <type>text/html</type>
+    </link>
+    <sym>Flag, Blue</sym>
+    <type>Nautical</type>
+    <fix>3d</fix>
+    <sat>6</sat>
+    <hdop>1</hdop>
+    <vdop>2</vdop>
+    <pdop>3</pdop>
+    <ageofdgpsdata>86400</ageofdgpsdata>
+    <dgpsid>13</dgpsid>
+    <extensions>
+      <custom:checkedBy>Jane Doe</custom:checkedBy>
+    </extensions>
+  </wpt>
+  <rte>
+    <name>Sample Route</name>
+    <cmt>Sample</cmt>
+    <desc>A Sample route</desc>
+    <src>Imagination</src>
+    <link href="https://www.example.com/route">
+      <text>Sample Route</text>
+      <type>text/html</type>
+    </link>
+    <number>1</number>
+    <type>A type</type>
+    <extensions>
+      <custom:checkedBy>Jane Doe</custom:checkedBy>
+    </extensions>
+    <rtept lat="0.0" lon="0.0"/>
+    <rtept lat="1.0" lon="1.0"/>
+  </rte>
+  <trk>
+    <name>Sample Track</name>
+    <cmt>track</cmt>
+    <desc>A sample track</desc>
+    <src>Imagination</src>
+    <link href="https://www.example.com/track">
+      <text>Sample Route</text>
+      <type>text/html</type>
+    </link>
+    <number>1</number>
+    <type>A type</type>
+    <extensions>
+      <custom:checkedBy>Jane Doe</custom:checkedBy>
+    </extensions>
+    <trkseg>
+      <trkpt lat="0.0" lon="0.0"/>
+      <trkpt lat="1.0" lon="1.0"/>
+      <extensions>
+        <custom:checkedBy>Jane Doe</custom:checkedBy>
+      </extensions>
+    </trkseg>
+    <trkseg>
+      <trkpt lat="2.0" lon="2.0"/>
+      <trkpt lat="3.0" lon="3.0"/>
+    </trkseg>
+  </trk>
+  <extensions>
+    <custom:checkedBy>Jane Doe</custom:checkedBy>
+  </extensions>
+</gpx>
diff --git a/src/libgpx/gpx-1-0.xsd b/src/libgpx/gpx-1-0.xsd
new file mode 100644 (file)
index 0000000..93c530f
--- /dev/null
@@ -0,0 +1,227 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- GPX.xsd version 1.0 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp -->
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:gpx="http://www.topografix.com/GPX/1/0" targetNamespace="http://www.topografix.com/GPX/1/0" elementFormDefault="qualified">
+
+<!-- Main GPX definition -->
+
+  <xsd:element name="gpx">
+    <xsd:complexType>
+      <xsd:sequence>
+        <xsd:element name="name" type="xsd:string" minOccurs="0"/>             <!-- GPX file name -->
+        <xsd:element name="desc" type="xsd:string" minOccurs="0"/>             <!-- GPX file description -->
+        <xsd:element name="author" type="xsd:string" minOccurs="0"/>           <!-- GPX file author -->
+        <xsd:element name="email" type="gpx:emailType" minOccurs="0"/> <!-- GPX file author email -->
+        <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>              <!-- GPX file URL -->
+               <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+        <xsd:element name="time" type="xsd:dateTime" minOccurs="0"/>           <!-- GPX file creation time -->
+        <xsd:element name="keywords" type="xsd:string" minOccurs="0"/>         <!-- GPX file keywords -->
+        <xsd:element name="bounds" type="gpx:boundsType" minOccurs="0"/>       <!-- GPX file bounding rect -->
+        <xsd:element name="wpt" minOccurs="0" maxOccurs="unbounded">
+          <xsd:complexType>
+            <xsd:sequence>     <!-- elements must appear in this order -->
+                         <!-- Position info -->
+              <xsd:element name="ele" type="xsd:decimal" minOccurs="0"/>
+              <xsd:element name="time" type="xsd:dateTime" minOccurs="0"/>
+                         <xsd:element name="magvar" type="gpx:degreesType" minOccurs="0"/>
+                         <xsd:element name="geoidheight" type="xsd:decimal" minOccurs="0"/>
+
+                         <!-- Description info -->
+                         <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="cmt" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="desc" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="src" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>
+                         <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="sym" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="type" type="xsd:string" minOccurs="0"/>
+
+                         <!-- Accuracy info -->
+                         <xsd:element name="fix" type="gpx:fixType" minOccurs="0"/>
+                         <xsd:element name="sat" type="xsd:nonNegativeInteger" minOccurs="0"/>
+                         <xsd:element name="hdop" type="xsd:decimal" minOccurs="0"/>
+                         <xsd:element name="vdop" type="xsd:decimal" minOccurs="0"/>
+                         <xsd:element name="pdop" type="xsd:decimal" minOccurs="0"/>
+                         <xsd:element name="ageofdgpsdata" type="xsd:decimal" minOccurs="0"/>
+                         <xsd:element name="dgpsid" type="gpx:dgpsStationType" minOccurs="0"/>
+
+                         <!-- you can add your own privately defined wpt elements at the end of the wpt -->
+              <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+            </xsd:sequence>
+            <xsd:attribute name="lat" type="gpx:latitudeType" use="required"/>
+            <xsd:attribute name="lon" type="gpx:longitudeType" use="required"/>
+          </xsd:complexType>
+        </xsd:element>
+        <xsd:element name="rte" minOccurs="0" maxOccurs="unbounded">
+          <xsd:complexType>
+            <xsd:sequence>
+              <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="cmt" type="xsd:string" minOccurs="0"/>
+              <xsd:element name="desc" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="src" type="xsd:string" minOccurs="0"/>     <!-- the source of this data: "Garmin eTrex", "Map", etc -->
+                         <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>
+                         <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0"/>      <!-- GPS track number -->
+       <!--      <xsd:element name="type"                      type="xsd:string"       minOccurs="0"/> PROPOSED -->
+                         <!-- you can add your own privately defined rte elements at the end of the rte -->
+              <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+              <xsd:element name="rtept" minOccurs="0" maxOccurs="unbounded">
+                <xsd:complexType>
+                  <xsd:sequence>       <!-- elements must appear in this order -->
+
+                                   <!-- Position info -->
+                    <xsd:element name="ele" type="xsd:decimal" minOccurs="0"/>
+                    <xsd:element name="time" type="xsd:dateTime" minOccurs="0"/>
+                                       <xsd:element name="magvar" type="gpx:degreesType" minOccurs="0"/>
+                                       <xsd:element name="geoidheight" type="xsd:decimal" minOccurs="0"/>
+
+                                       <!-- Description info -->
+                    <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+                    <xsd:element name="cmt" type="xsd:string" minOccurs="0"/>
+                    <xsd:element name="desc" type="xsd:string" minOccurs="0"/>
+                    <xsd:element name="src" type="xsd:string" minOccurs="0"/>
+                                       <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>
+                                   <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+                    <xsd:element name="sym" type="xsd:string" minOccurs="0"/>
+                    <xsd:element name="type" type="xsd:string" minOccurs="0"/>
+
+                                       <!-- Accuracy info -->
+                                       <xsd:element name="fix" type="gpx:fixType" minOccurs="0"/>
+                                       <xsd:element name="sat" type="xsd:nonNegativeInteger" minOccurs="0"/>
+                                       <xsd:element name="hdop" type="xsd:decimal" minOccurs="0"/>
+                                       <xsd:element name="vdop" type="xsd:decimal" minOccurs="0"/>
+                                       <xsd:element name="pdop" type="xsd:decimal" minOccurs="0"/>
+                                       <xsd:element name="ageofdgpsdata" type="xsd:decimal" minOccurs="0"/>
+                                       <xsd:element name="dgpsid" type="gpx:dgpsStationType" minOccurs="0"/>
+
+                                       <!-- you can add your own privately defined rtept elements at the end of the rtept -->
+                    <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+                  </xsd:sequence>
+                  <xsd:attribute name="lat" type="gpx:latitudeType" use="required"/>
+                  <xsd:attribute name="lon" type="gpx:longitudeType" use="required"/>
+                </xsd:complexType>
+              </xsd:element>
+            </xsd:sequence>
+          </xsd:complexType>
+        </xsd:element>
+        <xsd:element name="trk" minOccurs="0" maxOccurs="unbounded">
+          <xsd:complexType>
+            <xsd:sequence>
+              <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="cmt" type="xsd:string" minOccurs="0"/>
+              <xsd:element name="desc" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="src" type="xsd:string" minOccurs="0"/>     <!-- the source of this data: "Garmin eTrex", "Map", etc -->
+                         <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>
+                         <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+                         <xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0"/>      <!-- GPS track number -->
+       <!--      <xsd:element name="type"                      type="xsd:string"               minOccurs="0"/>  PROPOSED -->
+                         <!-- you can add your own privately defined trk elements at the end of the trk -->
+              <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+              <xsd:element name="trkseg" minOccurs="0" maxOccurs="unbounded">
+                <xsd:complexType>
+                  <xsd:sequence>       <!-- elements must appear in this order -->
+                                   <xsd:element name="trkpt" minOccurs="0" maxOccurs="unbounded">
+                                         <xsd:complexType>
+                                           <xsd:sequence>      <!-- elements must appear in this order -->
+
+                                                 <!-- Position info -->
+                                                 <xsd:element name="ele" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="time" type="xsd:dateTime" minOccurs="0"/>
+                                                 <xsd:element name="course" type="gpx:degreesType" minOccurs="0"/>
+                                                 <xsd:element name="speed" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="magvar" type="gpx:degreesType" minOccurs="0"/>
+                                                 <xsd:element name="geoidheight" type="xsd:decimal" minOccurs="0"/>
+
+                                                 <!-- Description info -->
+                                                 <xsd:element name="name" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="cmt" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="desc" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="src" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="url" type="xsd:anyURI" minOccurs="0"/>
+                                                 <xsd:element name="urlname" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="sym" type="xsd:string" minOccurs="0"/>
+                                                 <xsd:element name="type" type="xsd:string" minOccurs="0"/>
+
+                                                 <!-- Accuracy info -->
+                                                 <xsd:element name="fix" type="gpx:fixType" minOccurs="0"/>
+                                                 <xsd:element name="sat" type="xsd:nonNegativeInteger" minOccurs="0"/>
+                                                 <xsd:element name="hdop" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="vdop" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="pdop" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="ageofdgpsdata" type="xsd:decimal" minOccurs="0"/>
+                                                 <xsd:element name="dgpsid" type="gpx:dgpsStationType" minOccurs="0"/>
+
+                                                 <!-- you can add your own privately defined trkpt elements at the end of the trkpt -->
+                                                 <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+                                           </xsd:sequence>
+                                           <xsd:attribute name="lat" type="gpx:latitudeType" use="required"/>
+                                           <xsd:attribute name="lon" type="gpx:longitudeType" use="required"/>
+                                         </xsd:complexType>
+                                   </xsd:element>
+                                 </xsd:sequence>
+                           </xsd:complexType>
+                         </xsd:element>
+            </xsd:sequence>
+          </xsd:complexType>
+        </xsd:element>
+               <!-- you can add your own privately defined elements at the end of the GPX file -->
+        <xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="skip"/>
+      </xsd:sequence>
+      <xsd:attribute name="version" type="xsd:string" use="required" fixed="1.0"/>     <!-- version 1.0 -->
+      <xsd:attribute name="creator" type="xsd:string" use="required"/>
+    </xsd:complexType>
+  </xsd:element>
+
+  <!-- Other types used by GPX -->
+
+  <xsd:simpleType name="latitudeType">
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="-90.0"/>
+      <xsd:maxInclusive value="90.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="longitudeType">
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="-180.0"/>
+      <xsd:maxInclusive value="180.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="degreesType">  <!-- for bearing, heading, course.  Units are degrees, true -->
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="0.0"/>
+      <xsd:maxInclusive value="360.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="fixType">
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="none"/>  <!-- none means GPS had no fix.  To signify "the fix info is unknown, leave out the <fix> tag entirely -->
+      <xsd:enumeration value="2d"/>
+      <xsd:enumeration value="3d"/>
+      <xsd:enumeration value="dgps"/>
+      <xsd:enumeration value="pps"/>           <!-- military signal used -->
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="dgpsStationType">
+    <xsd:restriction base="xsd:integer">
+      <xsd:minInclusive value="0"/>
+      <xsd:maxInclusive value="1023"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:complexType name="boundsType">  <!-- bounding rect for data in file -->
+    <xsd:attribute name="minlat" type="gpx:latitudeType" use="required"/>
+    <xsd:attribute name="minlon" type="gpx:longitudeType" use="required"/>
+    <xsd:attribute name="maxlat" type="gpx:latitudeType" use="required"/>
+    <xsd:attribute name="maxlon" type="gpx:longitudeType" use="required"/>
+  </xsd:complexType>
+
+  <xsd:simpleType name="emailType">
+    <xsd:restriction base="xsd:string">
+      <xsd:pattern value="[\p{L}_]+(\.[\p{L}_]+)*@[\p{L}_]+(\.[\p{L}_]+)+"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+</xsd:schema>
diff --git a/src/libgpx/gpx-1-1.xsd b/src/libgpx/gpx-1-1.xsd
new file mode 100644 (file)
index 0000000..6ffd92d
--- /dev/null
@@ -0,0 +1,784 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.topografix.com/GPX/1/1" targetNamespace="http://www.topografix.com/GPX/1/1" elementFormDefault="qualified">
+
+<xsd:annotation>
+ <xsd:documentation>
+  GPX schema version 1.1 - For more information on GPX and this schema, visit http://www.topografix.com/gpx.asp
+
+  GPX uses the following conventions: all coordinates are relative to the WGS84 datum.  All measurements are in metric units.
+ </xsd:documentation>
+</xsd:annotation>
+
+  <xsd:element name="gpx" type="gpxType">
+    <xsd:annotation>
+      <xsd:documentation>
+               GPX is the root element in the XML file.
+         </xsd:documentation>
+       </xsd:annotation>
+  </xsd:element>
+
+  <xsd:complexType name="gpxType">
+    <xsd:annotation>
+      <xsd:documentation>
+               GPX documents contain a metadata header, followed by waypoints, routes, and tracks.  You can add your own elements
+               to the extensions section of the GPX document.
+         </xsd:documentation>
+       </xsd:annotation>
+       <xsd:sequence>
+        <xsd:element name="metadata" type="metadataType" minOccurs="0">
+         <xsd:annotation>
+          <xsd:documentation>
+               Metadata about the file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+        <xsd:element name="wpt" type="wptType" minOccurs="0" maxOccurs="unbounded">
+         <xsd:annotation>
+          <xsd:documentation>
+               A list of waypoints.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+        <xsd:element name="rte" type="rteType" minOccurs="0" maxOccurs="unbounded">
+         <xsd:annotation>
+          <xsd:documentation>
+               A list of routes.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+        <xsd:element name="trk" type="trkType" minOccurs="0" maxOccurs="unbounded">
+         <xsd:annotation>
+          <xsd:documentation>
+               A list of tracks.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+       </xsd:sequence>
+
+       <xsd:attribute name="version" type="xsd:string" use="required" fixed="1.1">
+     <xsd:annotation>
+      <xsd:documentation>
+               You must include the version number in your GPX document.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+       <xsd:attribute name="creator" type="xsd:string" use="required">
+     <xsd:annotation>
+      <xsd:documentation>
+               You must include the name or URL of the software that created your GPX document.  This allows others to
+               inform the creator of a GPX instance document that fails to validate.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="metadataType">
+    <xsd:annotation>
+      <xsd:documentation>
+               Information about the GPX file, author, and copyright restrictions goes in the metadata section.  Providing rich,
+               meaningful information about your GPX files allows others to search for and use your GPS data.
+         </xsd:documentation>
+       </xsd:annotation>
+    <xsd:sequence>     <!-- elements must appear in this order -->
+     <xsd:element name="name" type="xsd:string" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               The name of the GPX file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="desc" type="xsd:string" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               A description of the contents of the GPX file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="author" type="personType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               The person or organization who created the GPX file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="copyright" type="copyrightType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               Copyright and license information governing use of the file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
+      <xsd:annotation>
+       <xsd:documentation>
+               URLs associated with the location described in the file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="time" type="xsd:dateTime" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               The creation date of the file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="keywords" type="xsd:string" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               Keywords associated with the file.  Search engines or databases can use this information to classify the data.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+     <xsd:element name="bounds" type="boundsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               Minimum and maximum coordinates which describe the extent of the coordinates in the file.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="wptType">
+    <xsd:annotation>
+      <xsd:documentation>
+               wpt represents a waypoint, point of interest, or named feature on a map.
+         </xsd:documentation>
+       </xsd:annotation>
+    <xsd:sequence>     <!-- elements must appear in this order -->
+         <!-- Position info -->
+      <xsd:element name="ele" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Elevation (in meters) of the point.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="time" type="xsd:dateTime" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Creation/modification timestamp for element. Date and time in are in Univeral Coordinated Time (UTC), not local time! Conforms to ISO 8601 specification for date/time representation. Fractional seconds are allowed for millisecond timing in tracklogs. 
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="magvar" type="degreesType" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Magnetic variation (in degrees) at the point
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="geoidheight" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid.  As defined in NMEA GGA message.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+
+         <!-- Description info -->
+         <xsd:element name="name" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       The GPS name of the waypoint. This field will be transferred to and from the GPS. GPX does not place restrictions on the length of this field or the characters contained in it. It is up to the receiving application to validate the field before sending it to the GPS.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="cmt" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS waypoint comment. Sent to GPS as comment. 
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="desc" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       A text description of the element. Holds additional information about the element intended for the user, not the GPS.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="src" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Source of data. Included to give user some idea of reliability and accuracy of data.  "Garmin eTrex", "USGS quad Boston North", e.g.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Link to additional information about the waypoint.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="sym" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Text of GPS symbol name. For interchange with other programs, use the exact spelling of the symbol as displayed on the GPS.  If the GPS abbreviates words, spell them out.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="type" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Type (classification) of the waypoint.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+
+         <!-- Accuracy info -->
+         <xsd:element name="fix" type="fixType" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Type of GPX fix.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="sat" type="xsd:nonNegativeInteger" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Number of satellites used to calculate the GPX fix.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="hdop" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Horizontal dilution of precision.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="vdop" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Vertical dilution of precision.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="pdop" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Position dilution of precision.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="ageofdgpsdata" type="xsd:decimal" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Number of seconds since last DGPS update.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="dgpsid" type="dgpsStationType" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       ID of DGPS station used in differential correction.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+    </xsd:sequence>
+
+    <xsd:attribute name="lat" type="latitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The latitude of the point.  This is always in decimal degrees, and always in WGS84 datum.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+    <xsd:attribute name="lon" type="longitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+      The longitude of the point.  This is always in decimal degrees, and always in WGS84 datum.
+    </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="rteType">
+    <xsd:annotation>
+      <xsd:documentation>
+               rte represents route - an ordered list of waypoints representing a series of turn points leading to a destination.
+         </xsd:documentation>
+       </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="name" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS name of route.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="cmt" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS comment for route.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="desc" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Text description of route for user.  Not sent to GPS.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="src" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Source of data. Included to give user some idea of reliability and accuracy of data.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Links to external information about the route.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS route number.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="type" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Type (classification) of route.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+      <xsd:element name="rtept" type="wptType" minOccurs="0" maxOccurs="unbounded">
+         <xsd:annotation>
+          <xsd:documentation>
+               A list of route points.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="trkType">
+    <xsd:annotation>
+      <xsd:documentation>
+               trk represents a track - an ordered list of points describing a path.
+         </xsd:documentation>
+       </xsd:annotation>
+    <xsd:sequence>
+      <xsd:element name="name" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS name of track.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="cmt" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS comment for track.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="desc" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       User description of track.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="src" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Source of data. Included to give user some idea of reliability and accuracy of data.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+      <xsd:element name="link" type="linkType" minOccurs="0" maxOccurs="unbounded">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Links to external information about track.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="number" type="xsd:nonNegativeInteger" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       GPS track number.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+         <xsd:element name="type" type="xsd:string" minOccurs="0">
+               <xsd:annotation>
+                 <xsd:documentation>
+                       Type (classification) of track.
+                 </xsd:documentation>
+               </xsd:annotation>
+         </xsd:element>
+
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+  
+     <xsd:element name="trkseg" type="trksegType" minOccurs="0" maxOccurs="unbounded">
+      <xsd:annotation>
+       <xsd:documentation>
+               A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+  <xsd:complexType name="extensionsType">
+   <xsd:annotation>
+    <xsd:documentation>
+        You can add extend GPX by adding your own elements from another schema here.
+    </xsd:documentation>
+   </xsd:annotation>
+    <xsd:sequence>
+        <xsd:any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded">
+          <xsd:annotation>
+               <xsd:documentation>
+                You can add extend GPX by adding your own elements from another schema here.
+               </xsd:documentation>
+          </xsd:annotation>
+        </xsd:any>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="trksegType">
+   <xsd:annotation>
+    <xsd:documentation>
+        A Track Segment holds a list of Track Points which are logically connected in order. To represent a single GPS track where GPS reception was lost, or the GPS receiver was turned off, start a new Track Segment for each continuous span of track data.
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:sequence>      <!-- elements must appear in this order -->
+        <xsd:element name="trkpt" type="wptType" minOccurs="0" maxOccurs="unbounded">
+      <xsd:annotation>
+       <xsd:documentation>
+               A Track Point holds the coordinates, elevation, timestamp, and metadata for a single point in a track.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+
+        <xsd:element name="extensions" type="extensionsType" minOccurs="0">
+      <xsd:annotation>
+       <xsd:documentation>
+               You can add extend GPX by adding your own elements from another schema here.
+          </xsd:documentation>
+         </xsd:annotation>
+        </xsd:element>
+    </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="copyrightType">
+   <xsd:annotation>
+    <xsd:documentation>
+        Information about the copyright holder and any license governing use of this file.  By linking to an appropriate license,
+        you may place your data into the public domain or grant additional usage rights.
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:sequence>      <!-- elements must appear in this order -->
+    <xsd:element name="year" type="xsd:gYear" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Year of copyright.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+    <xsd:element name="license" type="xsd:anyURI" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Link to external file containing license text.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+   </xsd:sequence>
+   <xsd:attribute name="author" type="xsd:string" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               Copyright holder (TopoSoft, Inc.)
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="linkType">
+   <xsd:annotation>
+    <xsd:documentation>
+        A link to an external resource (Web page, digital photo, video clip, etc) with additional information.
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:sequence>      <!-- elements must appear in this order -->
+    <xsd:element name="text" type="xsd:string" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Text of hyperlink.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+    <xsd:element name="type" type="xsd:string" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Mime type of content (image/jpeg)
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+   </xsd:sequence>
+   <xsd:attribute name="href" type="xsd:anyURI" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               URL of hyperlink.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="emailType">
+   <xsd:annotation>
+    <xsd:documentation>
+        An email address.  Broken into two parts (id and domain) to help prevent email harvesting.
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:attribute name="id" type="xsd:string" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               id half of email address (billgates2004)
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+   <xsd:attribute name="domain" type="xsd:string" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               domain half of email address (hotmail.com)
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="personType">
+   <xsd:annotation>
+    <xsd:documentation>
+        A person or organization.
+    </xsd:documentation>
+   </xsd:annotation>
+    <xsd:sequence>     <!-- elements must appear in this order -->
+      <xsd:element name="name" type="xsd:string" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Name of person or organization.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+      <xsd:element name="email" type="emailType" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Email address.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+      <xsd:element name="link" type="linkType" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               Link to Web site or other external information about person.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+       </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="ptType">
+   <xsd:annotation>
+    <xsd:documentation>
+        A geographic point with optional elevation and time.  Available for use by other schemas.
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:sequence>      <!-- elements must appear in this order -->
+    <xsd:element name="ele" type="xsd:decimal" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               The elevation (in meters) of the point.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+    <xsd:element name="time" type="xsd:dateTime" minOccurs="0">
+        <xsd:annotation>
+         <xsd:documentation>
+               The time that the point was recorded.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:element>
+   </xsd:sequence>
+    <xsd:attribute name="lat" type="latitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The latitude of the point.  Decimal degrees, WGS84 datum.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+    <xsd:attribute name="lon" type="longitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The latitude of the point.  Decimal degrees, WGS84 datum.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+  <xsd:complexType name="ptsegType">
+   <xsd:annotation>
+    <xsd:documentation>
+        An ordered sequence of points.  (for polygons or polylines, e.g.)
+    </xsd:documentation>
+   </xsd:annotation>
+   <xsd:sequence>      <!-- elements must appear in this order -->
+        <xsd:element name="pt" type="ptType" minOccurs="0" maxOccurs="unbounded">
+          <xsd:annotation>
+               <xsd:documentation>
+                Ordered list of geographic points.
+               </xsd:documentation>
+          </xsd:annotation>
+        </xsd:element>
+   </xsd:sequence>
+  </xsd:complexType>
+
+  <xsd:complexType name="boundsType">
+   <xsd:annotation>
+    <xsd:documentation>
+        Two lat/lon pairs defining the extent of an element.
+    </xsd:documentation>
+   </xsd:annotation>
+    <xsd:attribute name="minlat" type="latitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The minimum latitude.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+    <xsd:attribute name="minlon" type="longitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The minimum longitude.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+    <xsd:attribute name="maxlat" type="latitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The maximum latitude.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+    <xsd:attribute name="maxlon" type="longitudeType" use="required">
+        <xsd:annotation>
+         <xsd:documentation>
+               The maximum longitude.
+         </xsd:documentation>
+        </xsd:annotation>
+       </xsd:attribute>
+  </xsd:complexType>
+
+
+  <xsd:simpleType name="latitudeType">
+        <xsd:annotation>
+         <xsd:documentation>
+               The latitude of the point.  Decimal degrees, WGS84 datum.
+         </xsd:documentation>
+        </xsd:annotation>
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="-90.0"/>
+      <xsd:maxInclusive value="90.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="longitudeType">
+        <xsd:annotation>
+         <xsd:documentation>
+               The longitude of the point.  Decimal degrees, WGS84 datum.
+         </xsd:documentation>
+        </xsd:annotation>
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="-180.0"/>
+      <xsd:maxExclusive value="180.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="degreesType">
+        <xsd:annotation>
+         <xsd:documentation>
+               Used for bearing, heading, course.  Units are decimal degrees, true (not magnetic).
+         </xsd:documentation>
+        </xsd:annotation>
+    <xsd:restriction base="xsd:decimal">
+      <xsd:minInclusive value="0.0"/>
+      <xsd:maxExclusive value="360.0"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="fixType">
+        <xsd:annotation>
+         <xsd:documentation>
+               Type of GPS fix.  none means GPS had no fix.  To signify "the fix info is unknown, leave out fixType entirely. pps = military signal used
+         </xsd:documentation>
+        </xsd:annotation>
+    <xsd:restriction base="xsd:string">
+      <xsd:enumeration value="none"/>
+      <xsd:enumeration value="2d"/>
+      <xsd:enumeration value="3d"/>
+      <xsd:enumeration value="dgps"/>
+      <xsd:enumeration value="pps"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <xsd:simpleType name="dgpsStationType">
+   <xsd:annotation>
+    <xsd:documentation>
+        Represents a differential GPS station.
+    </xsd:documentation>
+   </xsd:annotation>
+    <xsd:restriction base="xsd:integer">
+      <xsd:minInclusive value="0"/>
+      <xsd:maxInclusive value="1023"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+</xsd:schema>
\ No newline at end of file
index 8e9c615b7638092b38a85a0198795e9e580a197c..ba068b010b6b9f557c2e831063f1c540492e5092 100644 (file)
 
 namespace libgpx;
 
-use \DateTime;
-use \DateTimeZone;
-use \DomainException;
-use \Exception;
-use \InvalidArgumentException;
-use \RuntimeException;
+use \DateTimeImmutable;
+use \XMLReader;
 
 /**
  * Read GPX files.
@@ -40,45 +36,20 @@ class GPXReader
 {
 
   /**
-   * The namespace of the XML currently being parsed.
-   *
-   * @var string
-   */
-  protected $ns;
-
-  /**
-   * The GPX version of the XML currently being parsed.
-   *
-   * @var string
+   * Options used for XMLReader instances.
    */
-  protected $version;
-
-  /**
-   * The reader currently reading the XML.
-   *
-   * @var XMLReader
-   */
-  protected $xml;
+  const XMLREADER_OPTIONS = LIBXML_NOERROR | LIBXML_NOWARNING;
 
   /**
    * Read GPX from a string.
    *
-   * @param string $xml The GPX XML data.
+   * @param string $data The GPX XML data.
    * @return GPX The GPX data.
-   * @throws RuntimeException If the XML could not be read.
    * @throws MalformedGPXException If the GPX file is invalid.
    */
-  public function readString(string $xml)
+  public static function readString(string $data)
   {
-    try {
-      $this->xml = new XMLReader();
-      if (!$this->xml->XML($xml, null, LIBXML_NOERROR | LIBXML_NOWARNING)) {
-        throw new RuntimeException('Unable to read XML from string.');
-      }
-      return $this->read();
-    } finally {
-      $this->xml = null;
-    }
+    return self::read($data, true);
   }
 
   /**
@@ -88,12 +59,11 @@ class GPXReader
    *
    * @param string $filename The name of the file containing GPX XML data.
    * @return GPX The GPX data.
-   * @throws RuntimeException If the XML could not be read.
    * @throws MalformedGPXException If the GPX file is invalid.
    */
-  public function readFile(string $filename)
+  public static function readFile(string $filename)
   {
-    return $this->readURI($filename);
+    return self::read($filename, false);
   }
 
   /**
@@ -101,714 +71,634 @@ class GPXReader
    *
    * @param string $uri The location of the file containing GPX XML data.
    * @return GPX The GPX data.
-   * @throws RuntimeException If the XML could not be read.
    * @throws MalformedGPXException If the GPX file is invalid.
    */
-  public function readURI(string $uri)
+  public static function readURI(string $uri)
   {
-    try {
-      $this->xml = new XMLReader();
-      if (!$this->xml->open($uri, null, LIBXML_NOERROR | LIBXML_NOWARNING)) {
-        throw new RuntimeException(sprintf('Unable to read XML from: %s', $uri));
-      }
-      return $this->read();
-    } finally {
-      $this->xml = null;
-    }
+    return self::read($uri, false);
   }
 
   /**
-   * Read GPX data from XML.
+   * Read a GPX file.
    *
-   * @param XMLReader $xml Where to read the GPX data from.
-   * @return GPX The data that was read.
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param string $src The source of the GPX data.
+   * @param boolean $fromString If true `src` is treated as a string containing
+   *        the XML to parse otherwise it is treated as a URI to load data from.
+   * @return GPX The loaded GPX data.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function read()
+  public static function read(string $src, bool $fromString)
   {
-    $xml = $this->xml;
+    // Register stream wrapper
+    if (!$fromString) {
+      $wrappers = stream_get_wrappers();
+      do {
+        $wrapper = uniqid('libgpx-');
+      } while (in_array($wrapper, $wrappers));
+      if (!stream_wrapper_register($wrapper, '\libgpx\PeekStream'))
+        throw new MalformedGPXException(
+          'Unable to register stream.'
+        );
+    }
     try {
-      // Fast forward to the root element.
-      while ($xml->nodeType !== XMLReader::ELEMENT) {
-        $xml->read();
+      // Load data from file
+      $xml = new XMLReader();
+      if ($fromString) {
+        if (!@$xml->XML($src, null, self::XMLREADER_OPTIONS))
+          throw new MalformedGPXException(
+            'Unable to read XML string.'
+          );
+      } else {
+        if (!@$xml->open($wrapper . '://' . $src, null, self::XMLREADER_OPTIONS))
+          throw new MalformedGPXException(
+            sprintf('Unable to read source “%s”', $src)
+          );
       }
 
-      // Detect file version
-      switch ($xml->namespaceURI) {
+      // Namespace detection
+      $namespace = self::detectRootNamespace(
+        $fromString ? $src : PeekStream::peek($src)
+      );
+      switch ($namespace) {
         case libgpx::NAMESPACE_GPX_1_0:
-          $this->version = '1.0';
-          $this->ns = libgpx::NAMESPACE_GPX_1_0;
+          $xml->setSchema(__DIR__ . DIRECTORY_SEPARATOR . 'gpx-1-0.xsd');
           break;
         case libgpx::NAMESPACE_GPX_1_1:
-          $this->version = '1.1';
-          $this->ns = libgpx::NAMESPACE_GPX_1_1;
+          $xml->setSchema(__DIR__ . DIRECTORY_SEPARATOR . 'gpx-1-1.xsd');
           break;
-        default:
-          throw new MalformedGPXException('Unknown or unsupported file format.');
+        case null:
+          throw new MalformedGPXException(
+            'Supplied XML is not namespaced.'
+          );
+        default :
+          throw new MalformedGPXException(
+            sprintf('Unsupported namespace: %s', $namespace)
+          );
       }
 
-      // Read GPX element.
-      if ($xml->localName == 'gpx')
-        return $this->readGPX();
-      else
-        throw new MalformedGPXException('Root element must be "gpx".');
-
+      // Read XML
+      while ($valid = ($xml->read() && $xml->isValid())) {
+        if ($xml->nodeType == XMLReader::ELEMENT) break;
+      }
+      if (!$valid)
+        throw new MalformedGPXException(libxml_get_last_error()->message);
+      $gpx = self::readGPX($xml);
     } finally {
-      $this->version = null;
-      $this->ns = null;
+      if (!$fromString) stream_wrapper_unregister($wrapper);
     }
+    return $gpx;
   }
 
   /**
-   * Read a gpx type.
+   * Detect the namespace of the XML root element.
    *
-   * @return GPX The GPX data.
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param string $data The XML to extract namespace from.
+   * @return string The namespace of the root element.
+   * @throws MalformedGPXException If it was not possible to detect the namespace.
    */
-  protected function readGPX()
-  {
-    // Sanity check - ensure version attribute and schema declaration match.
-    if ($this->xml->getAttribute('version') != $this->version)
-      throw new MalformedGPXException(
-        'GPX version attribute does not match namespace declaration.'
-      );
-
-    $result = new GPX();
-    $result->setCreator($this->xml->getAttribute('creator'));
-
-    $struct = [
-      'elements' => [
-        'wpt' => function ($gpx) {
-          $gpx->getWaypoints()[] = $this->readWpt();
-        },
-        'rte' => function ($gpx) {
-          $gpx->getRoutes()[] = $this->readRte();
-        },
-        'trk' => function ($gpx) {
-          $gpx->getTracks()[] = $this->readTrk();
-        }
-      ]
-    ];
-    if ($this->version == '1.1') {
-      $struct['elements']['metadata'] = function ($gpx) {
-        $this->readMetadata($gpx);
-      };
-      $struct['elements']['extensions'] = function ($gpx) {
-        $this->readExtension($gpx->getExtensions());
-      };
-    } else {
-      $struct['elements']['name'] = function ($gpx) {
-        $gpx->setName($this->readXSDString());
-      };
-      $struct['elements']['desc'] = function ($gpx) {
-        $gpx->setDesc($this->readXSDString());
-      };
-      $struct['elements']['author'] = function ($gpx) {
-        $author = $gpx->getAuthor();
-        if ($author === null) {
-          $author = new Person();
-          $gpx->setAuthor($author);
-        }
-        $author->setName($this->readXSDString());
-      };
-      $struct['elements']['email'] = function ($gpx) {
-        $author = $gpx->getAuthor();
-        if ($author === null) {
-          $author = new Person();
-          $gpx->setAuthor($author);
-        }
-        $author->setEmail($this->readXSDString());
-      };
-      $struct['elements']['url'] = function ($gpx, &$state) {
-        $href = $this->readXSDString();
-        $links = $gpx->getLinks();
-        if ($links->isEmpty()) {
-          $link = new Link($href);
-          if (isset($state['urlname'])) {
-            $link->setText($state['urlname']);
-            unset($state['urlname']);
-          }
-          $links[] = $link;
-        } else {
-          $links->bottom()->setHref($href);
-        }
-      };
-      $struct['elements']['urlname'] = function ($gpx, &$state) {
-        $text = $this->readXSDString();
-        $links = $gpx->getLinks();
-        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 = $gpx->getKeywords();
-        $words = explode(',', $this->readXSDString());
-        foreach ($words as $word)
-          $keywords[] = trim($word);
-      };
-      $struct['elements']['bounds'] = false;
-      $struct['extensions'] = function ($gpx) {
-        $gpx->getExtensions()[] = $this->xml->readOuterXML();
-        $this->xml->next();
-      };
+  protected static function detectRootNamespace(string $data) {
+    $xml = new XMLReader();
+    $xml->XML($data, null, self::XMLREADER_OPTIONS);
+    $namespace = false;
+    while ($xml->read()) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        $namespace = $xml->namespaceURI;
+        break;
+      }
     }
-
-    $this->readStruct($struct, $result);
-    return $result;
-  }
-
-  /**
-   * Read a metadata type.
-   *
-   * Note: This is GPX 1.1 only as GPX 1.0 does not have a metadata type.
-   *
-   * @param GPX $gpx The gpx object to populate.
-   * @return void
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
-   */
-  protected function readMetadata(GPX $gpx)
-  {
-    $struct = [
-      'elements' => [
-        'name' => function ($gpx) {
-          $gpx->setName($this->readXSDString());
-        },
-        'desc' => function ($gpx) {
-          $gpx->setDesc($this->readXSDString());
-        },
-        'author' => function ($gpx) {
-          $gpx->setAuthor($this->readPerson());
-        },
-        'copyright' => function ($gpx) {
-          $gpx->setCopyright($this->readCopyright());
-        },
-        'link' => function ($gpx) {
-          $links = $gpx->getLinks();
-          $links[] = $this->readLink();
-        },
-        'time' => function ($gpx) {
-          $gpx->setTime($this->string2DateTime($this->readXSDString()));
-        },
-        'keywords' => function ($gpx) {
-          $keywords = $gpx->getKeywords();
-          $words = explode(',', $this->readXSDString());
-          foreach ($words as $word)
-            $keywords[] = trim($word);
-        },
-        'bounds' => false, // Not required as it is calculated internally.
-        'extensions' => function ($gpx) {
-          $this->readExtension($gpx->getMetadataExtensions());
-        }
-      ]
-    ];
-    $this->readStruct($struct, $gpx);
+    if ($namespace === false)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    $xml->close();
+    return $namespace;
   }
 
   /**
-   * Read a person type.
+   * Reads a gpx type from an XML document.
    *
-   * @return Person The person data.
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the gpx type.
+   * @return GPX A GPX data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readPerson()
+  protected static function readGPX(XMLReader $xml)
   {
-    $struct = [
-      'elements' => [
-        'name' => function ($person) {
-          $person->setName($this->readXSDString());
-        },
-        'email' => function ($person) {
-          $id = $this->xml->getAttribute('id');
-          if ($id === null)
-            throw new MalformedGPXException(
-              'Missing required attribute "id" in "email"'
-            );
-          $domain = $this->xml->getAttribute('domain');
-          if ($domain === null)
-            throw new MalformedGPXException(
-              'Missing required attribute "domain" in "email"'
-            );
-          $person->setEmail($id . '@' . $domain);
-          $this->xml->read();
-        },
-        'link' => function ($person) {
-          $person->setLink($this->readLink());
+    $gpx = new GPX();
+    $gpx->setCreator($xml->getAttribute('creator'));
+    $ns = $xml->namespaceURI;
+    $inMetadata = false;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        if ($xml->namespaceURI == $ns) {
+          switch ($xml->localName) {
+            case 'metadata':
+              $inMetadata = true;
+              break;
+            case 'name':
+              $gpx->setName($xml->readString());
+              break;
+            case 'desc':
+              $gpx->setDesc($xml->readString());
+              break;
+            case 'author':
+              if ($inMetadata) {
+                $gpx->setAuthor(self::readPerson($xml));
+              } else {
+                $person = new Person();
+                $person->setName($xml->readString());
+                $gpx->setAuthor($person);
+              }
+              break;
+            case 'email':
+              $person = $gpx->getAuthor();
+              if ($person !== null) $gpx->setAuthor($person = new Person());
+              $person->setEmail($xml->readString());
+              break;
+            case 'copyright':
+              $gpx->setCopyright(self::readCopyright($xml));
+              break;
+            case 'link':
+              $gpx->getLinks()[] = self::readLink($xml);
+              break;
+            case 'url':
+              $gpx->getLinks()[] = new Link($xml->readString());
+              break;
+            case 'urlname':
+              $links = $gpx->getLinks();
+              if (count($links) > 0)
+                $links[0]->setText($xml->readString());
+              break;
+            case 'time':
+              $gpx->setTime(new DateTimeImmutable($xml->readString()));
+              break;
+            case 'keywords':
+              $keywords = $gpx->getKeywords();
+              foreach (explode(',', $xml->readString()) as $keyword)
+                $keywords[] = trim($keyword);
+              break;
+            case 'wpt':
+              $gpx->getWaypoints()[] = self::readPoint($xml);
+              break;
+            case 'rte':
+              $gpx->getRoutes()[] = self::readRoute($xml);
+              break;
+            case 'trk':
+              $gpx->getTracks()[] = self::readTrack($xml);
+              break;
+            case 'extensions':
+              $exts = (
+                $inMetadata
+                ? $gpx->getMetadataExtensions()
+                : $gpx->getExtensions()
+              );
+              foreach (self::readExtensions($xml) as $ext) {
+                $exts[] = $ext;
+              }
+              break;
+          }
+        } else {
+          $gpx->getExtensions()[] = self::readExtension($xml);
         }
-      ]
-    ];
-    $person = new Person();
-    $this->readStruct($struct, $person);
-    return $person;
-  }
-
-  /**
-   * Read a link type.
-   *
-   * @return Link The link data.
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
-   */
-  protected function readLink()
-  {
-    $href = $this->xml->getAttribute('href');
-    if ($href === null)
-      throw new MalformedGPXException(
-        'Missing required attribute "href" in "link"'
-      );
-    $struct = [
-      'elements' => [
-        'text' => function ($link) {
-          $link->setText($this->readXSDString());
-        },
-        'type' => function ($link) {
-          $link->setType($this->readXSDString());
+      } elseif (
+        $xml->nodeType == XMLReader::END_ELEMENT
+        && $xml->namespaceURI == $ns
+      ) {
+        switch ($xml->localName) {
+          case 'metadata':
+            $inMetadata = false;
+            break;
+          case 'gpx':
+            break 2;
         }
-      ]
-    ];
-    $link = new Link($href);
-    $this->readStruct($struct, $link);
-    return $link;
+      }
+    }
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $gpx;
   }
 
   /**
-   * Read a copyright type.
+   * Reads a rte type from an XML document.
    *
-   * @return Copyright The copyright data.
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the rte type.
+   * @return Route A Route data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readCopyright()
+  protected static function readRoute(XMLReader $xml)
   {
-    $author = $this->xml->getAttribute('author');
-    if ($author === null)
-      throw new MalformedGPXException(
-        'Missing required attribute "author" in "copyright"'
-      );
-    $struct = [
-      'elements' => [
-        'year' => function ($copyright) {
-          try {
-            $year = $this->readXSDString();
-            $copyright->setYear($year);
-          } catch (InvalidArgumentException $e) {
-            throw new MalformedGPXException(
-              sprintf('"%s" is not a valid year', $year)
-            );
+    $route = new Route();
+    if ($xml->isEmptyElement) return $route;
+    $ns = $xml->namespaceURI;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        if ($xml->namespaceURI == $ns) {
+          switch ($xml->localName) {
+            case 'name':
+              $route->setName($xml->readString());
+              break;
+            case 'cmt':
+              $route->setComment($xml->readString());
+              break;
+            case 'desc':
+              $route->setDescription($xml->readString());
+              break;
+            case 'src':
+              $route->setSource($xml->readString());
+              break;
+            case 'link':
+              $route->getLinks()[] = self::readLink($xml);
+              break;
+            case 'url':
+              $route->getLinks()[] = new Link($xml->readString());
+              break;
+            case 'urlname':
+              $links = $route->getLinks();
+              if (count($links) > 0)
+                $links[0]->setText($xml->readString());
+              break;
+            case 'number':
+              $route->setNumber($xml->readString());
+              break;
+            case 'type';
+              $route->setType($xml->readString());
+              break;
+            case 'extensions':
+              $exts = $route->getExtensions();
+              foreach (self::readExtensions($xml) as $ext) {
+                $exts[] = $ext;
+              }
+              break;
+            case 'rtept':
+              $route->getPoints()[] = self::readPoint($xml);
+              break;
           }
-        },
-        'license' => function ($copyright) {
-          $copyright->setLicense($this->readXSDString());
+        } else {
+          $route->getExtensions()[] = self::readExtension($xml);
         }
-      ]
-    ];
-    $copyright = new Copyright($author);
-    $this->readStruct($struct, $copyright);
-    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();
+      } elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->namespaceURI == $ns && $xml->localName == 'rte') break;
       }
-    ];
-    $this->readStruct($struct, $list);
-  }
-
-  /**
-   * Read a wpt type.
-   *
-   * @see https://www.topografix.com/GPX/1/1/#type_wptType
-   *
-   * @return Point
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
-   */
-  protected function readWpt()
-  {
-    try {
-      $result = new Point(
-        $this->string2float($this->xml->getAttribute('lat')),
-        $this->string2float($this->xml->getAttribute('lon'))
-      );
-      $struct = $this->getDataTypeStruct();
-      $struct['elements']['ele'] = function ($point) {
-        $point->setEle($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['time'] = function ($point) {
-        $point->setTime($this->string2DateTime($this->readXSDString()));
-      };
-      $struct['elements']['magvar'] = function ($point) {
-        $point->setMagvar($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['geoidheight'] = function ($point) {
-        $point->setGeoidHeight($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['sym'] = function ($point) {
-        $point->setSymbol($this->readXSDString());
-      };
-      // "type" only appears in "wpt" in GPX 1.0
-      if ($this->version == '1.0')
-        $struct['elements']['type'] = function ($point) {
-          $point->setType($this->readXSDString());
-        };
-      $struct['elements']['fix'] = function ($point) {
-        $point->setFix($this->readXSDString());
-      };
-      $struct['elements']['sat'] = function ($point) {
-        $point->setSatellites($this->string2int($this->readXSDString()));
-      };
-      $struct['elements']['hdop'] = function ($point) {
-        $point->setHdop($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['vdop'] = function ($point) {
-        $point->setVdop($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['pdop'] = function ($point) {
-        $point->setPdop($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['ageofdgpsdata'] = function ($point) {
-        $point->setAgeOfDGPSData($this->string2float($this->readXSDString()));
-      };
-      $struct['elements']['dgpsid'] = function ($point) {
-        $point->setDGPSId($this->string2int($this->readXSDString()));
-      };
-      $this->readStruct($struct, $result);
-    } catch (DomainException $e) {
-      throw new MalformedGPXException(
-        $e->getMessage(), $e->getCode(), $e
-      );
     }
-    return $result;
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $route;
   }
 
   /**
-   * Read a rte type.
-   *
-   * @see https://www.topografix.com/GPX/1/1/#type_rteType
+   * Reads a trk type from an XML document.
    *
-   * @return Route
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the trk type.
+   * @return Track A Track data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readRte()
+  protected static function readTrack(XMLReader $xml)
   {
-    try {
-      $result = new Route();
-      $struct = $this->getDataTypeStruct();
-      $struct['elements']['number'] = function ($route) {
-        $route->setNumber($this->string2int($this->readXSDString()));
-      };
-      $struct['elements']['rtept'] = function ($route) {
-        $route->getPoints()[] = $this->readWpt();
-      };
-      $this->readStruct($struct, $result);
-    } catch (DomainException $e) {
-      throw new MalformedGPXException(
-        $e->getMessage(), $e->getCode(), $e
-      );
-    }
-    return $result;
-  }
-
-  /**
-   * Read a trk type.
-   *
-   * @see https://www.topografix.com/GPX/1/1/#type_trkType
-   *
-   * @return Track
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
-   */
-  protected function readTrk()
-  {
-    try {
-      $result = new Track();
-      $struct = $this->getDataTypeStruct();
-      $struct['elements']['number'] = function ($track) {
-        $track->setNumber($this->string2int($this->readXSDString()));
-      };
-      $struct['elements']['trkseg'] = function ($track) {
-        $track->getSegments()[] = $this->readTrkseg();
-      };
-      $this->readStruct($struct, $result);
-    } catch (DomainException $e) {
-      throw new MalformedGPXException(
-        $e->getMessage(), $e->getCode(), $e
-      );
+    $track = new Track();
+    $ns = $xml->namespaceURI;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        if ($xml->namespaceURI == $ns) {
+          switch ($xml->localName) {
+            case 'name':
+              $track->setName($xml->readString());
+              break;
+            case 'cmt':
+              $track->setComment($xml->readString());
+              break;
+            case 'desc':
+              $track->setDescription($xml->readString());
+              break;
+            case 'src':
+              $track->setSource($xml->readString());
+              break;
+            case 'link':
+              $track->getLinks()[] = self::readLink($xml);
+              break;
+            case 'url':
+              $track->getLinks()[] = new Link($xml->readString());
+              break;
+            case 'urlname':
+              $links = $track->getLinks();
+              if (count($links) > 0)
+                $links[0]->setText($xml->readString());
+              break;
+            case 'number':
+              $track->setNumber($xml->readString());
+              break;
+            case 'type';
+              $track->setType($xml->readString());
+              break;
+            case 'extensions':
+              $exts = $track->getExtensions();
+              foreach (self::readExtensions($xml) as $ext) {
+                $exts[] = $ext;
+              }
+              break;
+            case 'trkseg':
+              $track->getSegments()[] = self::readTrackSegment($xml);
+              break;
+          }
+        } else {
+          $track->getExtensions()[] = self::readExtension($xml);
+        }
+      } elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->namespaceURI == $ns && $xml->localName == 'trk') break;
+      }
     }
-    return $result;
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $track;
   }
 
   /**
-   * Read a trkseg type.
-   *
-   * @see https://www.topografix.com/GPX/1/1/#type_trksegType
+   * Reads a trkseg type from an XML document.
    *
-   * @return TrackSegment
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the trkseg type.
+   * @return TrackSegment A TrackSegment data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readTrkseg()
+  protected static function readTrackSegment(XMLReader $xml)
   {
-    $result = new TrackSegment();
-    $struct = [
-      'elements' => [
-        'trkpt' => function ($segment) {
-          $segment->getPoints()[] = $this->readWpt();
+    $segment = new TrackSegment();
+    $ns = $xml->namespaceURI;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        if ($xml->namespaceURI == $ns) {
+          switch ($xml->localName) {
+            case 'trkpt':
+              $segment->getPoints()[] = self::readPoint($xml);
+              break;
+            case 'extensions':
+              $exts = $segment->getExtensions();
+              foreach (self::readExtensions($xml) as $ext) {
+                $exts[] = $ext;
+              }
+              break;
+          }
         }
-      ]
-    ];
-    if ($this->version == '1.1')
-      $struct['elements']['extensions'] = function ($segment) {
-        $this->readExtension($segment->getExtensions());
-      };
-    $this->readStruct($struct, $result);
-    return $result;
+      } elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->namespaceURI == $ns && $xml->localName == 'trkseg') break;
+      }
+    }
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $segment;
   }
 
   /**
-   * Read a data structure from XML.
-   *
-   * This is a generalized function for parsing XML data structures and
-   * populating values based on data retrieved. The main control element is the
-   * `$struct` array which holds a list of callable functions which are used to
-   * modify the supplied object. The structure of the array is:
-   *
-   *     [
-   *       'extensions' => function($object, &$state) {...},
-   *       'elements' => [
-   *         '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.
+   * Reads a wpt type from an XML document.
    *
-   * The anonymous function parameters are as follows:
-   *
-   *   * `$object` - The object to be populated (as passed to this function)
-   *   * `$state`  - An array that can be used to store data temporarily while
-   *                 the structure is being written. This is useful if a value
-   *                 that is being read is split across several XML elements.
-   *
-   * @param array $struct a data structure as defined above.
-   * @param object $object The object to populate with data.
-   * @return void
-   * @throws MalformedGPXException If the GPX file was invalid or not supported.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the wpt type.
+   * @return Point A Point data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readStruct($struct, $object)
+  protected static function readPoint(XMLReader $xml)
   {
-    $xml = $this->xml;
-    if ($xml->isEmptyElement) {
-      $xml->read();
-      return;
-    }
-    $element = $xml->localName;
-    $xml->read();
-    $state = [];
-    while (true) {
-      if (
-        $xml->nodeType == XMLReader::END_ELEMENT
-        && $xml->namespaceURI == $this->ns
-        && $xml->localName == $element
-      ) return;
-      if ($xml->nodeType != XMLReader::ELEMENT) {
-        $xml->read();
-        continue;
-      }
-      if (
-        $xml->namespaceURI == $this->ns
-        && isset($struct['elements'][$xml->localName])
-      ) {
-        if (is_callable($struct['elements'][$xml->localName])) {
-          $struct['elements'][$xml->localName]($object, $state);
+    $point = new Point(
+      $xml->getAttribute('lat'),
+      $xml->getAttribute('lon')
+    );
+    if ($xml->isEmptyElement) return $point;
+    $ns = $xml->namespaceURI;
+    $name = $xml->localName;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        if ($xml->namespaceURI == $ns) {
+          switch ($xml->localName) {
+            case 'ele':
+              $point->setEle($xml->readString());
+              break;
+            case 'time':
+              $point->setTime(new DateTimeImmutable($xml->readString()));
+              break;
+            case 'magvar':
+              $point->setMagvar($xml->readString());
+              break;
+            case 'geoidheight':
+              $point->setGeoidHeight($xml->readString());
+              break;
+            case 'name':
+              $point->setName($xml->readString());
+              break;
+            case 'cmt':
+              $point->setComment($xml->readString());
+              break;
+            case 'desc':
+              $point->setDescription($xml->readString());
+              break;
+            case 'src':
+              $point->setSource($xml->readString());
+              break;
+            case 'link':
+              $point->getLinks()[] = self::readLink($xml);
+              break;
+            case 'url':
+              $point->getLinks()[] = new Link($xml->readString());
+              break;
+            case 'urlname':
+              $links = $point->getLinks();
+              if (count($links) > 0)
+                $links[0]->setText($xml->readString());
+              break;
+            case 'sym':
+              $point->setSymbol($xml->readString());
+              break;
+            case 'type':
+              $point->setType($xml->readString());
+              break;
+            case 'fix':
+              $point->setFix($xml->readString());
+              break;
+            case 'sat':
+              $point->setSatellites($xml->readString());
+              break;
+            case 'hdop':
+              $point->setHdop($xml->readString());
+              break;
+            case 'vdop':
+              $point->setVdop($xml->readString());
+              break;
+            case 'pdop':
+              $point->setPdop($xml->readString());
+              break;
+            case 'ageofdgpsdata':
+              $point->setAgeOfDGPSData($xml->readString());
+              break;
+            case 'dgpsid':
+              $point->setDGPSId($xml->readString());
+              break;
+            case 'extensions':
+              $exts = $point->getExtensions();
+              foreach (self::readExtensions($xml) as $ext) {
+                $exts[] = $ext;
+              }
+              break;
+          }
         } else {
-          $xml->read();
+          $point->getExtensions()[] = self::readExtension($xml);
         }
-        continue;
-      }
-      if (
-        $xml->namespaceURI != $this->ns
-        && isset($struct['extensions'])
-        && is_callable($struct['extensions'])
-      ) {
-        $struct['extensions']($object, $state);
-        continue;
+      } elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->namespaceURI == $ns && $xml->localName == $name) break;
       }
-      throw new MalformedGPXException(
-        sprintf(
-          'Unknown element "%s" in element "%s"',
-          $xml->localName,
-          $element
-        )
-      );
     }
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $point;
   }
 
   /**
-   * Generate a data structure for reading DataType elements.
+   * Reads a person type from an XML document.
    *
-   * @return array
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the person type.
+   * @return Person A Person data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function getDataTypeStruct()
+  protected static function readPerson(XMLReader $xml)
   {
-    $result = [
-      'elements' => [
-        'name' => function ($type) {
-          $type->setName($this->readXSDString());
-        },
-        'cmt' => function ($type) {
-          $type->setComment($this->readXSDString());
-        },
-        'desc' => function ($type) {
-          $type->setDescription($this->readXSDString());
-        },
-        'src' => function ($type) {
-          $type->setSource($this->readXSDString());
-        }
-      ]
-    ];
-    if ($this->version == '1.1') {
-      $result['elements']['link'] = function ($type) {
-        $type->getLinks()[] = $this->readLink();
-      };
-      $result['elements']['type'] = function ($type) {
-        $type->setType($this->readXSDString());
-      };
-      $result['elements']['extensions'] = function ($type) {
-        $this->readExtension($type->getExtensions());
-      };
-    } else {
-      $result['elements']['url'] = function ($type, &$state) {
-        $href = $this->readXSDString();
-        $links = $type->getLinks();
-        if ($links->isEmpty()) {
-          $link = new Link($href);
-          if (isset($state['urlname'])) {
-            $link->setText($state['urlname']);
-            unset($state['urlname']);
-          }
-          $links[] = $link;
-        } else {
-          $links->bottom()->setHref($href);
-        }
-      };
-      $result['elements']['urlname'] = function ($type, &$state) {
-        $text = $this->readXSDString();
-        $links = $type->getLinks();
-        if ($links->isEmpty()) {
-          $state['urlname'] = $text;
-        } else {
-          $links->bottom()->setText($text);
+    $person = new Person();
+    if ($xml->isEmptyElement) return $person;
+    $name = $xml->localName;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        switch ($xml->localName) {
+          case 'name':
+            $person->setName($xml->readString());
+            break;
+          case 'email':
+            $person->setEmail(
+              $xml->getAttribute('id') . '@' . $xml->getAttribute('domain')
+            );
+            break;
+          case 'link':
+            $person->setLink(self::readLink($xml));
+            break;
         }
-      };
-      $result['extensions'] = function ($type) {
-        $type->getExtensions()[] = $this->xml->readOuterXML();
-        $this->xml->next();
-      };
+      }  elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->localName == $name) break;
+      }
     }
-    return $result;
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $person;
   }
 
   /**
-   * Read an `xsd:string` type from XML.
+   * Reads a link type from an XML document.
    *
-   * @return string The text string.
-   * @throws MalformedGPXException If the element has non-text children.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the link type.
+   * @return Link A Link data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function readXSDString()
+  protected static function readLink(XMLReader $xml)
   {
-    $xml = $this->xml;
-    $result = '';
-    if ($xml->nodeType != XMLReader::ELEMENT || $xml->isEmptyElement)
-      return $result;
-    $element = $xml->name;
-    while ($xml->read()) {
-      switch ($xml->nodeType) {
-        case XMLReader::TEXT:
-        case XMLReader::CDATA:
-          $result .= $xml->value;
-          break;
-        case XMLReader::END_ELEMENT:
-          $xml->read();
-          break 2;
-        default:
-          throw new MalformedGPXException(
-            sprintf('Element "%s" contains non-text data.', $element)
-          );
+    $link = new Link($xml->getAttribute('href'));
+    if ($xml->isEmptyElement) return $link;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        switch ($xml->localName) {
+          case 'text':
+            $link->setText($xml->readString());
+            break;
+          case 'type':
+            $link->setType($xml->readString());
+            break;
+        }
+      } elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->localName == 'link') break;
       }
-
     }
-    return $result;
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $link;
   }
 
   /**
-   * Convert a string to DateTime.
+   * Reads a copyright type from an XML document.
    *
-   * @param string $timestamp The timestamp to convert.
-   * @return DateTime The parsed DateTime.
-   * @throws MalformedGPXException If the timestamp could not be parsed.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the copyright type.
+   * @return Copyright A Copyright data structure.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function string2DateTime(string $timestamp)
+  protected static function readCopyright(XMLReader $xml)
   {
-    try {
-      $result = new DateTime($timestamp, new DateTimeZone('Z'));
-    } catch (Exception $e) {
-      throw new MalformedGPXException(
-        sprintf('Unknown datetime format "%s"', $timestamp)
-      );
+    $copyright = new Copyright($xml->getAttribute('author'));
+    if ($xml->isEmptyElement) return $copyright;
+    $name = $xml->localName;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::ELEMENT) {
+        switch ($xml->localName) {
+          case 'year':
+            $copyright->setYear($xml->readString());
+            break;
+          case 'license':
+            $copyright->setLicense($xml->readString());
+            break;
+        }
+      }  elseif ($xml->nodeType == XMLReader::END_ELEMENT) {
+        if ($xml->localName == $name) break;
+      }
     }
-    return $result;
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $copyright;
   }
 
   /**
-   * Convert a string to a float.
+   * Reads an extensions type (GPX 1.1 only) from an XML document.
    *
-   * @param string $value The string value to convert.
-   * @return float
-   * @throws MalformedGPXException If the value is not numeric.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the extensions type.
+   * @return string[] An array of XML snippets describing the extensions.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function string2float(string $value)
+  protected static function readExtensions(XMLReader $xml)
   {
-    if (!is_numeric($value))
-      throw new MalformedGPXException(
-        sprintf('Expected decimal value but got "%s"', $value)
-      );
-    return floatval($value);
+    $extensions = [];
+    if (!$xml->isEmptyElement) {
+      $xml->read();
+      do {
+        switch ($xml->nodeType) {
+          case XMLReader::ELEMENT:
+            $extensions[] = $xml->readOuterXML();
+            break;
+          case XMLReader::END_ELEMENT:
+            break 2;
+        }
+      } while ($valid = ($xml->next() && $xml->isValid()));
+    }
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $extensions;
   }
 
   /**
-   * Convert a string to a int.
+   * Reads a extension (GPX 1.0 only) from an XML document.
    *
-   * @param string $value The string value to convert.
-   * @return int
-   * @throws MalformedGPXException If the value is not numeric.
+   * @param XMLReader $xml An XML document with the cursor positioned at the
+   *        start of the extension element.
+   * @return string An XML snippet describing the extension.
+   * @throws MalformedGPXException If there was a problem parsing the XML.
    */
-  protected function string2int(string $value)
+  protected static function readExtension(XMLReader $xml)
   {
-    if (!is_numeric($value))
-      throw new MalformedGPXException(
-        sprintf('Expected decimal value but got "%s"', $value)
-      );
-    return intval($value);
+    $extension = $xml->readOuterXML();
+    $depth = 0;
+    while ($valid = ($xml->read() && $xml->isValid())) {
+      if ($xml->nodeType == XMLReader::END_ELEMENT && $depth-- == 0) {
+        break;
+      } elseif ($xml->nodeType == XMLReader::ELEMENT && !$xml->isEmptyElement) {
+        $depth++;
+      }
+    }
+    if (!$valid)
+      throw new MalformedGPXException(libxml_get_last_error()->message);
+    return $extension;
   }
 
 }
diff --git a/src/libgpx/peekstream.php b/src/libgpx/peekstream.php
new file mode 100644 (file)
index 0000000..5f481fb
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+/**
+ * peekstream.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;
+
+/**
+ * A PHP stream wrapper with the ability to peek at the first read.
+ *
+ * Note: This class is used to workaround an issue with the `XMLReader` class.
+ *       It should not be called by code outside of libgpx.
+ *
+ * @internal
+ *
+ * @author Andy Street <andy@street.me.uk>
+ */
+class PeekStream
+{
+
+  /**
+   * All instances of this class with open streams.
+   *
+   * @var PeekStream[]
+   */
+  public static $instances = [];
+
+  /**
+   * The handle to the stream to read from.
+   *
+   * @var resource
+   */
+  protected $handle;
+
+  /**
+   * The stream path where data is loaded from.
+   *
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * Cache of the first successful read from the stream.
+   *
+   * @var mixed
+   */
+  protected $first_read;
+
+  /**
+   * The context for this stream.
+   *
+   * Unused but set by PHP so included for completeness.
+   *
+   * @var resource
+   */
+  public $context;
+
+  /**
+   * Remove the protocol from a path.
+   *
+   * @param string $path The path including the protocol for this stream wrapper.
+   * @return string The path without the protocol.
+   */
+  protected function stripProtocol(string $path)
+  {
+    $pos = strpos($path, ':');
+    return ($pos === false ? null : substr($path, $pos + 3));
+  }
+
+  /**
+   * Open a new stream.
+   *
+   * @param string $path The path to open.
+   * @param string $mode The mode used to open the stream.
+   * @param int $options Streams API flags.
+   * @param string $opened_path The path that was actually opened.
+   * @return boolean If it was possible to open a stream.
+   */
+  public function stream_open(
+    string $path,
+    string $mode,
+    int $options,
+    &$opened_path
+  ) {
+    $this->path = $this->stripProtocol($path);
+    if ($this->path === null) return false;
+
+    if (!in_array($mode, ['r', 'rb'])) return false;
+
+    $this->handle = fopen($this->path, $mode);
+    if (!is_resource($this->handle)) return false;
+    self::$instances[] = $this;
+
+    return true;
+  }
+
+  /**
+   * Close a stream.
+   *
+   * @return boolean If the stream was closed successfully.
+   */
+  public function stream_close()
+  {
+    $c = count(self::$instances);
+    for ($i = 0; $i < $c; $i++) {
+      if (self::$instances[$i] === $this) {
+        unset(self::$instances[$i]);
+        break;
+      }
+    }
+    return fclose($this->handle);
+  }
+
+  /**
+   * Check if the pointer is at the end of the stream.
+   *
+   * @return boolean If the pointer is at the end of the stream.
+   */
+  public function stream_eof()
+  {
+    return feof($this->handle);
+  }
+
+  /**
+   * Read data from the stream.
+   *
+   * @param int $count The number of bytes to read.
+   * @return string The bytes read or false on failure.
+   */
+  public function stream_read(int $count)
+  {
+    $data = fread($this->handle, $count);
+    if ($data !== false && $this->first_read === null)
+      $this->first_read = $data;
+    return $data;
+  }
+
+  /**
+   * Fetch file information.
+   *
+   * @param string $path The path to check.
+   * @param int $flags PHP streams API flags.
+   * @return array A `stat` compatible array.
+   */
+  public function url_stat(string $path, int $flags)
+  {
+    $path = $this->stripProtocol($path);
+    if ($path === null) return 0;
+    return stat($path);
+  }
+
+  /**
+   * Fetch the contents of the first read from a stream.
+   *
+   * @param string $path The path to peek at.
+   * @return string|null The first read or null if not available.
+   */
+  public static function peek(string $path)
+  {
+    $result = null;
+    $instance = null;
+    $c = count(self::$instances);
+    for ($i = 0; $i < $c; $i++) {
+      if (self::$instances[$i]->path === $path) {
+        $instance = self::$instances[$i];
+        break;
+      }
+    }
+    if ($instance instanceof PeekStream && is_string($instance->first_read)) {
+      $result = $instance->first_read;
+      $instance->first_read = false;
+    }
+    return $result;
+  }
+
+}