]> git.street.me.uk Git - andy/gpx.git/blob - src/libgpx/gpxwriter.php
Fix: PeakStream fails to function correctly after the first file
[andy/gpx.git] / src / libgpx / gpxwriter.php
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
25 namespace libgpx;
26
27 use \DateTime;
28 use \DateTimeInterface;
29 use \DateTimeZone;
30 use \RuntimeException;
31 use \UnexpectedValueException;
32 use \XMLWriter;
33
34 /**
35  * Write GPX files.
36  *
37  * @author Andy Street <andy@street.me.uk>
38  */
39 class 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);
190     $xml->flush();
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();
204     if (!$xml->openURI($filename))
205       throw RuntimeException(sprintf('Unable to write to "%s"', $filename));
206     $this->write($gpx, $xml);
207     $xml->flush();
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);
238     $waypoints = $gpx->getWaypoints(false);
239     if ($waypoints !== null) {
240       foreach ($waypoints as $wpt) {
241         $this->writePoint($wpt, 'wpt', $xml);
242       }
243     }
244     $routes = $gpx->getRoutes(false);
245     if ($routes !== null) {
246       foreach ($routes as $route) {
247         $this->writeRoute($route, $xml);
248       }
249     }
250     $tracks = $gpx->getTracks(false);
251     if ($tracks !== null) {
252       foreach ($tracks as $track) {
253         $this->writeTrack($track, $xml);
254       }
255     }
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     }
266     $xml->endElement();
267     $xml->endDocument();
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     }
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       }
302     }
303     $xml->writeElement('time', $this->createTimestamp(new DateTime()));
304     $keywords = $gpx->getKeywords(false);
305     if ($keywords !== null && !$keywords->isEmpty())
306       $xml->writeElement(
307         'keywords',
308         implode(', ', $gpx->getKeywords()->toArray())
309       );
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     }
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);
325         }
326         $xml->endElement();
327       }
328       $xml->endElement(); // </metadata>
329     }
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;
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);
432   }
433
434   /**
435    * Generate XML for GPX wpt type.
436    *
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.
440    * @return void
441    */
442   protected function writePoint(Point $point, string $element, XMLWriter $xml)
443   {
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();
460     if ($var !== null)
461       $xml->writeElement('name', $var);
462     $var = $point->getComment();
463     if ($var !== null)
464       $xml->writeElement('cmt', $var);
465     $var = $point->getDescription();
466     if ($var !== null)
467       $xml->writeElement('desc', $var);
468     $var = $point->getSource();
469     if ($var !== null)
470       $xml->writeElement('src', $var);
471     $var = $point->getLinks(false);
472     if ($var !== null) {
473       foreach($var as $link) {
474         $this->writeLink($link, $xml);
475         if ($this->format == '1.0')
476           break;
477       }
478     }
479     $var = $point->getSymbol();
480     if ($var !== null)
481       $xml->writeElement('sym', $var);
482     $var = $point->getType();
483     if ($var !== null)
484       $xml->writeElement('type', $var);
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);
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     }
516     $xml->endElement();
517   }
518
519   /**
520    * Write XML for a route element.
521    *
522    * @param Route $route The route to write.
523    * @param XMLWriter $xml Where to write.
524    * @return void
525    */
526   protected function writeRoute(Route $route, XMLWriter $xml)
527   {
528     $xml->startElement('rte');
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     }
549     $var = $route->getNumber();
550     if ($var !== null)
551       $xml->writeElement('number', $var);
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     }
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
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');
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     }
606     $var = $track->getNumber();
607     if ($var !== null)
608       $xml->writeElement('number', $var);
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     }
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);
651       if ($var !== null && !$var->isEmpty()) {
652         $xml->startElement('extensions');
653         foreach ($var as $extension) {
654           $xml->writeRaw($extension);
655         }
656         $xml->endElement();
657       }
658     }
659     $xml->endElement();
660   }
661
662 }