* * 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 \DateTime; use \DateTimeInterface; use \DateTimeZone; use \RuntimeException; use \UnexpectedValueException; use \XMLWriter; /** * Write GPX files. * * @author Andy Street */ class GPXWriter { /** * Who to credit as the document creator in the GPX root element. * * @var string * @see http://www.topografix.com/GPX/1/1/#type_gpxType */ protected $creator = 'libgpx'; /** * The version of the GPX standard to generate. * * @var string */ protected $format = '1.1'; /** * Whether to indent the XML output for readability. * * @var boolean */ protected $indent = true; /** * Whether to include milliseconds in timestamps. * * @var boolean */ protected $milliseconds = true; /** * Fetch the name credited as the creator of the XML files. * * @return string The creator. */ public function getCreator() { return $this->creator; } /** * Set the name to be credited as the XML document creator. * * @param string $creator The name to credit as the creator. * @return void */ public function setCreator(string $creator) { $this->creator = $creator; } /** * Fetch the GPX file format that this class generates. * * @return string The GPX file format. */ public function getFormat() { return $this->format; } /** * Set the GPX file format that this class generates. * * @param string $format Accepted values are "1.0" or "1.1". * @return void * @throws UnexpectedValueException If $format is not a known GPX version. */ public function setFormat(string $format) { switch ($format) { case '1.0': case '1.1': $this->format = $format; break; default: throw new UnexpectedValueException( sprintf('Unknown GPX version "%s"', $format) ); } } /** * Whether this class will indent output for readability. * * @return boolean If the output will be indented. */ public function getIndent() { return $this->indent; } /** * Set whether to indent output for readability. * * @param boolean $indent Whether to indent. * @return void */ public function setIndent(bool $indent) { $this->indent = $indent; } /** * Whether timestamps are written to include milliseconds. * * @return boolean */ public function getTimestampUseMilliseconds() { return $this->milliseconds; } /** * Set whether to output milliseconds in timestamps. * * @param boolean $milliseconds Whether to use milliseconds. * @return void */ public function setTimestampUseMilliseconds(bool $milliseconds) { $this->milliseconds = $milliseconds; } /** * Fetch an XML representation as a string. * * @param GPX $gpx The source for the XML. * @return string An XML representation of the GPX object. */ public function fetch(GPX $gpx) { $xml = new XMLWriter(); $xml->openMemory(); $this->write($gpx, $xml); return $xml->outputMemory(); } /** * Write XML to standard out. * * @param GPX $gpx The source for the XML. * @return void */ public function output(GPX $gpx) { $xml = new XMLWriter(); $xml->openURI('php://output'); $this->write($gpx, $xml); } /** * Write XML to a file. * * @param GPX $gpx The source for the XML. * @param string $filename Where to write the XML. * @return void * @throws RuntimeException If the file cannot be written to. */ public function save(GPX $gpx, string $filename) { $xml = new XMLWriter(); if (!$xml->openURI('php://output')) throw RuntimeException(sprintf('Unable to write to "%s"', $filename)); $this->write($gpx, $xml); } /** * Generate the XML. * * @param GPX $gpx The source for the XML. * @param XMLWriter $xml The instance to use to generate the XML. * @return void */ protected function write(GPX $gpx, XMLWriter $xml) { $xml->setIndent($this->indent); $xml->startDocument('1.0', 'UTF-8'); if ($this->format == '1.1') { $namespace = libgpx::NAMESPACE_GPX_1_1; $schema = libgpx::SCHEMA_GPX_1_1; } else { $namespace = libgpx::NAMESPACE_GPX_1_0; $schema = libgpx::SCHEMA_GPX_1_0; } $xml->startElementNs(null, 'gpx', $namespace); $xml->writeAttribute('version', $this->format); $xml->writeAttribute('creator', $this->creator); $xml->writeAttributeNS( 'xsi', 'schemaLocation', libgpx::NAMESPACE_XMLSCHEMA, $namespace . ' ' . $schema ); $this->writeMetadata($gpx, $xml); $waypoints = $gpx->getWaypoints(false); if ($waypoints !== null) { foreach ($waypoints as $wpt) { $this->writePoint($wpt, 'wpt', $xml); } } $routes = $gpx->getRoutes(false); if ($routes !== null) { foreach ($routes as $route) { $this->writeRoute($route, $xml); } } $tracks = $gpx->getTracks(false); if ($tracks !== null) { foreach ($tracks as $track) { $this->writeTrack($track, $xml); } } $extensions = $gpx->getExtensions(false); if ($extensions !== null && !$extensions->isEmpty()) { if ($this->format == '1.1') $xml->startElement('extensions'); foreach ($extensions as $extension) { $xml->writeRaw($extension); } if ($this->format == '1.1') $xml->endElement(); } $xml->endElement(); $xml->endDocument(); $xml->flush(); } /** * Generate XML for Metadata elements. * * @param GPX $gpx The object to source the metadata from. * @param XMLWriter $xml Where to write the metadata. * @return void */ protected function writeMetadata(GPX $gpx, XMLWriter $xml) { if ($this->format == '1.1') $xml->startElement('metadata'); $name = $gpx->getName(); if (!empty($name)) $xml->writeElement('name', $name); $desc = $gpx->getDesc(); if (!empty($desc)) $xml->writeElement('desc', $desc); $author = $gpx->getAuthor(); if ($author instanceof Person) $this->writePerson($author, $xml); if ($this->format == '1.1') { $copyright = $gpx->getCopyright(); if ($copyright instanceof Copyright) $this->writeCopyright($copyright, $xml); } $links = $gpx->getLinks(false); if ($links !== null) { foreach($links as $link) { $this->writeLink($link, $xml); if ($this->format == '1.0') break; } } $xml->writeElement('time', $this->createTimestamp(new DateTime())); $keywords = $gpx->getKeywords(false); if ($keywords !== null && !$keywords->isEmpty()) $xml->writeElement( 'keywords', implode(', ', $gpx->getKeywords()->toArray()) ); $bounds = $gpx->getBounds(); if ($bounds instanceof Bounds) { $xml->startElement('bounds'); $xml->writeAttribute('minlat', $bounds->getMinLat()); $xml->writeAttribute('minlon', $bounds->getMinLon()); $xml->writeAttribute('maxlat', $bounds->getMaxLat()); $xml->writeAttribute('maxlon', $bounds->getMaxLon()); $xml->endElement(); } if ($this->format == '1.1') { $extensions = $gpx->getMetadataExtensions(false); if ($extensions !== null && !$extensions->isEmpty()) { $xml->startElement('extensions'); foreach ($extensions as $extension) { $xml->writeRaw($extension); } $xml->endElement(); } $xml->endElement(); // } } /** * Generate XML for GPX Person type. * * @param Person $person The person object to source data from. * @param XMLWriter $xml Where to write the data. * @return void */ protected function writePerson(Person $person, XMLWriter $xml) { if ($this->format == '1.1') { $xml->startElement('author'); $name = $person->getName(); if (!empty($name)) $xml->writeElement('name', $name); $email = $person->getEmail(); if (!empty($email)) { $email = explode('@', $email, 2); $xml->startElement('email'); $xml->writeAttribute('id', $email[0]); $xml->writeAttribute('domain', $email[1]); $xml->endElement(); } $link = $person->getLink(); if (!empty($link)) $this->writeLink($link, $xml); $xml->endElement(); } else { $name = $person->getName(); if (!empty($name)) $xml->writeElement('author', $name); $email = $person->getEmail(); if (!empty($email)) $xml->writeElement('email', $email); } } /** * Generate XML for a GPX Link type. * * @param Link $link The link object to source data from. * @param XMLWriter $xml Where to write the data. * @return void */ protected function writeLink(Link $link, XMLWriter $xml) { if ($this->format == '1.1') { $xml->startElement('link'); $xml->writeAttribute('href', $link->getHref()); $text = $link->getText(); if (!empty($text)) $xml->writeElement('text', $text); $type = $link->getType(); if (!empty($type)) $xml->writeElement('type', $type); $xml->endElement(); } else { $xml->writeElement('url', $link->getHref()); $text = $link->getText(); if (!empty($text)) $xml->writeElement('urlname', $text); } } /** * Generate XML for GPX Copyright type. * * @param Copyright $copyright Object to source copyright data from. * @param XMLWriter $xml Where to write the data. * @return void */ protected function writeCopyright(Copyright $copyright, XMLWriter $xml) { $xml->startElement('copyright'); $xml->writeAttribute('author', $copyright->getAuthor()); $year = $copyright->getYear(); if (!empty($year)) $xml->writeElement('year', $year); $license = $copyright->getLicense(); if (!empty($license)) $xml->writeElement('license', $license); $xml->endElement(); } /** * Create an ISO 8601 timestamp. * * @param DateTimeInterface $timestamp The timestamp to format. * @param boolean $millis Include milliseconds. * @return string A timestamp. */ protected function createTimestamp( DateTimeInterface $timestamp, bool $millis = null ) { if ($millis === null) $millis = $this->milliseconds; if (!$timestamp->getTimezone()->getOffset($timestamp)) $timestamp = $timestamp->setTimezone(new DateTimeZone('UTC')); $format = 'Y-m-d\TH:i:s' . ($millis ? '.v\Z' : '\Z'); return $timestamp->format($format); } /** * Write out the common attributes for all data types. * * @param DataType $type The data type to write. * @param XMLWriter $xml Where to write. * @return void */ protected function writeDataType(DataType $type, XMLWriter $xml) { $var = $type->getName(); if ($var !== null) $xml->writeElement('name', $var); $var = $type->getComment(); if ($var !== null) $xml->writeElement('cmt', $var); $var = $type->getDescription(); if ($var !== null) $xml->writeElement('desc', $var); $var = $type->getSource(); if ($var !== null) $xml->writeElement('src', $var); $var = $type->getLinks(false); if ($var !== null) { foreach($var as $link) { $this->writeLink($link, $xml); if ($this->format == '1.0') break; } } if ($this->format == '1.1' || $type instanceof Point) { $var = $type->getType(); if ($var !== null) $xml->writeElement('type', $var); } $var = $type->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(); } } /** * Generate XML for GPX wpt type. * * @param Point $point The point to write. * @param string $element The XML element name (e.g. ) * @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->getSymbol(); if ($var !== null) $xml->writeElement('sym', $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); $this->writeDataType($point, $xml); $xml->endElement(); } /** * Write XML for a route element. * * @param Route $route The route to write. * @param XMLWriter $xml Where to write. * @return void */ protected function writeRoute(Route $route, XMLWriter $xml) { $xml->startElement('rte'); $var = $route->getNumber(); if ($var !== null) $xml->writeElement('number', $var); $this->writeDataType($route, $xml); $var = $route->getPoints(false); if ($var !== null) { foreach ($var as $point) { $this->writePoint($point, 'rtept', $xml); } } $xml->endElement(); } /** * Write XML for a track element. * * @param Track $track The track to write. * @param XMLWriter $xml Where to write. * @return void */ protected function writeTrack(Track $track, XMLWriter $xml) { $xml->startElement('trk'); $var = $track->getNumber(); if ($var !== null) $xml->writeElement('number', $var); $this->writeDataType($track, $xml); $var = $track->getSegments(false); if ($var !== null) { foreach ($var as $segment) { $this->writeTrackSegment($segment, $xml); } } $xml->endElement(); } /** * Write XML for a track segment element. * * @param TrackSegment $segment The segment to write. * @param XMLWriter $xml Where to write. * @return void */ protected function writeTrackSegment(TrackSegment $segment, XMLWriter $xml) { $xml->startElement('trkseg'); $var = $segment->getPoints(false); if ($var !== null) { foreach ($var as $point) { $this->writePoint($point, 'trkpt', $xml); } } if ($this->format == '1.1') { $var = $segment->getExtensions(false); if ($var !== null) { $xml->startElement('extensions'); foreach ($var as $extension) { $xml->writeRaw($extension); } $xml->endElement(); } } $xml->endElement(); } }