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);
194 * Write XML to a file.
196 * @param GPX $gpx The source for the XML.
197 * @param string $filename Where to write the XML.
199 * @throws RuntimeException If the file cannot be written to.
201 public function save(GPX $gpx, string $filename)
203 $xml = new XMLWriter();
204 if (!$xml->openURI($filename))
205 throw RuntimeException(sprintf('Unable to write to "%s"', $filename));
206 $this->write($gpx, $xml);
213 * @param GPX $gpx The source for the XML.
214 * @param XMLWriter $xml The instance to use to generate the XML.
217 protected function write(GPX $gpx, XMLWriter $xml)
219 $xml->setIndent($this->indent);
220 $xml->startDocument('1.0', 'UTF-8');
221 if ($this->format == '1.1') {
222 $namespace = libgpx::NAMESPACE_GPX_1_1;
223 $schema = libgpx::SCHEMA_GPX_1_1;
225 $namespace = libgpx::NAMESPACE_GPX_1_0;
226 $schema = libgpx::SCHEMA_GPX_1_0;
228 $xml->startElementNs(null, 'gpx', $namespace);
229 $xml->writeAttribute('version', $this->format);
230 $xml->writeAttribute('creator', $this->creator);
231 $xml->writeAttributeNS(
234 libgpx::NAMESPACE_XMLSCHEMA,
235 $namespace . ' ' . $schema
237 $this->writeMetadata($gpx, $xml);
238 $waypoints = $gpx->getWaypoints(false);
239 if ($waypoints !== null) {
240 foreach ($waypoints as $wpt) {
241 $this->writePoint($wpt, 'wpt', $xml);
244 $routes = $gpx->getRoutes(false);
245 if ($routes !== null) {
246 foreach ($routes as $route) {
247 $this->writeRoute($route, $xml);
250 $tracks = $gpx->getTracks(false);
251 if ($tracks !== null) {
252 foreach ($tracks as $track) {
253 $this->writeTrack($track, $xml);
256 $extensions = $gpx->getExtensions(false);
257 if ($extensions !== null && !$extensions->isEmpty()) {
258 if ($this->format == '1.1')
259 $xml->startElement('extensions');
260 foreach ($extensions as $extension) {
261 $xml->writeRaw($extension);
263 if ($this->format == '1.1')
271 * Generate XML for Metadata elements.
273 * @param GPX $gpx The object to source the metadata from.
274 * @param XMLWriter $xml Where to write the metadata.
277 protected function writeMetadata(GPX $gpx, XMLWriter $xml)
279 if ($this->format == '1.1')
280 $xml->startElement('metadata');
281 $name = $gpx->getName();
283 $xml->writeElement('name', $name);
284 $desc = $gpx->getDesc();
286 $xml->writeElement('desc', $desc);
287 $author = $gpx->getAuthor();
288 if ($author instanceof Person)
289 $this->writePerson($author, $xml);
290 if ($this->format == '1.1') {
291 $copyright = $gpx->getCopyright();
292 if ($copyright instanceof Copyright)
293 $this->writeCopyright($copyright, $xml);
295 $links = $gpx->getLinks(false);
296 if ($links !== null) {
297 foreach($links as $link) {
298 $this->writeLink($link, $xml);
299 if ($this->format == '1.0')
303 $xml->writeElement('time', $this->createTimestamp(new DateTime()));
304 $keywords = $gpx->getKeywords(false);
305 if ($keywords !== null && !$keywords->isEmpty())
308 implode(', ', $gpx->getKeywords()->toArray())
310 $bounds = $gpx->getBounds();
311 if ($bounds instanceof Bounds) {
312 $xml->startElement('bounds');
313 $xml->writeAttribute('minlat', $bounds->getMinLat());
314 $xml->writeAttribute('minlon', $bounds->getMinLon());
315 $xml->writeAttribute('maxlat', $bounds->getMaxLat());
316 $xml->writeAttribute('maxlon', $bounds->getMaxLon());
319 if ($this->format == '1.1') {
320 $extensions = $gpx->getMetadataExtensions(false);
321 if ($extensions !== null && !$extensions->isEmpty()) {
322 $xml->startElement('extensions');
323 foreach ($extensions as $extension) {
324 $xml->writeRaw($extension);
328 $xml->endElement(); // </metadata>
333 * Generate XML for GPX Person type.
335 * @param Person $person The person object to source data from.
336 * @param XMLWriter $xml Where to write the data.
339 protected function writePerson(Person $person, XMLWriter $xml)
341 if ($this->format == '1.1') {
342 $xml->startElement('author');
343 $name = $person->getName();
345 $xml->writeElement('name', $name);
346 $email = $person->getEmail();
347 if (!empty($email)) {
348 $email = explode('@', $email, 2);
349 $xml->startElement('email');
350 $xml->writeAttribute('id', $email[0]);
351 $xml->writeAttribute('domain', $email[1]);
354 $link = $person->getLink();
356 $this->writeLink($link, $xml);
359 $name = $person->getName();
361 $xml->writeElement('author', $name);
362 $email = $person->getEmail();
364 $xml->writeElement('email', $email);
369 * Generate XML for a GPX Link type.
371 * @param Link $link The link object to source data from.
372 * @param XMLWriter $xml Where to write the data.
375 protected function writeLink(Link $link, XMLWriter $xml)
377 if ($this->format == '1.1') {
378 $xml->startElement('link');
379 $xml->writeAttribute('href', $link->getHref());
380 $text = $link->getText();
382 $xml->writeElement('text', $text);
383 $type = $link->getType();
385 $xml->writeElement('type', $type);
388 $xml->writeElement('url', $link->getHref());
389 $text = $link->getText();
391 $xml->writeElement('urlname', $text);
396 * Generate XML for GPX Copyright type.
398 * @param Copyright $copyright Object to source copyright data from.
399 * @param XMLWriter $xml Where to write the data.
402 protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
404 $xml->startElement('copyright');
405 $xml->writeAttribute('author', $copyright->getAuthor());
406 $year = $copyright->getYear();
408 $xml->writeElement('year', $year);
409 $license = $copyright->getLicense();
410 if (!empty($license))
411 $xml->writeElement('license', $license);
416 * Create an ISO 8601 timestamp.
418 * @param DateTimeInterface $timestamp The timestamp to format.
419 * @param boolean $millis Include milliseconds.
420 * @return string A timestamp.
422 protected function createTimestamp(
423 DateTimeInterface $timestamp,
426 if ($millis === null)
427 $millis = $this->milliseconds;
428 if (!$timestamp->getTimezone()->getOffset($timestamp))
429 $timestamp = $timestamp->setTimezone(new DateTimeZone('UTC'));
430 $format = 'Y-m-d\TH:i:s' . ($millis ? '.v\Z' : '\Z');
431 return $timestamp->format($format);
435 * Generate XML for GPX wpt type.
437 * @param Point $point The point to write.
438 * @param string $element The XML element name (e.g. <wpt>)
439 * @param XMLWriter $xml Where to write the point.
442 protected function writePoint(Point $point, string $element, XMLWriter $xml)
444 $xml->startElement($element);
445 $xml->writeAttribute('lat', $point->getLatitude());
446 $xml->writeAttribute('lon', $point->getLongitude());
447 $var = $point->getEle();
449 $xml->writeElement('ele', $var);
450 $var = $point->getTime();
452 $xml->writeElement('time', $this->createTimestamp($var));
453 $var = $point->getMagvar();
455 $xml->writeElement('magvar', $var);
456 $var = $point->getGeoidHeight();
458 $xml->writeElement('geoidheight', $var);
459 $var = $point->getName();
461 $xml->writeElement('name', $var);
462 $var = $point->getComment();
464 $xml->writeElement('cmt', $var);
465 $var = $point->getDescription();
467 $xml->writeElement('desc', $var);
468 $var = $point->getSource();
470 $xml->writeElement('src', $var);
471 $var = $point->getLinks(false);
473 foreach($var as $link) {
474 $this->writeLink($link, $xml);
475 if ($this->format == '1.0')
479 $var = $point->getSymbol();
481 $xml->writeElement('sym', $var);
482 $var = $point->getType();
484 $xml->writeElement('type', $var);
485 $var = $point->getFix();
487 $xml->writeElement('fix', $var);
488 $var = $point->getSatellites();
490 $xml->writeElement('sat', $var);
491 $var = $point->getHdop();
493 $xml->writeElement('hdop', $var);
494 $var = $point->getVdop();
496 $xml->writeElement('vdop', $var);
497 $var = $point->getPdop();
499 $xml->writeElement('pdop', $var);
500 $var = $point->getAgeOfDGPSData();
502 $xml->writeElement('ageofdgpsdata', $var);
503 $var = $point->getDGPSId();
505 $xml->writeElement('dgpsid', $var);
506 $var = $point->getExtensions(false);
507 if ($var !== null && !$var->isEmpty()) {
508 if ($this->format == '1.1')
509 $xml->startElement('extensions');
510 foreach ($var as $extension) {
511 $xml->writeRaw($extension);
513 if ($this->format == '1.1')
520 * Write XML for a route element.
522 * @param Route $route The route to write.
523 * @param XMLWriter $xml Where to write.
526 protected function writeRoute(Route $route, XMLWriter $xml)
528 $xml->startElement('rte');
529 $var = $route->getName();
531 $xml->writeElement('name', $var);
532 $var = $route->getComment();
534 $xml->writeElement('cmt', $var);
535 $var = $route->getDescription();
537 $xml->writeElement('desc', $var);
538 $var = $route->getSource();
540 $xml->writeElement('src', $var);
541 $var = $route->getLinks(false);
543 foreach($var as $link) {
544 $this->writeLink($link, $xml);
545 if ($this->format == '1.0')
549 $var = $route->getNumber();
551 $xml->writeElement('number', $var);
552 if ($this->format == '1.1') {
553 $var = $route->getType();
555 $xml->writeElement('type', $var);
557 $var = $route->getExtensions(false);
558 if ($var !== null && !$var->isEmpty()) {
559 if ($this->format == '1.1')
560 $xml->startElement('extensions');
561 foreach ($var as $extension) {
562 $xml->writeRaw($extension);
564 if ($this->format == '1.1')
567 $var = $route->getPoints(false);
569 foreach ($var as $point) {
570 $this->writePoint($point, 'rtept', $xml);
577 * Write XML for a track element.
579 * @param Track $track The track to write.
580 * @param XMLWriter $xml Where to write.
583 protected function writeTrack(Track $track, XMLWriter $xml)
585 $xml->startElement('trk');
586 $var = $track->getName();
588 $xml->writeElement('name', $var);
589 $var = $track->getComment();
591 $xml->writeElement('cmt', $var);
592 $var = $track->getDescription();
594 $xml->writeElement('desc', $var);
595 $var = $track->getSource();
597 $xml->writeElement('src', $var);
598 $var = $track->getLinks(false);
600 foreach($var as $link) {
601 $this->writeLink($link, $xml);
602 if ($this->format == '1.0')
606 $var = $track->getNumber();
608 $xml->writeElement('number', $var);
609 if ($this->format == '1.1') {
610 $var = $track->getType();
612 $xml->writeElement('type', $var);
614 $var = $track->getExtensions(false);
615 if ($var !== null && !$var->isEmpty()) {
616 if ($this->format == '1.1')
617 $xml->startElement('extensions');
618 foreach ($var as $extension) {
619 $xml->writeRaw($extension);
621 if ($this->format == '1.1')
624 $var = $track->getSegments(false);
626 foreach ($var as $segment) {
627 $this->writeTrackSegment($segment, $xml);
634 * Write XML for a track segment element.
636 * @param TrackSegment $segment The segment to write.
637 * @param XMLWriter $xml Where to write.
640 protected function writeTrackSegment(TrackSegment $segment, XMLWriter $xml)
642 $xml->startElement('trkseg');
643 $var = $segment->getPoints(false);
645 foreach ($var as $point) {
646 $this->writePoint($point, 'trkpt', $xml);
649 if ($this->format == '1.1') {
650 $var = $segment->getExtensions(false);
651 if ($var !== null && !$var->isEmpty()) {
652 $xml->startElement('extensions');
653 foreach ($var as $extension) {
654 $xml->writeRaw($extension);