]> git.street.me.uk Git - andy/gpx.git/blob - src/libgpx/gpxwriter.php
Add extensions for GPX type
[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   }
191
192   /**
193    * Write XML to a file.
194    *
195    * @param GPX $gpx The source for the XML.
196    * @param string $filename Where to write the XML.
197    * @return void
198    * @throws RuntimeException If the file cannot be written to.
199    */
200   public function save(GPX $gpx, string $filename)
201   {
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);
206   }
207
208   /**
209    * Generate the XML.
210    *
211    * @param GPX $gpx The source for the XML.
212    * @param XMLWriter $xml The instance to use to generate the XML.
213    * @return void
214    */
215   protected function write(GPX $gpx, XMLWriter $xml)
216   {
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;
222     } else {
223       $namespace = libgpx::NAMESPACE_GPX_1_0;
224       $schema = libgpx::SCHEMA_GPX_1_0;
225     }
226     $xml->startElementNs(null, 'gpx', $namespace);
227     $xml->writeAttribute('version', $this->format);
228     $xml->writeAttribute('creator', $this->creator);
229     $xml->writeAttributeNS(
230       'xsi',
231       'schemaLocation',
232       libgpx::NAMESPACE_XMLSCHEMA,
233       $namespace . ' ' . $schema
234     );
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);
240       }
241     }
242     $routes = $gpx->getRoutes(false);
243     if ($routes !== null) {
244       foreach ($routes as $route) {
245         $this->writeRoute($route, $xml);
246       }
247     }
248     $tracks = $gpx->getTracks(false);
249     if ($tracks !== null) {
250       foreach ($tracks as $track) {
251         $this->writeTrack($track, $xml);
252       }
253     }
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);
260       }
261       if ($this->format == '1.1')
262         $xml->endElement();
263     }
264     $xml->endElement();
265     $xml->endDocument();
266     $xml->flush();
267   }
268
269   /**
270    * Generate XML for Metadata elements.
271    *
272    * @param GPX $gpx The object to source the metadata from.
273    * @param XMLWriter $xml Where to write the metadata.
274    * @return void
275    */
276   protected function writeMetadata(GPX $gpx, XMLWriter $xml)
277   {
278     if ($this->format == '1.1')
279       $xml->startElement('metadata');
280     $name = $gpx->getName();
281     if (!empty($name))
282       $xml->writeElement('name', $name);
283     $desc = $gpx->getDesc();
284     if (!empty($desc))
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);
293     }
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')
299           break;
300       }
301     }
302     $xml->writeElement('time', $this->createTimestamp(new DateTime()));
303     $keywords = $gpx->getKeywords(false);
304     if ($keywords !== null && !$keywords->isEmpty())
305       $xml->writeElement(
306         'keywords',
307         implode(', ', $gpx->getKeywords()->toArray())
308       );
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());
316       $xml->endElement();
317     }
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);
324         }
325         $xml->endElement();
326       }
327       $xml->endElement(); // </metadata>
328     }
329   }
330
331   /**
332    * Generate XML for GPX Person type.
333    *
334    * @param Person $person The person object to source data from.
335    * @param XMLWriter $xml Where to write the data.
336    * @return void
337    */
338   protected function writePerson(Person $person, XMLWriter $xml)
339   {
340     if ($this->format == '1.1') {
341       $xml->startElement('author');
342       $name = $person->getName();
343       if (!empty($name))
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]);
351         $xml->endElement();
352       }
353       $link = $person->getLink();
354       if (!empty($link))
355         $this->writeLink($link, $xml);
356       $xml->endElement();
357     } else {
358       $name = $person->getName();
359       if (!empty($name))
360         $xml->writeElement('author', $name);
361       $email = $person->getEmail();
362       if (!empty($email))
363         $xml->writeElement('email', $email);
364     }
365   }
366
367   /**
368    * Generate XML for a GPX Link type.
369    *
370    * @param Link $link The link object to source data from.
371    * @param XMLWriter $xml Where to write the data.
372    * @return void
373    */
374   protected function writeLink(Link $link, XMLWriter $xml)
375   {
376     if ($this->format == '1.1') {
377       $xml->startElement('link');
378       $xml->writeAttribute('href', $link->getHref());
379       $text = $link->getText();
380       if (!empty($text))
381         $xml->writeElement('text', $text);
382       $type = $link->getType();
383       if (!empty($type))
384         $xml->writeElement('type', $type);
385       $xml->endElement();
386     } else {
387       $xml->writeElement('url', $link->getHref());
388       $text = $link->getText();
389       if (!empty($text))
390         $xml->writeElement('urlname', $text);
391     }
392   }
393
394   /**
395    * Generate XML for GPX Copyright type.
396    *
397    * @param Copyright $copyright Object to source copyright data from.
398    * @param XMLWriter $xml Where to write the data.
399    * @return void
400    */
401   protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
402   {
403     $xml->startElement('copyright');
404     $xml->writeAttribute('author', $copyright->getAuthor());
405     $year = $copyright->getYear();
406     if (!empty($year))
407       $xml->writeElement('year', $year);
408     $license = $copyright->getLicense();
409     if (!empty($license))
410       $xml->writeElement('license', $license);
411     $xml->endElement();
412   }
413
414   /**
415    * Create an ISO 8601 timestamp.
416    *
417    * @param DateTimeInterface $timestamp The timestamp to format.
418    * @param boolean $millis Include milliseconds.
419    * @return string A timestamp.
420    */
421   protected function createTimestamp(
422     DateTimeInterface $timestamp,
423     bool $millis = null
424   ) {
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);
431   }
432
433   /**
434    * Write out the common attributes for all data types.
435    *
436    * @param DataType $type The data type to write.
437    * @param XMLWriter $xml Where to write.
438    * @return void
439    */
440   protected function writeDataType(DataType $type, XMLWriter $xml)
441   {
442     $var = $type->getName();
443     if ($var !== null)
444       $xml->writeElement('name', $var);
445     $var = $type->getComment();
446     if ($var !== null)
447       $xml->writeElement('cmt', $var);
448     $var = $type->getDescription();
449     if ($var !== null)
450       $xml->writeElement('desc', $var);
451     $var = $type->getSource();
452     if ($var !== null)
453       $xml->writeElement('src', $var);
454     $var = $type->getLinks(false);
455     if ($var !== null) {
456       foreach($var as $link) {
457         $this->writeLink($link, $xml);
458         if ($this->format == '1.0')
459           break;
460       }
461     }
462     if ($this->format == '1.1' || $type instanceof Point) {
463       $var = $type->getType();
464       if ($var !== null)
465         $xml->writeElement('type', $var);
466     }
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);
473       }
474       if ($this->format == '1.1')
475         $xml->endElement();
476     }
477   }
478
479   /**
480    * Generate XML for GPX wpt type.
481    *
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.
485    * @return void
486    */
487   protected function writePoint(Point $point, string $element, XMLWriter $xml)
488   {
489     $xml->startElement($element);
490     $xml->writeAttribute('lat', $point->getLatitude());
491     $xml->writeAttribute('lon', $point->getLongitude());
492     $val = $point->getEle();
493     if ($val !== null)
494       $xml->writeElement('ele', $val);
495     $val = $point->getTime();
496     if ($val !== null)
497       $xml->writeElement('time', $this->createTimestamp($val));
498     $val = $point->getMagvar();
499     if ($val !== null)
500       $xml->writeElement('magvar', $val);
501     $val = $point->getGeoidHeight();
502     if ($val !== null)
503       $xml->writeElement('geoidheight', $val);
504     $var = $point->getSymbol();
505     if ($var !== null)
506       $xml->writeElement('sym', $var);
507     $var = $point->getFix();
508     if ($var !== null)
509       $xml->writeElement('fix', $var);
510     $var = $point->getSatellites();
511     if ($var !== null)
512       $xml->writeElement('sat', $var);
513     $var = $point->getHdop();
514     if ($var !== null)
515       $xml->writeElement('hdop', $var);
516     $var = $point->getVdop();
517     if ($var !== null)
518       $xml->writeElement('vdop', $var);
519     $var = $point->getPdop();
520     if ($var !== null)
521       $xml->writeElement('pdop', $var);
522     $var = $point->getAgeOfDGPSData();
523     if ($var !== null)
524       $xml->writeElement('ageofdgpsdata', $var);
525     $var = $point->getDGPSId();
526     if ($var !== null)
527       $xml->writeElement('dgpsid', $var);
528     $this->writeDataType($point, $xml);
529     $xml->endElement();
530   }
531
532   /**
533    * Write XML for a route element.
534    *
535    * @param Route $route The route to write.
536    * @param XMLWriter $xml Where to write.
537    * @return void
538    */
539   protected function writeRoute(Route $route, XMLWriter $xml)
540   {
541     $xml->startElement('rte');
542     $var = $route->getNumber();
543     if ($var !== null)
544       $xml->writeElement('number', $var);
545     $this->writeDataType($route, $xml);
546     $var = $route->getPoints(false);
547     if ($var !== null) {
548       foreach ($var as $point) {
549         $this->writePoint($point, 'rtept', $xml);
550       }
551     }
552     $xml->endElement();
553   }
554
555   /**
556    * Write XML for a track element.
557    *
558    * @param Track $track The track to write.
559    * @param XMLWriter $xml Where to write.
560    * @return void
561    */
562   protected function writeTrack(Track $track, XMLWriter $xml)
563   {
564     $xml->startElement('trk');
565     $var = $track->getNumber();
566     if ($var !== null)
567       $xml->writeElement('number', $var);
568     $this->writeDataType($track, $xml);
569     $var = $track->getSegments(false);
570     if ($var !== null) {
571       foreach ($var as $segment) {
572         $this->writeTrackSegment($segment, $xml);
573       }
574     }
575     $xml->endElement();
576   }
577
578   /**
579    * Write XML for a track segment element.
580    *
581    * @param TrackSegment $segment The segment to write.
582    * @param XMLWriter $xml Where to write.
583    * @return void
584    */
585   protected function writeTrackSegment(TrackSegment $segment, XMLWriter $xml)
586   {
587     $xml->startElement('trkseg');
588     $var = $segment->getPoints(false);
589     if ($var !== null) {
590       foreach ($var as $point) {
591         $this->writePoint($point, 'trkpt', $xml);
592       }
593     }
594     if ($this->format == '1.1') {
595       $var = $segment->getExtensions(false);
596       if ($var !== null) {
597         $xml->startElement('extensions');
598         foreach ($var as $extension) {
599           $xml->writeRaw($extension);
600         }
601         $xml->endElement();
602       }
603     }
604     $xml->endElement();
605   }
606
607 }