use \DateTime;
use \DateTimeZone;
+use \DomainException;
use \Exception;
use \InvalidArgumentException;
use \RuntimeException;
$result->setCreator($this->xml->getAttribute('creator'));
$struct = [
- 'elements' => []
+ 'elements' => [
+ 'wpt' => function ($gpx) {
+ $gpx->getWaypoints()[] = $this->readWpt();
+ }
+ ]
];
if ($this->version == '1.1') {
$struct['elements']['metadata'] = function ($gpx) {
$struct['elements']['url'] = function ($gpx, &$state) {
$href = $this->readXSDString();
$links = $gpx->getLinks();
- if ($links->isEmpty) {
+ if ($links->isEmpty()) {
$link = new Link($href);
if (isset($state['urlname'])) {
$link->setText($state['urlname']);
$struct['elements']['urlname'] = function ($gpx, &$state) {
$text = $this->readXSDString();
$links = $gpx->getLinks();
- if ($links->isEmpty) {
+ if ($links->isEmpty()) {
$state['urlname'] = $text;
} else {
$links->bottom()->setText($text);
$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 = [
+ 'elements' => [
+ 'ele' => function ($point) {
+ $point->setEle($this->string2float($this->readXSDString()));
+ },
+ 'time' => function ($point) {
+ $point->setTime($this->string2DateTime($this->readXSDString()));
+ },
+ 'magvar' => function ($point) {
+ $point->setMagvar($this->string2float($this->readXSDString()));
+ },
+ 'geoidheight' => function ($point) {
+ $point->setGeoidHeight($this->string2float($this->readXSDString()));
+ },
+ 'name' => function ($point) {
+ $point->setName($this->readXSDString());
+ },
+ 'cmt' => function ($point) {
+ $point->setComment($this->readXSDString());
+ },
+ 'desc' => function ($point) {
+ $point->setDescription($this->readXSDString());
+ },
+ 'src' => function ($point) {
+ $point->setSource($this->readXSDString());
+ },
+ 'sym' => function ($point) {
+ $point->setSymbol($this->readXSDString());
+ },
+ 'type' => function ($point) {
+ $point->setType($this->readXSDString());
+ },
+ 'fix' => function ($point) {
+ $point->setFix($this->readXSDString());
+ },
+ 'sat' => function ($point) {
+ $point->setSatellites($this->string2int($this->readXSDString()));
+ },
+ 'hdop' => function ($point) {
+ $point->setHdop($this->string2float($this->readXSDString()));
+ },
+ 'vdop' => function ($point) {
+ $point->setVdop($this->string2float($this->readXSDString()));
+ },
+ 'pdop' => function ($point) {
+ $point->setPdop($this->string2float($this->readXSDString()));
+ },
+ 'ageofdgpsdata' => function ($point) {
+ $point->setAgeOfDGPSData($this->string2float($this->readXSDString()));
+ },
+ 'dgpsid' => function ($point) {
+ $point->setDGPSId($this->string2int($this->readXSDString()));
+ }
+ ]
+ ];
+ if ($this->version == '1.1') {
+ $struct['elements']['link'] = function ($point) {
+ $point->getLinks()[] = $this->readLink();
+ };
+ $struct['elements']['extensions'] = function ($point) {
+ $this->readExtension($point->getExtensions());
+ };
+ } else {
+ $struct['elements']['url'] = function ($point, &$state) {
+ $href = $this->readXSDString();
+ $links = $point->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 ($point, &$state) {
+ $text = $this->readXSDString();
+ $links = $point->getLinks();
+ if ($links->isEmpty()) {
+ $state['urlname'] = $text;
+ } else {
+ $links->bottom()->setText($text);
+ }
+ };
+ $struct['extensions'] = function ($point) {
+ $point->getExtensions()[] = $this->xml->readOuterXML();
+ $this->xml->next();
+ };
+ }
+ $this->readStruct($struct, $result);
+ } catch (DomainException $e) {
+ throw new MalformedGPXException(
+ $e->getMessage(), $e->getCode(), $e
+ );
+ }
+ return $result;
+ }
+
/**
* Read a data structure from XML.
*
return $result;
}
+ /**
+ * Convert a string to a float.
+ *
+ * @param string $value The string value to convert.
+ * @return float
+ * @throws MalformedGPXException If the value is not numeric.
+ */
+ protected function string2float(string $value)
+ {
+ if (!is_numeric($value))
+ throw new MalformedGPXException(
+ sprintf('Expected decimal value but got "%s"', $value)
+ );
+ return floatval($value);
+ }
+
+ /**
+ * Convert a string to a int.
+ *
+ * @param string $value The string value to convert.
+ * @return int
+ * @throws MalformedGPXException If the value is not numeric.
+ */
+ protected function string2int(string $value)
+ {
+ if (!is_numeric($value))
+ throw new MalformedGPXException(
+ sprintf('Expected decimal value but got "%s"', $value)
+ );
+ return intval($value);
+ }
+
}
$namespace . ' ' . $schema
);
$this->writeMetadata($gpx, $xml);
+ foreach ($gpx->getWaypoints() as $wpt) {
+ $this->writePoint($wpt, 'wpt', $xml);
+ }
$xml->endElement();
$xml->endDocument();
$xml->flush();
return $timestamp->format($format);
}
+ /**
+ * Generate XML for GPX wpt type.
+ *
+ * @param Point $point The point to write.
+ * @param string $element The XML element name (e.g. <wpt>)
+ * @param XMLWriter $xml Where to write the point.
+ * @return void
+ */
+ protected function writePoint(Point $point, string $element, XMLWriter $xml)
+ {
+ $xml->startElement($element);
+ $xml->writeAttribute('lat', $point->getLatitude());
+ $xml->writeAttribute('lon', $point->getLongitude());
+ $val = $point->getEle();
+ if ($val !== null)
+ $xml->writeElement('ele', $val);
+ $val = $point->getTime();
+ if ($val !== null)
+ $xml->writeElement('time', $this->createTimestamp($val));
+ $val = $point->getMagvar();
+ if ($val !== null)
+ $xml->writeElement('magvar', $val);
+ $val = $point->getGeoidHeight();
+ if ($val !== null)
+ $xml->writeElement('geoidheight', $val);
+ $var = $point->getName();
+ if ($var !== null)
+ $xml->writeElement('name', $var);
+ $var = $point->getComment();
+ if ($var !== null)
+ $xml->writeElement('cmt', $var);
+ $var = $point->getDescription();
+ if ($var !== null)
+ $xml->writeElement('desc', $var);
+ $var = $point->getSource();
+ if ($var !== null)
+ $xml->writeElement('src', $var);
+ $var = $point->getLinks(false);
+ if ($var !== null) {
+ foreach($var as $link) {
+ $this->writeLink($link, $xml);
+ if ($this->format == '1.0')
+ break;
+ }
+ }
+ $var = $point->getSymbol();
+ if ($var !== null)
+ $xml->writeElement('sym', $var);
+ $var = $point->getType();
+ if ($var !== null)
+ $xml->writeElement('type', $var);
+ $var = $point->getFix();
+ if ($var !== null)
+ $xml->writeElement('fix', $var);
+ $var = $point->getSatellites();
+ if ($var !== null)
+ $xml->writeElement('sat', $var);
+ $var = $point->getHdop();
+ if ($var !== null)
+ $xml->writeElement('hdop', $var);
+ $var = $point->getVdop();
+ if ($var !== null)
+ $xml->writeElement('vdop', $var);
+ $var = $point->getPdop();
+ if ($var !== null)
+ $xml->writeElement('pdop', $var);
+ $var = $point->getAgeOfDGPSData();
+ if ($var !== null)
+ $xml->writeElement('ageofdgpsdata', $var);
+ $var = $point->getDGPSId();
+ if ($var !== null)
+ $xml->writeElement('dgpsid', $var);
+ $var = $point->getExtensions(false);
+ if ($var !== null && !$var->isEmpty()) {
+ if ($this->format == '1.1')
+ $xml->startElement('extensions');
+ foreach ($var as $extension) {
+ $xml->writeRaw($extension);
+ }
+ if ($this->format == '1.1')
+ $xml->endElement();
+ }
+ $xml->endElement();
+ }
+
}
--- /dev/null
+<?php
+/**
+ * point.php
+ *
+ * Copyright 2018 Andy Street <andy@street.me.uk>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301, USA.
+ *
+ *
+ */
+
+namespace libgpx;
+
+use \DomainException;
+use \DateTimeInterface;
+
+/**
+ * A geographic point.
+ *
+ * @see https://www.topografix.com/GPX/1/1/#type_wptType
+ *
+ * @author Andy Street <andy@street.me.uk>
+ */
+class Point implements Geographic
+{
+
+ /**
+ * The latitude of the point.
+ *
+ * @var float
+ */
+ protected $lat;
+
+ /**
+ * The longitude of the point.
+ *
+ * @var float
+ */
+ protected $lon;
+
+ /**
+ * Elevation (in meters) of the point.
+ *
+ * @var float
+ */
+ protected $ele;
+
+ /**
+ * 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.
+ *
+ * @var DateTimeInterface
+ */
+ protected $time;
+
+ /**
+ * Magnetic variation (in degrees) at the point.
+ *
+ * @var float
+ */
+ protected $magvar;
+
+ /**
+ * Height (in meters) of geoid (mean sea level) above WGS84 earth ellipsoid.
+ * As defined in NMEA GGA message.
+ *
+ * @var float
+ */
+ protected $geoidheight;
+
+ /**
+ * The name of the point.
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * GPS waypoint comment.
+ *
+ * Sent to GPS as comment.
+ *
+ * @var string
+ */
+ protected $comment;
+
+ /**
+ * A text description of the element.
+ *
+ * Holds additional information about the element intended for the user,
+ * not the GPS.
+ *
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * Source of data.
+ *
+ * Included to give user some idea of reliability and accuracy of data.
+ * E.g. "Garmin eTrex", "USGS quad Boston North".
+ *
+ * @var string
+ */
+ protected $source;
+
+ /**
+ * A list of links associated with this point.
+ *
+ * @var TypedDoublyLinkedList (Link)
+ */
+ protected $links;
+
+ /**
+ * 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.
+ *
+ * @var string
+ */
+ protected $symbol;
+
+ /**
+ * Type (classification) of the point.
+ *
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * Type of GPS fix.
+ *
+ * "none", "2d", "3d", "dgps" or "pps".
+ *
+ * None means GPS had no fix. To signify "the fix info is unknown, leave out
+ * fixType entirely. pps = military signal used.
+ *
+ * @var string
+ */
+ protected $fix;
+
+ /**
+ * Number of satellites used to calculate the GPX fix.
+ *
+ * @var int
+ */
+ protected $satellites;
+
+ /**
+ * Horizontal dilution of precision.
+ *
+ * @var float
+ */
+ protected $hdop;
+
+ /**
+ * Vertical dilution of precision.
+ *
+ * @var float
+ */
+ protected $vdop;
+
+ /**
+ * Position dilution of precision.
+ *
+ * @var float
+ */
+ protected $pdop;
+
+ /**
+ * Number of seconds since last DGPS update.
+ *
+ * @var float
+ */
+ protected $ageofdgpsdata;
+
+ /**
+ * ID of DGPS station used in differential correction.
+ *
+ * @var int
+ */
+ protected $dgpsid;
+
+ /**
+ * A list of XML snippets describing unsupported data.
+ *
+ * @var TypedDoublyLinkedList (string)
+ */
+ protected $extensions;
+
+ /**
+ * Create a new point.
+ *
+ * @param float $lat Latitude
+ * @param float $lon Longitude
+ * @throws DomainException If not a valid latitude or longitude.
+ */
+ public function __construct(float $lat, float $lon)
+ {
+ $this->setLatitude($lat);
+ $this->setLongitude($lon);
+ }
+
+ /**
+ * Fetch the latitude of the point.
+ *
+ * @return float
+ */
+ public function getLatitude()
+ {
+ return $this->lat;
+ }
+
+ /**
+ * Set the latitude of the point.
+ *
+ * @param float $lat Latitude
+ * @return void
+ * @throws DomainException If not a valid latitude.
+ */
+ public function setLatitude(float $lat)
+ {
+ if ($lat < -90 || $lat > 90)
+ throw new DomainException(
+ sprintf('Value %s is not in the range -90-+90.', $lat)
+ );
+ $this->lat = $lat;
+ }
+
+ /**
+ * Fetch the longitude of the point.
+ *
+ * @return float
+ */
+ public function getLongitude()
+ {
+ return $this->lon;
+ }
+
+ /**
+ * Set the longitude of the point.
+ *
+ * @param float $lon Longitude
+ * @return void
+ * @throws DomainException If not a valid longitude.
+ */
+ public function setLongitude(float $lon)
+ {
+ if ($lon < -180 || $lon > 180)
+ throw new DomainException(
+ sprintf('Value %s is not in the range -180-+180.', $lon)
+ );
+ $this->lon = $lon;
+ }
+
+ /**
+ * Fetch the elevation of the point.
+ *
+ * @return float|null The elevation in meters or null if not set.
+ */
+ public function getEle()
+ {
+ return $this->ele;
+ }
+
+ /**
+ * Set the elevation of the point.
+ *
+ * @param float $ele The elevation in meters or null to delete.
+ * @return void
+ */
+ public function setEle(float $ele = null)
+ {
+ $this->ele = $ele;
+ }
+
+ /**
+ * Fetch the creation time of the point.
+ *
+ * @return DateTimeInterface|null The time or null if not set.
+ */
+ public function getTime()
+ {
+ return $this->time;
+ }
+
+ /**
+ * Set the creation time of the point.
+ *
+ * @param DateTimeInterface $time The creation time or null to delete.
+ * @return void
+ */
+ public function setTime(DateTimeInterface $time = null)
+ {
+ $this->time = $time;
+ }
+
+ /**
+ * Fetch the magnetic variation.
+ *
+ * @return float|null The magnetic variation in degrees or null if not set.
+ */
+ public function getMagvar()
+ {
+ return $this->magvar;
+ }
+
+ /**
+ * Set the magnetic variation.
+ *
+ * @param float $magvar The magnetic variation in degrees or null to delete.
+ * @return void
+ * @throws DomainException if not in range 0 <= value < 360
+ */
+ public function setMagvar(float $magvar = null)
+ {
+ if (
+ $magvar !== null
+ && ($magvar < 0 || $magvar >= 360)
+ ) {
+ throw new DomainException(
+ sprintf('Value %s is not in the range 0 <= value < 360.', $lon)
+ );
+ }
+ $this->magvar = $magvar;
+ }
+
+ /**
+ * Fetch the geoid height at the point.
+ *
+ * @return float The geoid height in meters.
+ */
+ public function getGeoidHeight()
+ {
+ return $this->geoidheight;
+ }
+
+ /**
+ * Set the geoid height at the point.
+ *
+ * @param float $geoidheight The geoid height in meters.
+ * @return void
+ */
+ public function setGeoidHeight(float $geoidheight = null)
+ {
+ $this->geoidheight = $geoidheight;
+ }
+
+ /**
+ * Fetch the name of the point.
+ *
+ * @return string|null The name or null if not set.
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the name of the point.
+ *
+ * @param string $name The name or null to delete.
+ * @return void
+ */
+ public function setName(string $name = null)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Fetch a comment for the point.
+ *
+ * @return string The comment or null if not set.
+ */
+ public function getComment()
+ {
+ return $this->comment;
+ }
+
+ /**
+ * Set a comment for the point.
+ *
+ * @param string $comment The comment or null to delete.
+ * @return void
+ */
+ public function setComment(string $comment = null)
+ {
+ $this->comment = $comment;
+ }
+
+ /**
+ * Fetch the description for the point.
+ *
+ * @return string The description or null if not set.
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * Set the description of the point.
+ *
+ * @param string $description The description or null to delete.
+ * @return void
+ */
+ public function setDescription(string $description = null)
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * Fetch the source of the data.
+ *
+ * @return string|null The source or null if not set.
+ */
+ public function getSource()
+ {
+ return $this->source;
+ }
+
+ /**
+ * Set the source of the data.
+ *
+ * @param string $source The source of the data or null to delete.
+ * @return void
+ */
+ public function setSource(string $source = null)
+ {
+ $this->source = $source;
+ }
+
+ /**
+ * Fetch a list of links associated with this point.
+ *
+ * @param boolean $create Create the list if it does not already exist.
+ * @return TypedDoublyLinkedList|null A list of Link objects.
+ */
+ public function getLinks(bool $create = true)
+ {
+ if ($create && $this->links === null)
+ $this->links = new TypedDoublyLinkedList('libgpx\Link');
+ return $this->links;
+ }
+
+ /**
+ * Fetch the waypoint symbol.
+ *
+ * @return string|null The symbol or null if not set.
+ */
+ public function getSymbol()
+ {
+ return $this->symbol;
+ }
+
+ /**
+ * Set the symbol representing this point.
+ *
+ * @param string $symbol The name of the symbol or null to delete.
+ * @return void
+ */
+ public function setSymbol(string $symbol = null)
+ {
+ $this->symbol = $symbol;
+ }
+
+ /**
+ * Fetch the type of the point.
+ *
+ * @return string|null The type or null if not set.
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Set the type of the point.
+ *
+ * @param string $type The type or null to delete.
+ * @return void
+ */
+ public function setType(string $type = null)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * Fetch the fix type for this point.
+ *
+ * @return string|null The fix type or null if not set.
+ */
+ public function getFix()
+ {
+ return $this->fix;
+ }
+
+ /**
+ * Set the fix type.
+ *
+ * @param string $fix "none", "2d", "3d", "dgps", "pps" or null to delete.
+ * @return void
+ * @throws DomainException If an invalid fix type is passed.
+ */
+ public function setFix(string $fix = null)
+ {
+ if ($fix !== null && !in_array($fix, ['none', '2d', '3d', 'dgps', 'pps']))
+ throw new DomainException(sprintf('Unknown fix type "%s"', $fix));
+ $this->fix = $fix;
+ }
+
+ /**
+ * Fetch the number of satellites used to calculate the fix.
+ *
+ * @return int The number of satellites or null if not set.
+ */
+ public function getSatellites()
+ {
+ return $this->satellites;
+ }
+
+ /**
+ * Set the number of satellites used to calculate the fix.
+ *
+ * @param int $satellites The number if satellites or null to delete.
+ * @return void
+ * @throws DomainException If $satellites < 0
+ */
+ public function setSatellites(int $satellites = null)
+ {
+ if ($satellites !== null && $satellites < 0)
+ throw new DomainException(
+ sprintf('Satellites must be >= 0 but got %s', $satellites)
+ );
+ $this->satellites = $satellites;
+ }
+
+ /**
+ * Fetch the hdop of the point.
+ *
+ * @return float|null The hdop or null if not set.
+ */
+ public function getHdop()
+ {
+ return $this->hdop;
+ }
+
+ /**
+ * Set the hdop of the point.
+ *
+ * @param float $hdop The hdop or null to delete.
+ * @return void
+ */
+ public function setHdop(float $hdop = null)
+ {
+ $this->hdop = $hdop;
+ }
+
+ /**
+ * Fetch the vdop of the point.
+ *
+ * @return float|null The vdop or null if not set.
+ */
+ public function getVdop()
+ {
+ return $this->vdop;
+ }
+
+ /**
+ * Set the vdop of the point.
+ *
+ * @param float $vdop The vdop or null to delete.
+ * @return void
+ */
+ public function setVdop(float $vdop = null)
+ {
+ $this->vdop = $vdop;
+ }
+
+ /**
+ * Fetch the pdop of the point.
+ *
+ * @return float|null The pdop or null if not set.
+ */
+ public function getPdop()
+ {
+ return $this->pdop;
+ }
+
+ /**
+ * Set the pdop of the point.
+ *
+ * @param float $pdop The pdop or null to delete.
+ * @return void
+ */
+ public function setPdop(float $pdop = null)
+ {
+ $this->pdop = $pdop;
+ }
+
+ /**
+ * Fetch the age of the DGPS data.
+ *
+ * @return float The age of DGPS data in seconds or null if not set.
+ */
+ public function getAgeOfDGPSData()
+ {
+ return $this->ageofdgpsdata;
+ }
+
+ /**
+ * Set the age of the DGPS data.
+ *
+ * @param float $ageofdgpsdata The age in seconds or null to delete.
+ * @return void
+ */
+ public function setAgeOfDGPSData(float $ageofdgpsdata = null)
+ {
+ $this->ageofdgpsdata = $ageofdgpsdata;
+ }
+
+ /**
+ * Fetch the id of the DGPS station.
+ *
+ * @return int The DGPS id or null if not set.
+ */
+ public function getDGPSId()
+ {
+ return $this->dgpsid;
+ }
+
+ /**
+ * Set the id of the DGPS station.
+ *
+ * @param int $dgpsid The station id.
+ * @return void
+ * @throws DomainException If the station id < 0 or > 1023.
+ */
+ public function setDGPSId(int $dgpsid = null)
+ {
+ if ($dgpsid !== null && ($dgpsid < 0 || $dgpsid > 1023))
+ throw new DomainException(
+ sprintf('DGPS id must be >= 0 and <= 1023 but got %s', $dgpsid)
+ );
+ return $this->dgpsid = $dgpsid;
+ }
+
+ /**
+ * Fetch a list of XML strings that describe unsupported elements.
+ *
+ * @param boolean $create Create the list if it does not already exist.
+ * @return TypedDoublyLinkedList|null A list of XML strings.
+ */
+ public function getExtensions(bool $create = true)
+ {
+ if ($create && $this->extensions === null)
+ $this->extensions = new TypedDoublyLinkedList('string');
+ return $this->extensions;
+ }
+
+ /**
+ * Fetch a bounding box that covers the feature.
+ *
+ * @return Bounds|null A bounding box covering the extent of the feature or
+ * null if not applicable.
+ */
+ public function getBounds()
+ {
+ return new Bounds($this->lat, $this->lon, $this->lat, $this->lon);
+ }
+}