]> git.street.me.uk Git - andy/gpx.git/blame - src/libgpx/gpxwriter.php
Bugfix: Timestamps not converted to UTC when writing GPX
[andy/gpx.git] / src / libgpx / gpxwriter.php
CommitLineData
88564339
AS
1<?php
2/**
3 * gpxwriter.php
4 *
5 * Copyright 2018 Andy Street <andy@street.me.uk>
6 *
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.
11 *
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.
16 *
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,
20 * MA 02110-1301, USA.
21 *
22 *
23 */
24
25namespace libgpx;
26
27use \DateTime;
28use \DateTimeInterface;
29use \DateTimeZone;
30use \RuntimeException;
31use \UnexpectedValueException;
32use \XMLWriter;
33
34/**
35 * Write GPX files.
36 *
37 * @author Andy Street <andy@street.me.uk>
38 */
39class GPXWriter
40{
41
42 /**
43 * Who to credit as the document creator in the GPX root element.
44 *
45 * @var string
46 * @see http://www.topografix.com/GPX/1/1/#type_gpxType
47 */
48 protected $creator = 'libgpx';
49
50 /**
51 * The version of the GPX standard to generate.
52 *
53 * @var string
54 */
55 protected $format = '1.1';
56
57 /**
58 * Whether to indent the XML output for readability.
59 *
60 * @var boolean
61 */
62 protected $indent = true;
63
64 /**
65 * Whether to include milliseconds in timestamps.
66 *
67 * @var boolean
68 */
69 protected $milliseconds = true;
70
71 /**
72 * Fetch the name credited as the creator of the XML files.
73 *
74 * @return string The creator.
75 */
76 public function getCreator()
77 {
78 return $this->creator;
79 }
80
81 /**
82 * Set the name to be credited as the XML document creator.
83 *
84 * @param string $creator The name to credit as the creator.
85 * @return void
86 */
87 public function setCreator(string $creator)
88 {
89 $this->creator = $creator;
90 }
91
92 /**
93 * Fetch the GPX file format that this class generates.
94 *
95 * @return string The GPX file format.
96 */
97 public function getFormat()
98 {
99 return $this->format;
100 }
101
102 /**
103 * Set the GPX file format that this class generates.
104 *
105 * @param string $format Accepted values are "1.0" or "1.1".
106 * @return void
107 * @throws UnexpectedValueException If $format is not a known GPX version.
108 */
109 public function setFormat(string $format)
110 {
111 switch ($format) {
112 case '1.0':
113 case '1.1':
114 $this->format = $format;
115 break;
116 default:
117 throw new UnexpectedValueException(
118 sprintf('Unknown GPX version "%s"', $format)
119 );
120 }
121 }
122
123 /**
124 * Whether this class will indent output for readability.
125 *
126 * @return boolean If the output will be indented.
127 */
128 public function getIndent()
129 {
130 return $this->indent;
131 }
132
133 /**
134 * Set whether to indent output for readability.
135 *
136 * @param boolean $indent Whether to indent.
137 * @return void
138 */
139 public function setIndent(bool $indent)
140 {
141 $this->indent = $indent;
142 }
143
144 /**
145 * Whether timestamps are written to include milliseconds.
146 *
147 * @return boolean
148 */
149 public function getTimestampUseMilliseconds()
150 {
151 return $this->milliseconds;
152 }
153
154 /**
155 * Set whether to output milliseconds in timestamps.
156 *
157 * @param boolean $milliseconds Whether to use milliseconds.
158 * @return void
159 */
160 public function setTimestampUseMilliseconds(bool $milliseconds)
161 {
162 $this->milliseconds = $milliseconds;
163 }
164
165 /**
166 * Fetch an XML representation as a string.
167 *
168 * @param GPX $gpx The source for the XML.
169 * @return string An XML representation of the GPX object.
170 */
171 public function fetch(GPX $gpx)
172 {
173 $xml = new XMLWriter();
174 $xml->openMemory();
175 $this->write($gpx, $xml);
176 return $xml->outputMemory();
177 }
178
179 /**
180 * Write XML to standard out.
181 *
182 * @param GPX $gpx The source for the XML.
183 * @return void
184 */
185 public function output(GPX $gpx)
186 {
187 $xml = new XMLWriter();
188 $xml->openURI('php://output');
189 $this->write($gpx, $xml);
43086e87 190 $xml->flush();
88564339
AS
191 }
192
193 /**
194 * Write XML to a file.
195 *
196 * @param GPX $gpx The source for the XML.
197 * @param string $filename Where to write the XML.
198 * @return void
199 * @throws RuntimeException If the file cannot be written to.
200 */
201 public function save(GPX $gpx, string $filename)
202 {
203 $xml = new XMLWriter();
43086e87 204 if (!$xml->openURI($filename))
88564339
AS
205 throw RuntimeException(sprintf('Unable to write to "%s"', $filename));
206 $this->write($gpx, $xml);
43086e87 207 $xml->flush();
88564339
AS
208 }
209
210 /**
211 * Generate the XML.
212 *
213 * @param GPX $gpx The source for the XML.
214 * @param XMLWriter $xml The instance to use to generate the XML.
215 * @return void
216 */
217 protected function write(GPX $gpx, XMLWriter $xml)
218 {
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;
224 } else {
225 $namespace = libgpx::NAMESPACE_GPX_1_0;
226 $schema = libgpx::SCHEMA_GPX_1_0;
227 }
228 $xml->startElementNs(null, 'gpx', $namespace);
229 $xml->writeAttribute('version', $this->format);
230 $xml->writeAttribute('creator', $this->creator);
231 $xml->writeAttributeNS(
232 'xsi',
233 'schemaLocation',
234 libgpx::NAMESPACE_XMLSCHEMA,
235 $namespace . ' ' . $schema
236 );
237 $this->writeMetadata($gpx, $xml);
57c75891
AS
238 $waypoints = $gpx->getWaypoints(false);
239 if ($waypoints !== null) {
240 foreach ($waypoints as $wpt) {
241 $this->writePoint($wpt, 'wpt', $xml);
242 }
f528d248 243 }
f17d2566
AS
244 $routes = $gpx->getRoutes(false);
245 if ($routes !== null) {
246 foreach ($routes as $route) {
247 $this->writeRoute($route, $xml);
248 }
249 }
35c309cc
AS
250 $tracks = $gpx->getTracks(false);
251 if ($tracks !== null) {
252 foreach ($tracks as $track) {
253 $this->writeTrack($track, $xml);
254 }
255 }
e0c26559
AS
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);
262 }
263 if ($this->format == '1.1')
264 $xml->endElement();
265 }
88564339
AS
266 $xml->endElement();
267 $xml->endDocument();
88564339
AS
268 }
269
270 /**
271 * Generate XML for Metadata elements.
272 *
273 * @param GPX $gpx The object to source the metadata from.
274 * @param XMLWriter $xml Where to write the metadata.
275 * @return void
276 */
277 protected function writeMetadata(GPX $gpx, XMLWriter $xml)
278 {
279 if ($this->format == '1.1')
280 $xml->startElement('metadata');
281 $name = $gpx->getName();
282 if (!empty($name))
283 $xml->writeElement('name', $name);
284 $desc = $gpx->getDesc();
285 if (!empty($desc))
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);
294 }
57c75891
AS
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')
300 break;
301 }
88564339
AS
302 }
303 $xml->writeElement('time', $this->createTimestamp(new DateTime()));
57c75891
AS
304 $keywords = $gpx->getKeywords(false);
305 if ($keywords !== null && !$keywords->isEmpty())
7c465a82
AS
306 $xml->writeElement(
307 'keywords',
308 implode(', ', $gpx->getKeywords()->toArray())
309 );
88564339
AS
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());
317 $xml->endElement();
318 }
7c465a82 319 if ($this->format == '1.1') {
57c75891
AS
320 $extensions = $gpx->getMetadataExtensions(false);
321 if ($extensions !== null && !$extensions->isEmpty()) {
7c465a82
AS
322 $xml->startElement('extensions');
323 foreach ($extensions as $extension) {
324 $xml->writeRaw($extension);
325 }
326 $xml->endElement();
327 }
328 $xml->endElement(); // </metadata>
329 }
88564339
AS
330 }
331
332 /**
333 * Generate XML for GPX Person type.
334 *
335 * @param Person $person The person object to source data from.
336 * @param XMLWriter $xml Where to write the data.
337 * @return void
338 */
339 protected function writePerson(Person $person, XMLWriter $xml)
340 {
341 if ($this->format == '1.1') {
342 $xml->startElement('author');
343 $name = $person->getName();
344 if (!empty($name))
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]);
352 $xml->endElement();
353 }
354 $link = $person->getLink();
355 if (!empty($link))
356 $this->writeLink($link, $xml);
357 $xml->endElement();
358 } else {
359 $name = $person->getName();
360 if (!empty($name))
361 $xml->writeElement('author', $name);
362 $email = $person->getEmail();
363 if (!empty($email))
364 $xml->writeElement('email', $email);
365 }
366 }
367
368 /**
369 * Generate XML for a GPX Link type.
370 *
371 * @param Link $link The link object to source data from.
372 * @param XMLWriter $xml Where to write the data.
373 * @return void
374 */
375 protected function writeLink(Link $link, XMLWriter $xml)
376 {
377 if ($this->format == '1.1') {
378 $xml->startElement('link');
379 $xml->writeAttribute('href', $link->getHref());
380 $text = $link->getText();
381 if (!empty($text))
382 $xml->writeElement('text', $text);
383 $type = $link->getType();
384 if (!empty($type))
385 $xml->writeElement('type', $type);
386 $xml->endElement();
387 } else {
388 $xml->writeElement('url', $link->getHref());
389 $text = $link->getText();
390 if (!empty($text))
391 $xml->writeElement('urlname', $text);
392 }
393 }
394
395 /**
396 * Generate XML for GPX Copyright type.
397 *
398 * @param Copyright $copyright Object to source copyright data from.
399 * @param XMLWriter $xml Where to write the data.
400 * @return void
401 */
402 protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
403 {
404 $xml->startElement('copyright');
405 $xml->writeAttribute('author', $copyright->getAuthor());
406 $year = $copyright->getYear();
407 if (!empty($year))
408 $xml->writeElement('year', $year);
409 $license = $copyright->getLicense();
410 if (!empty($license))
411 $xml->writeElement('license', $license);
412 $xml->endElement();
413 }
414
415 /**
416 * Create an ISO 8601 timestamp.
417 *
418 * @param DateTimeInterface $timestamp The timestamp to format.
419 * @param boolean $millis Include milliseconds.
420 * @return string A timestamp.
421 */
422 protected function createTimestamp(
423 DateTimeInterface $timestamp,
424 bool $millis = null
425 ) {
426 if ($millis === null)
427 $millis = $this->milliseconds;
a5179f5d 428 if ($timestamp->getTimezone()->getOffset($timestamp) != 0)
88564339
AS
429 $timestamp = $timestamp->setTimezone(new DateTimeZone('UTC'));
430 $format = 'Y-m-d\TH:i:s' . ($millis ? '.v\Z' : '\Z');
431 return $timestamp->format($format);
432 }
433
09a36623 434 /**
43086e87 435 * Generate XML for GPX wpt type.
09a36623 436 *
43086e87
AS
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.
09a36623
AS
440 * @return void
441 */
43086e87 442 protected function writePoint(Point $point, string $element, XMLWriter $xml)
09a36623 443 {
43086e87
AS
444 $xml->startElement($element);
445 $xml->writeAttribute('lat', $point->getLatitude());
446 $xml->writeAttribute('lon', $point->getLongitude());
447 $var = $point->getEle();
448 if ($var !== null)
449 $xml->writeElement('ele', $var);
450 $var = $point->getTime();
451 if ($var !== null)
452 $xml->writeElement('time', $this->createTimestamp($var));
453 $var = $point->getMagvar();
454 if ($var !== null)
455 $xml->writeElement('magvar', $var);
456 $var = $point->getGeoidHeight();
457 if ($var !== null)
458 $xml->writeElement('geoidheight', $var);
459 $var = $point->getName();
09a36623
AS
460 if ($var !== null)
461 $xml->writeElement('name', $var);
43086e87 462 $var = $point->getComment();
09a36623
AS
463 if ($var !== null)
464 $xml->writeElement('cmt', $var);
43086e87 465 $var = $point->getDescription();
09a36623
AS
466 if ($var !== null)
467 $xml->writeElement('desc', $var);
43086e87 468 $var = $point->getSource();
09a36623
AS
469 if ($var !== null)
470 $xml->writeElement('src', $var);
43086e87 471 $var = $point->getLinks(false);
09a36623
AS
472 if ($var !== null) {
473 foreach($var as $link) {
474 $this->writeLink($link, $xml);
475 if ($this->format == '1.0')
476 break;
477 }
478 }
f528d248
AS
479 $var = $point->getSymbol();
480 if ($var !== null)
481 $xml->writeElement('sym', $var);
43086e87
AS
482 $var = $point->getType();
483 if ($var !== null)
484 $xml->writeElement('type', $var);
f528d248
AS
485 $var = $point->getFix();
486 if ($var !== null)
487 $xml->writeElement('fix', $var);
488 $var = $point->getSatellites();
489 if ($var !== null)
490 $xml->writeElement('sat', $var);
491 $var = $point->getHdop();
492 if ($var !== null)
493 $xml->writeElement('hdop', $var);
494 $var = $point->getVdop();
495 if ($var !== null)
496 $xml->writeElement('vdop', $var);
497 $var = $point->getPdop();
498 if ($var !== null)
499 $xml->writeElement('pdop', $var);
500 $var = $point->getAgeOfDGPSData();
501 if ($var !== null)
502 $xml->writeElement('ageofdgpsdata', $var);
503 $var = $point->getDGPSId();
504 if ($var !== null)
505 $xml->writeElement('dgpsid', $var);
43086e87
AS
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);
512 }
513 if ($this->format == '1.1')
514 $xml->endElement();
515 }
f528d248
AS
516 $xml->endElement();
517 }
518
f17d2566
AS
519 /**
520 * Write XML for a route element.
521 *
522 * @param Route $route The route to write.
523 * @param XMLWriter $xml Where to write.
35c309cc 524 * @return void
f17d2566
AS
525 */
526 protected function writeRoute(Route $route, XMLWriter $xml)
527 {
528 $xml->startElement('rte');
43086e87
AS
529 $var = $route->getName();
530 if ($var !== null)
531 $xml->writeElement('name', $var);
532 $var = $route->getComment();
533 if ($var !== null)
534 $xml->writeElement('cmt', $var);
535 $var = $route->getDescription();
536 if ($var !== null)
537 $xml->writeElement('desc', $var);
538 $var = $route->getSource();
539 if ($var !== null)
540 $xml->writeElement('src', $var);
541 $var = $route->getLinks(false);
542 if ($var !== null) {
543 foreach($var as $link) {
544 $this->writeLink($link, $xml);
545 if ($this->format == '1.0')
546 break;
547 }
548 }
f17d2566
AS
549 $var = $route->getNumber();
550 if ($var !== null)
551 $xml->writeElement('number', $var);
43086e87
AS
552 if ($this->format == '1.1') {
553 $var = $route->getType();
554 if ($var !== null)
555 $xml->writeElement('type', $var);
556 }
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);
563 }
564 if ($this->format == '1.1')
565 $xml->endElement();
566 }
f17d2566
AS
567 $var = $route->getPoints(false);
568 if ($var !== null) {
569 foreach ($var as $point) {
570 $this->writePoint($point, 'rtept', $xml);
571 }
572 }
573 $xml->endElement();
574 }
575
35c309cc
AS
576 /**
577 * Write XML for a track element.
578 *
579 * @param Track $track The track to write.
580 * @param XMLWriter $xml Where to write.
581 * @return void
582 */
583 protected function writeTrack(Track $track, XMLWriter $xml)
584 {
585 $xml->startElement('trk');
43086e87
AS
586 $var = $track->getName();
587 if ($var !== null)
588 $xml->writeElement('name', $var);
589 $var = $track->getComment();
590 if ($var !== null)
591 $xml->writeElement('cmt', $var);
592 $var = $track->getDescription();
593 if ($var !== null)
594 $xml->writeElement('desc', $var);
595 $var = $track->getSource();
596 if ($var !== null)
597 $xml->writeElement('src', $var);
598 $var = $track->getLinks(false);
599 if ($var !== null) {
600 foreach($var as $link) {
601 $this->writeLink($link, $xml);
602 if ($this->format == '1.0')
603 break;
604 }
605 }
35c309cc
AS
606 $var = $track->getNumber();
607 if ($var !== null)
608 $xml->writeElement('number', $var);
43086e87
AS
609 if ($this->format == '1.1') {
610 $var = $track->getType();
611 if ($var !== null)
612 $xml->writeElement('type', $var);
613 }
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);
620 }
621 if ($this->format == '1.1')
622 $xml->endElement();
623 }
35c309cc
AS
624 $var = $track->getSegments(false);
625 if ($var !== null) {
626 foreach ($var as $segment) {
627 $this->writeTrackSegment($segment, $xml);
628 }
629 }
630 $xml->endElement();
631 }
632
633 /**
634 * Write XML for a track segment element.
635 *
636 * @param TrackSegment $segment The segment to write.
637 * @param XMLWriter $xml Where to write.
638 * @return void
639 */
640 protected function writeTrackSegment(TrackSegment $segment, XMLWriter $xml)
641 {
642 $xml->startElement('trkseg');
643 $var = $segment->getPoints(false);
644 if ($var !== null) {
645 foreach ($var as $point) {
646 $this->writePoint($point, 'trkpt', $xml);
647 }
648 }
649 if ($this->format == '1.1') {
650 $var = $segment->getExtensions(false);
43086e87 651 if ($var !== null && !$var->isEmpty()) {
35c309cc
AS
652 $xml->startElement('extensions');
653 foreach ($var as $extension) {
654 $xml->writeRaw($extension);
655 }
656 $xml->endElement();
657 }
658 }
659 $xml->endElement();
660 }
661
88564339 662}