]> git.street.me.uk Git - andy/gpx.git/blame - src/libgpx/gpxwriter.php
Add extensions for GPX type
[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);
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);
57c75891
AS
236 $waypoints = $gpx->getWaypoints(false);
237 if ($waypoints !== null) {
238 foreach ($waypoints as $wpt) {
239 $this->writePoint($wpt, 'wpt', $xml);
240 }
f528d248 241 }
f17d2566
AS
242 $routes = $gpx->getRoutes(false);
243 if ($routes !== null) {
244 foreach ($routes as $route) {
245 $this->writeRoute($route, $xml);
246 }
247 }
35c309cc
AS
248 $tracks = $gpx->getTracks(false);
249 if ($tracks !== null) {
250 foreach ($tracks as $track) {
251 $this->writeTrack($track, $xml);
252 }
253 }
e0c26559
AS
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 }
88564339
AS
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 }
57c75891
AS
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 }
88564339
AS
301 }
302 $xml->writeElement('time', $this->createTimestamp(new DateTime()));
57c75891
AS
303 $keywords = $gpx->getKeywords(false);
304 if ($keywords !== null && !$keywords->isEmpty())
7c465a82
AS
305 $xml->writeElement(
306 'keywords',
307 implode(', ', $gpx->getKeywords()->toArray())
308 );
88564339
AS
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 }
7c465a82 318 if ($this->format == '1.1') {
57c75891
AS
319 $extensions = $gpx->getMetadataExtensions(false);
320 if ($extensions !== null && !$extensions->isEmpty()) {
7c465a82
AS
321 $xml->startElement('extensions');
322 foreach ($extensions as $extension) {
323 $xml->writeRaw($extension);
324 }
325 $xml->endElement();
326 }
327 $xml->endElement(); // </metadata>
328 }
88564339
AS
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
09a36623
AS
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
f528d248
AS
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);
f528d248
AS
504 $var = $point->getSymbol();
505 if ($var !== null)
506 $xml->writeElement('sym', $var);
f528d248
AS
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);
09a36623 528 $this->writeDataType($point, $xml);
f528d248
AS
529 $xml->endElement();
530 }
531
f17d2566
AS
532 /**
533 * Write XML for a route element.
534 *
535 * @param Route $route The route to write.
536 * @param XMLWriter $xml Where to write.
35c309cc 537 * @return void
f17d2566
AS
538 */
539 protected function writeRoute(Route $route, XMLWriter $xml)
540 {
541 $xml->startElement('rte');
f17d2566
AS
542 $var = $route->getNumber();
543 if ($var !== null)
544 $xml->writeElement('number', $var);
09a36623 545 $this->writeDataType($route, $xml);
f17d2566
AS
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
35c309cc
AS
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
88564339 607}