5 * Copyright 2018 Andy Street <andy@street.me.uk>
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.
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.
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,
28 use \DateTimeInterface;
30 use \RuntimeException;
31 use \UnexpectedValueException;
37 * @author Andy Street <andy@street.me.uk>
43 * Who to credit as the document creator in the GPX root element.
46 * @see http://www.topografix.com/GPX/1/1/#type_gpxType
48 protected $creator = 'libgpx';
51 * The version of the GPX standard to generate.
55 protected $format = '1.1';
58 * Whether to indent the XML output for readability.
62 protected $indent = true;
65 * Whether to include milliseconds in timestamps.
69 protected $milliseconds = true;
72 * Fetch the name credited as the creator of the XML files.
74 * @return string The creator.
76 public function getCreator()
78 return $this->creator;
82 * Set the name to be credited as the XML document creator.
84 * @param string $creator The name to credit as the creator.
87 public function setCreator(string $creator)
89 $this->creator = $creator;
93 * Fetch the GPX file format that this class generates.
95 * @return string The GPX file format.
97 public function getFormat()
103 * Set the GPX file format that this class generates.
105 * @param string $format Accepted values are "1.0" or "1.1".
107 * @throws UnexpectedValueException If $format is not a known GPX version.
109 public function setFormat(string $format)
114 $this->format = $format;
117 throw new UnexpectedValueException(
118 sprintf('Unknown GPX version "%s"', $format)
124 * Whether this class will indent output for readability.
126 * @return boolean If the output will be indented.
128 public function getIndent()
130 return $this->indent;
134 * Set whether to indent output for readability.
136 * @param boolean $indent Whether to indent.
139 public function setIndent(bool $indent)
141 $this->indent = $indent;
145 * Whether timestamps are written to include milliseconds.
149 public function getTimestampUseMilliseconds()
151 return $this->milliseconds;
155 * Set whether to output milliseconds in timestamps.
157 * @param boolean $milliseconds Whether to use milliseconds.
160 public function setTimestampUseMilliseconds(bool $milliseconds)
162 $this->milliseconds = $milliseconds;
166 * Fetch an XML representation as a string.
168 * @param GPX $gpx The source for the XML.
169 * @return string An XML representation of the GPX object.
171 public function fetch(GPX $gpx)
173 $xml = new XMLWriter();
175 $this->write($gpx, $xml);
176 return $xml->outputMemory();
180 * Write XML to standard out.
182 * @param GPX $gpx The source for the XML.
185 public function output(GPX $gpx)
187 $xml = new XMLWriter();
188 $xml->openURI('php://output');
189 $this->write($gpx, $xml);
193 * Write XML to a file.
195 * @param GPX $gpx The source for the XML.
196 * @param string $filename Where to write the XML.
198 * @throws RuntimeException If the file cannot be written to.
200 public function save(GPX $gpx, string $filename)
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);
211 * @param GPX $gpx The source for the XML.
212 * @param XMLWriter $xml The instance to use to generate the XML.
215 protected function write(GPX $gpx, XMLWriter $xml)
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;
223 $namespace = libgpx::NAMESPACE_GPX_1_0;
224 $schema = libgpx::SCHEMA_GPX_1_0;
226 $xml->startElementNs(null, 'gpx', $namespace);
227 $xml->writeAttribute('version', $this->format);
228 $xml->writeAttribute('creator', $this->creator);
229 $xml->writeAttributeNS(
232 libgpx::NAMESPACE_XMLSCHEMA,
233 $namespace . ' ' . $schema
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);
242 $routes = $gpx->getRoutes(false);
243 if ($routes !== null) {
244 foreach ($routes as $route) {
245 $this->writeRoute($route, $xml);
254 * Generate XML for Metadata elements.
256 * @param GPX $gpx The object to source the metadata from.
257 * @param XMLWriter $xml Where to write the metadata.
260 protected function writeMetadata(GPX $gpx, XMLWriter $xml)
262 if ($this->format == '1.1')
263 $xml->startElement('metadata');
264 $name = $gpx->getName();
266 $xml->writeElement('name', $name);
267 $desc = $gpx->getDesc();
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);
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')
286 $xml->writeElement('time', $this->createTimestamp(new DateTime()));
287 $keywords = $gpx->getKeywords(false);
288 if ($keywords !== null && !$keywords->isEmpty())
291 implode(', ', $gpx->getKeywords()->toArray())
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());
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);
311 $xml->endElement(); // </metadata>
316 * Generate XML for GPX Person type.
318 * @param Person $person The person object to source data from.
319 * @param XMLWriter $xml Where to write the data.
322 protected function writePerson(Person $person, XMLWriter $xml)
324 if ($this->format == '1.1') {
325 $xml->startElement('author');
326 $name = $person->getName();
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]);
337 $link = $person->getLink();
339 $this->writeLink($link, $xml);
342 $name = $person->getName();
344 $xml->writeElement('author', $name);
345 $email = $person->getEmail();
347 $xml->writeElement('email', $email);
352 * Generate XML for a GPX Link type.
354 * @param Link $link The link object to source data from.
355 * @param XMLWriter $xml Where to write the data.
358 protected function writeLink(Link $link, XMLWriter $xml)
360 if ($this->format == '1.1') {
361 $xml->startElement('link');
362 $xml->writeAttribute('href', $link->getHref());
363 $text = $link->getText();
365 $xml->writeElement('text', $text);
366 $type = $link->getType();
368 $xml->writeElement('type', $type);
371 $xml->writeElement('url', $link->getHref());
372 $text = $link->getText();
374 $xml->writeElement('urlname', $text);
379 * Generate XML for GPX Copyright type.
381 * @param Copyright $copyright Object to source copyright data from.
382 * @param XMLWriter $xml Where to write the data.
385 protected function writeCopyright(Copyright $copyright, XMLWriter $xml)
387 $xml->startElement('copyright');
388 $xml->writeAttribute('author', $copyright->getAuthor());
389 $year = $copyright->getYear();
391 $xml->writeElement('year', $year);
392 $license = $copyright->getLicense();
393 if (!empty($license))
394 $xml->writeElement('license', $license);
399 * Create an ISO 8601 timestamp.
401 * @param DateTimeInterface $timestamp The timestamp to format.
402 * @param boolean $millis Include milliseconds.
403 * @return string A timestamp.
405 protected function createTimestamp(
406 DateTimeInterface $timestamp,
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);
418 * Generate XML for GPX wpt type.
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.
425 protected function writePoint(Point $point, string $element, XMLWriter $xml)
427 $xml->startElement($element);
428 $xml->writeAttribute('lat', $point->getLatitude());
429 $xml->writeAttribute('lon', $point->getLongitude());
430 $val = $point->getEle();
432 $xml->writeElement('ele', $val);
433 $val = $point->getTime();
435 $xml->writeElement('time', $this->createTimestamp($val));
436 $val = $point->getMagvar();
438 $xml->writeElement('magvar', $val);
439 $val = $point->getGeoidHeight();
441 $xml->writeElement('geoidheight', $val);
442 $var = $point->getName();
444 $xml->writeElement('name', $var);
445 $var = $point->getComment();
447 $xml->writeElement('cmt', $var);
448 $var = $point->getDescription();
450 $xml->writeElement('desc', $var);
451 $var = $point->getSource();
453 $xml->writeElement('src', $var);
454 $var = $point->getLinks(false);
456 foreach($var as $link) {
457 $this->writeLink($link, $xml);
458 if ($this->format == '1.0')
462 $var = $point->getSymbol();
464 $xml->writeElement('sym', $var);
465 $var = $point->getType();
467 $xml->writeElement('type', $var);
468 $var = $point->getFix();
470 $xml->writeElement('fix', $var);
471 $var = $point->getSatellites();
473 $xml->writeElement('sat', $var);
474 $var = $point->getHdop();
476 $xml->writeElement('hdop', $var);
477 $var = $point->getVdop();
479 $xml->writeElement('vdop', $var);
480 $var = $point->getPdop();
482 $xml->writeElement('pdop', $var);
483 $var = $point->getAgeOfDGPSData();
485 $xml->writeElement('ageofdgpsdata', $var);
486 $var = $point->getDGPSId();
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);
496 if ($this->format == '1.1')
503 * Write XML for a route element.
505 * @param Route $route The route to write.
506 * @param XMLWriter $xml Where to write.
508 protected function writeRoute(Route $route, XMLWriter $xml)
510 $xml->startElement('rte');
511 $var = $route->getName();
513 $xml->writeElement('name', $var);
514 $var = $route->getComment();
516 $xml->writeElement('cmt', $var);
517 $var = $route->getDescription();
519 $xml->writeElement('desc', $var);
520 $var = $route->getSource();
522 $xml->writeElement('src', $var);
523 $var = $route->getLinks(false);
525 foreach($var as $link) {
526 $this->writeLink($link, $xml);
527 if ($this->format == '1.0')
531 $var = $route->getNumber();
533 $xml->writeElement('number', $var);
534 if ($this->format == '1.1') {
535 $var = $route->getType();
537 $xml->writeElement('type', $var);
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);
546 if ($this->format == '1.1')
549 $var = $route->getPoints(false);
551 foreach ($var as $point) {
552 $this->writePoint($point, 'rtept', $xml);