5 * Copyright 2018 Andy Street <andy@street.me.uk>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
28 use \DateTimeInterface;
30 use \RuntimeException;
31 use \UnexpectedValueException;
37 * @author Andy Street <andy@street.me.uk>
43 * Who to credit as the document creator in the GPX root element.
46 * @see http://www.topografix.com/GPX/1/1/#type_gpxType
48 protected $creator = 'libgpx';
51 * The version of the GPX standard to generate.
55 protected $format = '1.1';
58 * Whether to indent the XML output for readability.
62 protected $indent = true;
65 * Whether to include milliseconds in timestamps.
69 protected $milliseconds = true;
72 * Fetch the name credited as the creator of the XML files.
74 * @return string The creator.
76 public function getCreator()
78 return $this->creator;
82 * Set the name to be credited as the XML document creator.
84 * @param string $creator The name to credit as the creator.
87 public function setCreator(string $creator)
89 $this->creator = $creator;
93 * Fetch the GPX file format that this class generates.
95 * @return string The GPX file format.
97 public function getFormat()
103 * Set the GPX file format that this class generates.
105 * @param string $format Accepted values are "1.0" or "1.1".
107 * @throws UnexpectedValueException If $format is not a known GPX version.
109 public function setFormat(string $format)
114 $this->format = $format;
117 throw new UnexpectedValueException(
118 sprintf('Unknown GPX version "%s"', $format)
124 * Whether this class will indent output for readability.
126 * @return boolean If the output will be indented.
128 public function getIndent()
130 return $this->indent;
134 * Set whether to indent output for readability.
136 * @param boolean $indent Whether to indent.
139 public function setIndent(bool $indent)
141 $this->indent = $indent;
145 * Whether timestamps are written to include milliseconds.
149 public function getTimestampUseMilliseconds()
151 return $this->milliseconds;
155 * Set whether to output milliseconds in timestamps.
157 * @param boolean $milliseconds Whether to use milliseconds.
160 public function setTimestampUseMilliseconds(bool $milliseconds)
162 $this->milliseconds = $milliseconds;
166 * Fetch an XML representation as a string.
168 * @param GPX $gpx The source for the XML.
169 * @return string An XML representation of the GPX object.
171 public function fetch(GPX $gpx)
173 $xml = new XMLWriter();
175 $this->write($gpx, $xml);
176 return $xml->outputMemory();
180 * Write XML to standard out.
182 * @param GPX $gpx The source for the XML.
185 public function output(GPX $gpx)
187 $xml = new XMLWriter();
188 $xml->openURI('php://output');
189 $this->write($gpx, $xml);
193 * Write XML to a file.
195 * @param GPX $gpx The source for the XML.
196 * @param string $filename Where to write the XML.
198 * @throws RuntimeException If the file cannot be written to.
200 public function save(GPX $gpx, string $filename)
202 $xml = new XMLWriter();
203 if (!$xml->openURI('php://output'))
204 throw RuntimeException(sprintf('Unable to write to "%s"', $filename));
205 $this->write($gpx, $xml);
211 * @param GPX $gpx The source for the XML.
212 * @param XMLWriter $xml The instance to use to generate the XML.
215 protected function write(GPX $gpx, XMLWriter $xml)
217 $xml->setIndent($this->indent);
218 $xml->startDocument('1.0', 'UTF-8');
219 if ($this->format == '1.1') {
220 $namespace = libgpx::NAMESPACE_GPX_1_1;
221 $schema = libgpx::SCHEMA_GPX_1_1;
223 $namespace = libgpx::NAMESPACE_GPX_1_0;
224 $schema = libgpx::SCHEMA_GPX_1_0;
226 $xml->startElementNs(null, 'gpx', $namespace);
227 $xml->writeAttribute('version', $this->format);
228 $xml->writeAttribute('creator', $this->creator);
229 $xml->writeAttributeNS(
232 libgpx::NAMESPACE_XMLSCHEMA,
233 $namespace . ' ' . $schema
235 $this->writeMetadata($gpx, $xml);
236 $waypoints = $gpx->getWaypoints(false);
237 if ($waypoints !== null) {
238 foreach ($waypoints as $wpt) {
239 $this->writePoint($wpt, 'wpt', $xml);
242 $routes = $gpx->getRoutes(false);
243 if ($routes !== null) {
244 foreach ($routes as $route) {
245 $this->writeRoute($route, $xml);
248 $tracks = $gpx->getTracks(false);
249 if ($tracks !== null) {
250 foreach ($tracks as $track) {
251 $this->writeTrack($track, $xml);
254 $extensions = $gpx->getExtensions(false);
255 if ($extensions !== null && !$extensions->isEmpty()) {
256 if ($this->format == '1.1')
257 $xml->startElement('extensions');
258 foreach ($extensions as $extension) {
259 $xml->writeRaw($extension);
261 if ($this->format == '1.1')
270 * Generate XML for Metadata elements.
272 * @param GPX $gpx The object to source the metadata from.
273 * @param XMLWriter $xml Where to write the metadata.
276 protected function writeMetadata(GPX $gpx, XMLWriter $xml)
278 if ($this->format == '1.1')
279 $xml->startElement('metadata');
280 $name = $gpx->getName();
282 $xml->writeElement('name', $name);
283 $desc = $gpx->getDesc();
285 $xml->writeElement('desc', $desc);
286 $author = $gpx->getAuthor();
287 if ($author instanceof Person)
288 $this->writePerson($author, $xml);
289 if ($this->format == '1.1') {
290 $copyright = $gpx->getCopyright();
291 if ($copyright instanceof Copyright)
292 $this->writeCopyright($copyright, $xml);
294 $links = $gpx->getLinks(false);
295 if ($links !== null) {
296 foreach($links as $link) {
297 $this->writeLink($link, $xml);
298 if ($this->format == '1.0')
302 $xml->writeElement('time', $this->createTimestamp(new DateTime()));
303 $keywords = $gpx->getKeywords(false);
304 if ($keywords !== null && !$keywords->isEmpty())
307 implode(', ', $gpx->getKeywords()->toArray())
309 $bounds = $gpx->getBounds();
310 if ($bounds instanceof Bounds) {
311 $xml->startElement('bounds');
312 $xml->writeAttribute('minlat', $bounds->getMinLat());
313 $xml->writeAttribute('minlon', $bounds->getMinLon());
314 $xml->writeAttribute('maxlat', $bounds->getMaxLat());
315 $xml->writeAttribute('maxlon', $bounds->getMaxLon());
318 if ($this->format == '1.1') {
319 $extensions = $gpx->getMetadataExtensions(false);
320 if ($extensions !== null && !$extensions->isEmpty()) {
321 $xml->startElement('extensions');
322 foreach ($extensions as $extension) {
323 $xml->writeRaw($extension);
327 $xml->endElement(); // </metadata>
332 * Generate XML for GPX Person type.
334 * @param Person $person The person object to source data from.
335 * @param XMLWriter $xml Where to write the data.
338 protected function writePerson(Person $person, XMLWriter $xml)
340 if ($this->format == '1.1') {
341 $xml->startElement('author');
342 $name = $person->getName();
344 $xml->writeElement('name', $name);
345 $email = $person->getEmail();
346 if (!empty($email)) {
347 $email = explode('@', $email, 2);
348 $xml->startElement('email');
349 $xml->writeAttribute('id', $email[0]);
350 $xml->writeAttribute('domain', $email[1]);
353 $link = $person->getLink();
355 $this->writeLink($link, $xml);
358 $name = $person->getName();
360 $xml->writeElement('author', $name);
361 $email = $person->getEmail();
363 $xml->writeElement('email', $email);
368 * Generate XML for a GPX Link type.
370 * @param Link $link The link object to source data from.
371 * @param XMLWriter $xml Where to write the data.
374 protected function writeLink(Link $link, XMLWriter $xml)
376 if ($this->format == '1.1') {
377 $xml->startElement('link');
378 $xml->writeAttribute('href', $link->getHref());
379 $text = $link->getText();
381 $xml->writeElement('text', $text);
382 $type = $link->getType();
384 $xml->writeElement('type', $type);
387 $xml->writeElement('url', $link->getHref());
388 $text = $link->getText();
390 $xml->writeElement('urlname', $text);
395 * Generate XML for GPX Copyright type.
397 * @param Copyright $copyright Object to source copyright data from.
398 * @param XMLWriter $xml Where to write the data.
401 protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
403 $xml->startElement('copyright');
404 $xml->writeAttribute('author', $copyright->getAuthor());
405 $year = $copyright->getYear();
407 $xml->writeElement('year', $year);
408 $license = $copyright->getLicense();
409 if (!empty($license))
410 $xml->writeElement('license', $license);
415 * Create an ISO 8601 timestamp.
417 * @param DateTimeInterface $timestamp The timestamp to format.
418 * @param boolean $millis Include milliseconds.
419 * @return string A timestamp.
421 protected function createTimestamp(
422 DateTimeInterface $timestamp,
425 if ($millis === null)
426 $millis = $this->milliseconds;
427 if (!$timestamp->getTimezone()->getOffset($timestamp))
428 $timestamp = $timestamp->setTimezone(new DateTimeZone('UTC'));
429 $format = 'Y-m-d\TH:i:s' . ($millis ? '.v\Z' : '\Z');
430 return $timestamp->format($format);
434 * Write out the common attributes for all data types.
436 * @param DataType $type The data type to write.
437 * @param XMLWriter $xml Where to write.
440 protected function writeDataType(DataType $type, XMLWriter $xml)
442 $var = $type->getName();
444 $xml->writeElement('name', $var);
445 $var = $type->getComment();
447 $xml->writeElement('cmt', $var);
448 $var = $type->getDescription();
450 $xml->writeElement('desc', $var);
451 $var = $type->getSource();
453 $xml->writeElement('src', $var);
454 $var = $type->getLinks(false);
456 foreach($var as $link) {
457 $this->writeLink($link, $xml);
458 if ($this->format == '1.0')
462 if ($this->format == '1.1' || $type instanceof Point) {
463 $var = $type->getType();
465 $xml->writeElement('type', $var);
467 $var = $type->getExtensions(false);
468 if ($var !== null && !$var->isEmpty()) {
469 if ($this->format == '1.1')
470 $xml->startElement('extensions');
471 foreach ($var as $extension) {
472 $xml->writeRaw($extension);
474 if ($this->format == '1.1')
480 * Generate XML for GPX wpt type.
482 * @param Point $point The point to write.
483 * @param string $element The XML element name (e.g. <wpt>)
484 * @param XMLWriter $xml Where to write the point.
487 protected function writePoint(Point $point, string $element, XMLWriter $xml)
489 $xml->startElement($element);
490 $xml->writeAttribute('lat', $point->getLatitude());
491 $xml->writeAttribute('lon', $point->getLongitude());
492 $val = $point->getEle();
494 $xml->writeElement('ele', $val);
495 $val = $point->getTime();
497 $xml->writeElement('time', $this->createTimestamp($val));
498 $val = $point->getMagvar();
500 $xml->writeElement('magvar', $val);
501 $val = $point->getGeoidHeight();
503 $xml->writeElement('geoidheight', $val);
504 $var = $point->getSymbol();
506 $xml->writeElement('sym', $var);
507 $var = $point->getFix();
509 $xml->writeElement('fix', $var);
510 $var = $point->getSatellites();
512 $xml->writeElement('sat', $var);
513 $var = $point->getHdop();
515 $xml->writeElement('hdop', $var);
516 $var = $point->getVdop();
518 $xml->writeElement('vdop', $var);
519 $var = $point->getPdop();
521 $xml->writeElement('pdop', $var);
522 $var = $point->getAgeOfDGPSData();
524 $xml->writeElement('ageofdgpsdata', $var);
525 $var = $point->getDGPSId();
527 $xml->writeElement('dgpsid', $var);
528 $this->writeDataType($point, $xml);
533 * Write XML for a route element.
535 * @param Route $route The route to write.
536 * @param XMLWriter $xml Where to write.
539 protected function writeRoute(Route $route, XMLWriter $xml)
541 $xml->startElement('rte');
542 $var = $route->getNumber();
544 $xml->writeElement('number', $var);
545 $this->writeDataType($route, $xml);
546 $var = $route->getPoints(false);
548 foreach ($var as $point) {
549 $this->writePoint($point, 'rtept', $xml);
556 * Write XML for a track element.
558 * @param Track $track The track to write.
559 * @param XMLWriter $xml Where to write.
562 protected function writeTrack(Track $track, XMLWriter $xml)
564 $xml->startElement('trk');
565 $var = $track->getNumber();
567 $xml->writeElement('number', $var);
568 $this->writeDataType($track, $xml);
569 $var = $track->getSegments(false);
571 foreach ($var as $segment) {
572 $this->writeTrackSegment($segment, $xml);
579 * Write XML for a track segment element.
581 * @param TrackSegment $segment The segment to write.
582 * @param XMLWriter $xml Where to write.
585 protected function writeTrackSegment(TrackSegment $segment, XMLWriter $xml)
587 $xml->startElement('trkseg');
588 $var = $segment->getPoints(false);
590 foreach ($var as $point) {
591 $this->writePoint($point, 'trkpt', $xml);
594 if ($this->format == '1.1') {
595 $var = $segment->getExtensions(false);
597 $xml->startElement('extensions');
598 foreach ($var as $extension) {
599 $xml->writeRaw($extension);