1 <?php 2 3 namespace geoPHP\Adapter; 4 5 use DOMDocument; 6 use DOMElement; 7 use DOMXPath; 8 use geoPHP\Geometry\Collection; 9 use geoPHP\geoPHP; 10 use geoPHP\Geometry\Geometry; 11 use geoPHP\Geometry\GeometryCollection; 12 use geoPHP\Geometry\Point; 13 use geoPHP\Geometry\LineString; 14 use geoPHP\Geometry\MultiLineString; 15 16 /* 17 * Copyright (c) Patrick Hayes 18 * 19 * This code is open-source and licenced under the Modified BSD License. 20 * For the full copyright and license information, please view the LICENSE 21 * file that was distributed with this source code. 22 */ 23 24 /** 25 * PHP Geometry/GPX encoder/decoder 26 */ 27 class GPX implements GeoAdapter 28 { 29 30 protected $nss = ''; // Name-space string. eg 'georss:' 31 32 /** 33 * @var GpxTypes 34 */ 35 protected $gpxTypes; 36 37 /** 38 * @var DOMXPath 39 */ 40 protected $xpath; 41 42 protected $parseGarminRpt = false; 43 44 protected $trackFromRoute = null; 45 46 /** 47 * Read GPX string into geometry object 48 * 49 * @param string $gpx A GPX string 50 * @param array|null $allowedElements Which elements can be read from each GPX type 51 * If not specified, every element defined in the GPX specification can be read 52 * Can be overwritten with an associative array, with type name in keys. 53 * eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null] 54 * @return Geometry|GeometryCollection 55 * @throws \Exception If GPX is not a valid XML 56 */ 57 public function read($gpx, $allowedElements = null) 58 { 59 $this->gpxTypes = new GpxTypes($allowedElements); 60 61 //libxml_use_internal_errors(true); // why? 62 63 // Load into DOMDocument 64 $xmlObject = new DOMDocument('1.0', 'UTF-8'); 65 $xmlObject->preserveWhiteSpace = false; 66 @$xmlObject->loadXML($gpx); 67 if ($xmlObject === false) { 68 throw new \Exception("Invalid GPX: " . $gpx); 69 } 70 71 $this->parseGarminRpt = strpos($gpx, 'gpxx:rpt') > 0; 72 73 // Initialize XPath parser if needed (currently only for Garmin extensions) 74 if ($this->parseGarminRpt) { 75 $this->xpath = new DOMXPath($xmlObject); 76 $this->xpath->registerNamespace('gpx', 'http://www.topografix.com/GPX/1/1'); 77 $this->xpath->registerNamespace('gpxx', 'http://www.garmin.com/xmlschemas/GpxExtensions/v3'); 78 } 79 80 try { 81 $geom = $this->geomFromXML($xmlObject); 82 if ($geom->isEmpty()) { 83 /* Geometry was empty but maybe because its tags was not lower cased. 84 We try to lower-case tags and try to run again, but just once. 85 */ 86 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 87 $caller = isset($backtrace[1]['function']) ? $backtrace[1]['function'] : null; 88 if ($caller && $caller !== __FUNCTION__) { 89 $gpx = preg_replace_callback( 90 "/(<\/?\w+)(.*?>)/", 91 function ($m) { 92 return strtolower($m[1]) . $m[2]; 93 }, 94 $gpx 95 ); 96 $geom = $this->read($gpx, $allowedElements); 97 } 98 } 99 } catch (\Exception $e) { 100 throw new \Exception("Cannot Read Geometry From GPX: " . $gpx); 101 } 102 103 return $geom; 104 } 105 106 /** 107 * Parses the GPX XML and returns a geometry 108 * @param DOMDocument $xmlObject 109 * @return GeometryCollection|Geometry Returns the geometry representation of the GPX (@see geoPHP::buildGeometry) 110 */ 111 protected function geomFromXML($xmlObject) 112 { 113 /** @var Geometry[] $geometries */ 114 $geometries = array_merge( 115 $this->parseWaypoints($xmlObject), 116 $this->parseTracks($xmlObject), 117 $this->parseRoutes($xmlObject) 118 ); 119 120 if (isset($this->trackFromRoute)) { 121 $trackFromRoute = new LineString($this->trackFromRoute); 122 $trackFromRoute->setData('gpxType', 'track'); 123 $trackFromRoute->setData('type', 'planned route'); 124 $geometries[] = $trackFromRoute; 125 } 126 127 $geometry = geoPHP::buildGeometry($geometries); 128 if (in_array('metadata', $this->gpxTypes->get('gpxType')) && $xmlObject->getElementsByTagName('metadata')->length === 1) { 129 $metadata = self::parseNodeProperties( 130 $xmlObject->getElementsByTagName('metadata')->item(0), 131 $this->gpxTypes->get('metadataType') 132 ); 133 if ($geometry->getData() !== null && $metadata !== null) { 134 $geometry = new GeometryCollection([$geometry]); 135 } 136 $geometry->setData($metadata); 137 } 138 139 return $geometry; 140 } 141 142 protected function childElements($xml, $nodeName = '') 143 { 144 $children = []; 145 foreach ($xml->childNodes as $child) { 146 if ($child->nodeName == $nodeName) { 147 $children[] = $child; 148 } 149 } 150 return $children; 151 } 152 153 /** 154 * @param DOMElement $node 155 * @return Point 156 */ 157 protected function parsePoint($node) 158 { 159 $lat = $node->attributes->getNamedItem("lat")->nodeValue; 160 $lon = $node->attributes->getNamedItem("lon")->nodeValue; 161 $elevation = null; 162 $ele = $node->getElementsByTagName('ele'); 163 if ($ele->length) { 164 $elevation = $ele->item(0)->nodeValue; 165 } 166 $point = new Point($lon, $lat, $elevation); 167 $point->setData($this->parseNodeProperties($node, $this->gpxTypes->get($node->nodeName . 'Type'))); 168 if ($node->nodeName === 'rtept' && $this->parseGarminRpt) { 169 foreach ($this->xpath->query('.//gpx:extensions/gpxx:RoutePointExtension/gpxx:rpt', $node) as $element) { 170 $this->trackFromRoute[] = $this->parsePoint($element); 171 } 172 } 173 return $point; 174 } 175 176 /** 177 * @param DOMDocument $xmlObject 178 * @return Point[] 179 */ 180 protected function parseWaypoints($xmlObject) 181 { 182 if (!in_array('wpt', $this->gpxTypes->get('gpxType'))) { 183 return []; 184 } 185 $points = []; 186 $wptElements = $xmlObject->getElementsByTagName('wpt'); 187 foreach ($wptElements as $wpt) { 188 $point = $this->parsePoint($wpt); 189 $point->setData('gpxType', 'waypoint'); 190 $points[] = $point; 191 } 192 return $points; 193 } 194 195 /** 196 * @param DOMDocument $xmlObject 197 * @return LineString[] 198 */ 199 protected function parseTracks($xmlObject) 200 { 201 if (!in_array('trk', $this->gpxTypes->get('gpxType'))) { 202 return []; 203 } 204 $tracks = []; 205 $trkElements = $xmlObject->getElementsByTagName('trk'); 206 foreach ($trkElements as $trk) { 207 $segments = []; 208 /** @noinspection SpellCheckingInspection */ 209 foreach ($this->childElements($trk, 'trkseg') as $trkseg) { 210 $points = []; 211 /** @noinspection SpellCheckingInspection */ 212 foreach ($this->childElements($trkseg, 'trkpt') as $trkpt) { 213 $points[] = $this->parsePoint($trkpt); 214 } 215 // Avoids creating invalid LineString 216 $segments[] = new LineString(count($points) <> 1 ? $points : []); 217 } 218 $track = count($segments) === 0 219 ? new LineString() 220 : (count($segments) === 1 221 ? $segments[0] 222 : new MultiLineString($segments)); 223 $track->setData($this->parseNodeProperties($trk, $this->gpxTypes->get('trkType'))); 224 $track->setData('gpxType', 'track'); 225 $tracks[] = $track; 226 } 227 return $tracks; 228 } 229 230 /** 231 * @param DOMDocument $xmlObject 232 * @return LineString[] 233 */ 234 protected function parseRoutes($xmlObject) 235 { 236 if (!in_array('rte', $this->gpxTypes->get('gpxType'))) { 237 return []; 238 } 239 $lines = []; 240 $rteElements = $xmlObject->getElementsByTagName('rte'); 241 foreach ($rteElements as $rte) { 242 $components = []; 243 /** @noinspection SpellCheckingInspection */ 244 foreach ($this->childElements($rte, 'rtept') as $routePoint) { 245 /** @noinspection SpellCheckingInspection */ 246 $components[] = $this->parsePoint($routePoint); 247 } 248 $line = new LineString($components); 249 $line->setData($this->parseNodeProperties($rte, $this->gpxTypes->get('rteType'))); 250 $line->setData('gpxType', 'route'); 251 $lines[] = $line; 252 } 253 return $lines; 254 } 255 256 /** 257 * Parses a DOMNode and returns its content in a multidimensional associative array 258 * eg: <wpt><name>Test</name><link href="example.com"><text>Example</text></link></wpt> 259 * to: ['name' => 'Test', 'link' => ['text'] => 'Example', '@attributes' => ['href' => 'example.com']] 260 * 261 * @param \DOMNode $node 262 * @param string[]|null $tagList 263 * @return array|string 264 */ 265 protected static function parseNodeProperties($node, $tagList = null) 266 { 267 if ($node->nodeType === XML_TEXT_NODE) { 268 return $node->nodeValue; 269 } 270 $result = []; 271 foreach ($node->childNodes as $childNode) { 272 /** @var \DOMNode $childNode */ 273 if ($childNode->hasChildNodes()) { 274 if ($tagList === null || in_array($childNode->nodeName, $tagList ?: [])) { 275 if ($node->firstChild->nodeName == $node->lastChild->nodeName && $node->childNodes->length > 1) { 276 $result[$childNode->nodeName][] = self::parseNodeProperties($childNode); 277 } else { 278 $result[$childNode->nodeName] = self::parseNodeProperties($childNode); 279 } 280 } 281 } elseif ($childNode->nodeType === 1 && in_array($childNode->nodeName, $tagList ?: [])) { 282 $result[$childNode->nodeName] = self::parseNodeProperties($childNode); 283 } elseif ($childNode->nodeType === 3) { 284 $result = $childNode->nodeValue; 285 } 286 } 287 if ($node->hasAttributes()) { 288 if (is_string($result)) { 289 // As of the GPX specification text node cannot have attributes, thus this never happens 290 $result = ['#text' => $result]; 291 } 292 $attributes = []; 293 foreach ($node->attributes as $attribute) { 294 if ($attribute->name !== 'lat' && $attribute->name !== 'lon' && trim($attribute->value) !== '') { 295 $attributes[$attribute->name] = trim($attribute->value); 296 } 297 } 298 if (count($attributes)) { 299 $result['@attributes'] = $attributes; 300 } 301 } 302 return $result; 303 } 304 305 306 /** 307 * Serialize geometries into a GPX string. 308 * 309 * @param Geometry|GeometryCollection $geometry 310 * @param string|null $namespace 311 * @param array|null $allowedElements Which elements can be added to each GPX type 312 * If not specified, every element defined in the GPX specification can be added 313 * Can be overwritten with an associative array, with type name in keys. 314 * eg.: ['wptType' => ['ele', 'name'], 'trkptType' => ['ele'], 'metadataType' => null] 315 * @return string The GPX string representation of the input geometries 316 */ 317 public function write(Geometry $geometry, $namespace = null, $allowedElements = null) 318 { 319 if ($namespace) { 320 $this->nss = $namespace . ':'; 321 } 322 $this->gpxTypes = new GpxTypes($allowedElements); 323 324 return 325 '<?xml version="1.0" encoding="UTF-8"?> 326 <' . $this->nss . 'gpx creator="geoPHP" version="1.1" 327 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 328 xmlns="http://www.topografix.com/GPX/1/1" 329 xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" > 330 331 ' . $this->geometryToGPX($geometry) . 332 '</' . $this->nss . 'gpx> 333 '; 334 } 335 336 /** 337 * @param Geometry|Collection $geometry 338 * @return string 339 */ 340 protected function geometryToGPX($geometry) 341 { 342 switch ($geometry->geometryType()) { 343 case Geometry::POINT: 344 /** @var Point $geometry */ 345 return $this->pointToGPX($geometry); 346 case Geometry::LINE_STRING: 347 case Geometry::MULTI_LINE_STRING: 348 /** @var LineString $geometry */ 349 return $this->linestringToGPX($geometry); 350 case Geometry::POLYGON: 351 case Geometry::MULTI_POINT: 352 case Geometry::MULTI_POLYGON: 353 case Geometry::GEOMETRY_COLLECTION: 354 return $this->collectionToGPX($geometry); 355 } 356 return ''; 357 } 358 359 /** 360 * @param Point $geom 361 * @param string $tag Can be "wpt", "trkpt" or "rtept" 362 * @return string 363 */ 364 private function pointToGPX($geom, $tag = 'wpt') 365 { 366 if ($geom->isEmpty() || ($tag === 'wpt' && !in_array($tag, $this->gpxTypes->get('gpxType')))) { 367 return ''; 368 } 369 $indent = $tag === 'trkpt' ? "\t\t" : ($tag === 'rtept' ? "\t" : ''); 370 371 if ($geom->hasZ() || $geom->getData() !== null) { 372 $node = $indent . "<" . $this->nss . $tag . " lat=\"" . $geom->getY() . "\" lon=\"" . $geom->getX() . "\">\n"; 373 if ($geom->hasZ()) { 374 $geom->setData('ele', $geom->z()); 375 } 376 $node .= self::processGeometryData($geom, $this->gpxTypes->get($tag . 'Type'), $indent . "\t") . 377 $indent . "</" . $this->nss . $tag . ">\n"; 378 if ($geom->hasZ()) { 379 $geom->setData('ele', null); 380 } 381 return $node; 382 } 383 return $indent . "<" . $this->nss . $tag . " lat=\"" . $geom->getY() . "\" lon=\"" . $geom->getX() . "\" />\n"; 384 } 385 386 /** 387 * Writes a LineString or MultiLineString to the GPX 388 * 389 * The (Multi)LineString will be included in a <trk></trk> block 390 * The LineString or each LineString of the MultiLineString will be in <trkseg> </trkseg> inside the <trk> 391 * 392 * @param LineString|MultiLineString $geom 393 * @return string 394 */ 395 private function linestringToGPX($geom) 396 { 397 $isTrack = $geom->getData('gpxType') === 'route' ? false : true; 398 if ($geom->isEmpty() || !in_array($isTrack ? 'trk' : 'rte', $this->gpxTypes->get('gpxType'))) { 399 return ''; 400 } 401 402 if ($isTrack) { // write as <trk> 403 404 /** @noinspection SpellCheckingInspection */ 405 $gpx = "<" . $this->nss . "trk>\n" . self::processGeometryData($geom, $this->gpxTypes->get('trkType')); 406 $components = $geom->geometryType() === 'LineString' ? [$geom] : $geom->getComponents(); 407 foreach ($components as $lineString) { 408 $gpx .= "\t<" . $this->nss . "trkseg>\n"; 409 foreach ($lineString->getPoints() as $point) { 410 $gpx .= $this->pointToGPX($point, 'trkpt'); 411 } 412 $gpx .= "\t</" . $this->nss . "trkseg>\n"; 413 } 414 /** @noinspection SpellCheckingInspection */ 415 $gpx .= "</" . $this->nss . "trk>\n"; 416 } else { // write as <rte> 417 418 /** @noinspection SpellCheckingInspection */ 419 $gpx = "<" . $this->nss . "rte>\n" . self::processGeometryData($geom, $this->gpxTypes->get('rteType')); 420 foreach ($geom->getPoints() as $point) { 421 $gpx .= $this->pointToGPX($point, 'rtept'); 422 } 423 /** @noinspection SpellCheckingInspection */ 424 $gpx .= "</" . $this->nss . "rte>\n"; 425 } 426 427 return $gpx; 428 } 429 430 /** 431 * @param Collection $geometry 432 * @return string 433 */ 434 public function collectionToGPX($geometry) 435 { 436 $metadata = self::processGeometryData($geometry, $this->gpxTypes->get('metadataType')); 437 $metadata = empty($metadata) || !in_array('metadataType', $this->gpxTypes->get('gpxType')) 438 ? '' 439 : "<metadata>\n{$metadata}</metadata>\n\n"; 440 $wayPoints = $routes = $tracks = ""; 441 442 foreach ($geometry->getComponents() as $component) { 443 if (strpos($component->geometryType(), 'Point') !== false) { 444 $wayPoints .= $this->geometryToGPX($component); 445 } 446 if (strpos($component->geometryType(), 'LineString') !== false && $component->getData('gpxType') === 'route') { 447 $routes .= $this->geometryToGPX($component); 448 } 449 if (strpos($component->geometryType(), 'LineString') !== false && $component->getData('gpxType') !== 'route') { 450 $tracks .= $this->geometryToGPX($component); 451 } 452 if (strpos($component->geometryType(), 'Point') === false && strpos($component->geometryType(), 'LineString') === false) { 453 return $this->geometryToGPX($component); 454 } 455 } 456 457 return $metadata . $wayPoints . $routes . $tracks; 458 } 459 460 /** 461 * @param Geometry $geometry 462 * @param string[] $tagList Allowed tags 463 * @param string $indent 464 * @return string 465 */ 466 protected static function processGeometryData($geometry, $tagList, $indent = "\t") 467 { 468 $tags = ''; 469 if ($geometry->getData() !== null) { 470 foreach ($tagList as $tagName) { 471 if ($geometry->hasDataProperty($tagName)) { 472 $tags .= self::createNodes($tagName, $geometry->getData($tagName), $indent) . "\n"; 473 } 474 } 475 } 476 return $tags; 477 } 478 479 /** 480 * @param string $tagName 481 * @param string|array $value 482 * @param string $indent 483 * @return string 484 */ 485 protected static function createNodes($tagName, $value, $indent) 486 { 487 $attributes = ''; 488 if (!is_array($value)) { 489 $returnValue = $value; 490 } else { 491 $returnValue = ''; 492 if (array_key_exists('@attributes', $value)) { 493 $attributes = ''; 494 foreach ($value['@attributes'] as $attributeName => $attributeValue) { 495 $attributes .= ' ' . $attributeName . '="' . $attributeValue . '"'; 496 } 497 unset($value['@attributes']); 498 } 499 foreach ($value as $subKey => $subValue) { 500 $returnValue .= "\n" . self::createNodes($subKey, $subValue, $indent . "\t") . "\n" . $indent; 501 } 502 } 503 return $indent . "<{$tagName}{$attributes}>{$returnValue}</{$tagName}>"; 504 } 505 } 506