]> git.street.me.uk Git - andy/gpx.git/blob - src/libgpx/gpxwriter.php
bb0af30d71d008be3353ae4a2c4e5ebf1ba38856
[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     $xml->endElement();
249     $xml->endDocument();
250     $xml->flush();
251   }
252
253   /**
254    * Generate XML for Metadata elements.
255    *
256    * @param GPX $gpx The object to source the metadata from.
257    * @param XMLWriter $xml Where to write the metadata.
258    * @return void
259    */
260   protected function writeMetadata(GPX $gpx, XMLWriter $xml)
261   {
262     if ($this->format == '1.1')
263       $xml->startElement('metadata');
264     $name = $gpx->getName();
265     if (!empty($name))
266       $xml->writeElement('name', $name);
267     $desc = $gpx->getDesc();
268     if (!empty($desc))
269       $xml->writeElement('desc', $desc);
270     $author = $gpx->getAuthor();
271     if ($author instanceof Person)
272       $this->writePerson($author, $xml);
273     if ($this->format == '1.1') {
274       $copyright = $gpx->getCopyright();
275       if ($copyright instanceof Copyright)
276         $this->writeCopyright($copyright, $xml);
277     }
278     $links = $gpx->getLinks(false);
279     if ($links !== null) {
280       foreach($links as $link) {
281         $this->writeLink($link, $xml);
282         if ($this->format == '1.0')
283           break;
284       }
285     }
286     $xml->writeElement('time', $this->createTimestamp(new DateTime()));
287     $keywords = $gpx->getKeywords(false);
288     if ($keywords !== null && !$keywords->isEmpty())
289       $xml->writeElement(
290         'keywords',
291         implode(', ', $gpx->getKeywords()->toArray())
292       );
293     $bounds = $gpx->getBounds();
294     if ($bounds instanceof Bounds) {
295       $xml->startElement('bounds');
296       $xml->writeAttribute('minlat', $bounds->getMinLat());
297       $xml->writeAttribute('minlon', $bounds->getMinLon());
298       $xml->writeAttribute('maxlat', $bounds->getMaxLat());
299       $xml->writeAttribute('maxlon', $bounds->getMaxLon());
300       $xml->endElement();
301     }
302     if ($this->format == '1.1') {
303       $extensions = $gpx->getMetadataExtensions(false);
304       if ($extensions !== null && !$extensions->isEmpty()) {
305         $xml->startElement('extensions');
306         foreach ($extensions as $extension) {
307           $xml->writeRaw($extension);
308         }
309         $xml->endElement();
310       }
311       $xml->endElement(); // </metadata>
312     }
313   }
314
315   /**
316    * Generate XML for GPX Person type.
317    *
318    * @param Person $person The person object to source data from.
319    * @param XMLWriter $xml Where to write the data.
320    * @return void
321    */
322   protected function writePerson(Person $person, XMLWriter $xml)
323   {
324     if ($this->format == '1.1') {
325       $xml->startElement('author');
326       $name = $person->getName();
327       if (!empty($name))
328         $xml->writeElement('name', $name);
329       $email = $person->getEmail();
330       if (!empty($email)) {
331         $email = explode('@', $email, 2);
332         $xml->startElement('email');
333         $xml->writeAttribute('id', $email[0]);
334         $xml->writeAttribute('domain', $email[1]);
335         $xml->endElement();
336       }
337       $link = $person->getLink();
338       if (!empty($link))
339         $this->writeLink($link, $xml);
340       $xml->endElement();
341     } else {
342       $name = $person->getName();
343       if (!empty($name))
344         $xml->writeElement('author', $name);
345       $email = $person->getEmail();
346       if (!empty($email))
347         $xml->writeElement('email', $email);
348     }
349   }
350
351   /**
352    * Generate XML for a GPX Link type.
353    *
354    * @param Link $link The link object to source data from.
355    * @param XMLWriter $xml Where to write the data.
356    * @return void
357    */
358   protected function writeLink(Link $link, XMLWriter $xml)
359   {
360     if ($this->format == '1.1') {
361       $xml->startElement('link');
362       $xml->writeAttribute('href', $link->getHref());
363       $text = $link->getText();
364       if (!empty($text))
365         $xml->writeElement('text', $text);
366       $type = $link->getType();
367       if (!empty($type))
368         $xml->writeElement('type', $type);
369       $xml->endElement();
370     } else {
371       $xml->writeElement('url', $link->getHref());
372       $text = $link->getText();
373       if (!empty($text))
374         $xml->writeElement('urlname', $text);
375     }
376   }
377
378   /**
379    * Generate XML for GPX Copyright type.
380    *
381    * @param Copyright $copyright Object to source copyright data from.
382    * @param XMLWriter $xml Where to write the data.
383    * @return void
384    */
385   protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
386   {
387     $xml->startElement('copyright');
388     $xml->writeAttribute('author', $copyright->getAuthor());
389     $year = $copyright->getYear();
390     if (!empty($year))
391       $xml->writeElement('year', $year);
392     $license = $copyright->getLicense();
393     if (!empty($license))
394       $xml->writeElement('license', $license);
395     $xml->endElement();
396   }
397
398   /**
399    * Create an ISO 8601 timestamp.
400    *
401    * @param DateTimeInterface $timestamp The timestamp to format.
402    * @param boolean $millis Include milliseconds.
403    * @return string A timestamp.
404    */
405   protected function createTimestamp(
406     DateTimeInterface $timestamp,
407     bool $millis = null
408   ) {
409     if ($millis === null)
410       $millis = $this->milliseconds;
411     if (!$timestamp->getTimezone()->getOffset($timestamp))
412       $timestamp = $timestamp->setTimezone(new DateTimeZone('UTC'));
413     $format = 'Y-m-d\TH:i:s' . ($millis ? '.v\Z' : '\Z');
414     return $timestamp->format($format);
415   }
416
417   /**
418    * Generate XML for GPX wpt type.
419    *
420    * @param Point $point The point to write.
421    * @param string $element The XML element name (e.g. <wpt>)
422    * @param XMLWriter $xml Where to write the point.
423    * @return void
424    */
425   protected function writePoint(Point $point, string $element, XMLWriter $xml)
426   {
427     $xml->startElement($element);
428     $xml->writeAttribute('lat', $point->getLatitude());
429     $xml->writeAttribute('lon', $point->getLongitude());
430     $val = $point->getEle();
431     if ($val !== null)
432       $xml->writeElement('ele', $val);
433     $val = $point->getTime();
434     if ($val !== null)
435       $xml->writeElement('time', $this->createTimestamp($val));
436     $val = $point->getMagvar();
437     if ($val !== null)
438       $xml->writeElement('magvar', $val);
439     $val = $point->getGeoidHeight();
440     if ($val !== null)
441       $xml->writeElement('geoidheight', $val);
442     $var = $point->getName();
443     if ($var !== null)
444       $xml->writeElement('name', $var);
445     $var = $point->getComment();
446     if ($var !== null)
447       $xml->writeElement('cmt', $var);
448     $var = $point->getDescription();
449     if ($var !== null)
450       $xml->writeElement('desc', $var);
451     $var = $point->getSource();
452     if ($var !== null)
453       $xml->writeElement('src', $var);
454     $var = $point->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     $var = $point->getSymbol();
463     if ($var !== null)
464       $xml->writeElement('sym', $var);
465     $var = $point->getType();
466     if ($var !== null)
467       $xml->writeElement('type', $var);
468     $var = $point->getFix();
469     if ($var !== null)
470       $xml->writeElement('fix', $var);
471     $var = $point->getSatellites();
472     if ($var !== null)
473       $xml->writeElement('sat', $var);
474     $var = $point->getHdop();
475     if ($var !== null)
476       $xml->writeElement('hdop', $var);
477     $var = $point->getVdop();
478     if ($var !== null)
479       $xml->writeElement('vdop', $var);
480     $var = $point->getPdop();
481     if ($var !== null)
482       $xml->writeElement('pdop', $var);
483     $var = $point->getAgeOfDGPSData();
484     if ($var !== null)
485       $xml->writeElement('ageofdgpsdata', $var);
486     $var = $point->getDGPSId();
487     if ($var !== null)
488       $xml->writeElement('dgpsid', $var);
489     $var = $point->getExtensions(false);
490     if ($var !== null && !$var->isEmpty()) {
491       if ($this->format == '1.1')
492         $xml->startElement('extensions');
493       foreach ($var as $extension) {
494         $xml->writeRaw($extension);
495       }
496       if ($this->format == '1.1')
497         $xml->endElement();
498     }
499     $xml->endElement();
500   }
501
502   /**
503    * Write XML for a route element.
504    *
505    * @param Route $route The route to write.
506    * @param XMLWriter $xml Where to write.
507    */
508   protected function writeRoute(Route $route, XMLWriter $xml)
509   {
510     $xml->startElement('rte');
511     $var = $route->getName();
512     if ($var !== null)
513       $xml->writeElement('name', $var);
514     $var = $route->getComment();
515     if ($var !== null)
516       $xml->writeElement('cmt', $var);
517     $var = $route->getDescription();
518     if ($var !== null)
519       $xml->writeElement('desc', $var);
520     $var = $route->getSource();
521     if ($var !== null)
522       $xml->writeElement('src', $var);
523     $var = $route->getLinks(false);
524     if ($var !== null) {
525       foreach($var as $link) {
526         $this->writeLink($link, $xml);
527         if ($this->format == '1.0')
528           break;
529       }
530     }
531     $var = $route->getNumber();
532     if ($var !== null)
533       $xml->writeElement('number', $var);
534     if ($this->format == '1.1') {
535       $var = $route->getType();
536       if ($var !== null)
537         $xml->writeElement('type', $var);
538     }
539     $var = $route->getExtensions(false);
540     if ($var !== null && !$var->isEmpty()) {
541       if ($this->format == '1.1')
542         $xml->startElement('extensions');
543       foreach ($var as $extension) {
544         $xml->writeRaw($extension);
545       }
546       if ($this->format == '1.1')
547         $xml->endElement();
548     }
549     $var = $route->getPoints(false);
550     if ($var !== null) {
551       foreach ($var as $point) {
552         $this->writePoint($point, 'rtept', $xml);
553       }
554     }
555     $xml->endElement();
556   }
557
558 }