1<?php 2 3/* 4 * @author Báthory Péter 5 * @since 2016-02-27 6 * 7 * This code is open-source and licenced under the Modified BSD License. 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11namespace geoPHP\Adapter; 12 13use geoPHP\Geometry\Collection; 14use geoPHP\Geometry\Geometry; 15use geoPHP\Geometry\GeometryCollection; 16use geoPHP\Geometry\Point; 17use geoPHP\Geometry\MultiPoint; 18use geoPHP\Geometry\LineString; 19use geoPHP\Geometry\MultiLineString; 20use geoPHP\Geometry\Polygon; 21use geoPHP\Geometry\MultiPolygon; 22 23/** 24 * PHP Geometry <-> OpenStreetMap XML encoder/decoder 25 * 26 * This adapter is not ready yet. It lacks a relation writer, and the reader has problems with invalid multipolygons 27 * Since geoPHP doesn't support metadata, it cannot read and write OSM tags. 28 */ 29class OSM implements GeoAdapter 30{ 31 const OSM_COORDINATE_PRECISION = '%.7f'; 32 const OSM_API_URL = 'http://openstreetmap.org/api/0.6/'; 33 34 /** @var \DOMDocument $xmlObj */ 35 protected $xmlObj; 36 37 protected $nodes = []; 38 39 protected $ways = []; 40 41 protected $idCounter = 0; 42 43 /** 44 * Read OpenStreetMap XML string into geometry objects 45 * 46 * @param string $osm An OSM XML string 47 * 48 * @return Geometry|GeometryCollection 49 * @throws \Exception 50 */ 51 public function read($osm) 52 { 53 // Load into DOMDocument 54 $xmlobj = new \DOMDocument(); 55 $xmlobj->loadXML($osm); 56 if ($xmlobj === false) { 57 throw new \Exception("Invalid OSM XML: " . substr($osm, 0, 100)); 58 } 59 60 $this->xmlObj = $xmlobj; 61 try { 62 $geom = $this->geomFromXML(); 63 } catch (\Exception $e) { 64 throw new \Exception("Cannot read geometries from OSM XML: " . $e->getMessage()); 65 } 66 67 return $geom; 68 } 69 70 protected function geomFromXML() 71 { 72 $geometries = []; 73 74 // Processing OSM Nodes 75 $nodes = []; 76 foreach ($this->xmlObj->getElementsByTagName('node') as $node) { 77 /** @var \DOMElement $node */ 78 $lat = $node->attributes->getNamedItem('lat')->nodeValue; 79 $lon = $node->attributes->getNamedItem('lon')->nodeValue; 80 $id = intval($node->attributes->getNamedItem('id')->nodeValue); 81 $tags = []; 82 foreach ($node->getElementsByTagName('tag') as $tag) { 83 $key = $tag->attributes->getNamedItem('k')->nodeValue; 84 if ($key === 'source' || $key === 'fixme' || $key === 'created_by') { 85 continue; 86 } 87 $tags[$key] = $tag->attributes->getNamedItem('v')->nodeValue; 88 } 89 $nodes[$id] = [ 90 'point' => new Point($lon, $lat), 91 'assigned' => false, 92 'tags' => $tags 93 ]; 94 } 95 if (empty($nodes)) { 96 return new GeometryCollection(); 97 } 98 99 // Processing OSM Ways 100 $ways = []; 101 foreach ($this->xmlObj->getElementsByTagName('way') as $way) { 102 /** @var \DOMElement $way */ 103 $id = intval($way->attributes->getNamedItem('id')->nodeValue); 104 $wayNodes = []; 105 foreach ($way->getElementsByTagName('nd') as $node) { 106 $ref = intval($node->attributes->getNamedItem('ref')->nodeValue); 107 if (isset($nodes[$ref])) { 108 $nodes[$ref]['assigned'] = true; 109 $wayNodes[] = $ref; 110 } 111 } 112 $tags = []; 113 foreach ($way->getElementsByTagName('tag') as $tag) { 114 $key = $tag->attributes->getNamedItem('k')->nodeValue; 115 if ($key === 'source' || $key === 'fixme' || $key === 'created_by') { 116 continue; 117 } 118 $tags[$key] = $tag->attributes->getNamedItem('v')->nodeValue; 119 } 120 if (count($wayNodes) >= 2) { 121 $ways[$id] = [ 122 'nodes' => $wayNodes, 123 'assigned' => false, 124 'tags' => $tags, 125 'isRing' => ($wayNodes[0] === $wayNodes[count($wayNodes) - 1]) 126 ]; 127 } 128 } 129 130 131 // Processing OSM Relations 132 foreach ($this->xmlObj->getElementsByTagName('relation') as $relation) { 133 /** @var \DOMElement $relation */ 134 /** @var Point[] */ 135 $relationPoints = []; 136 /** @var LineString[] */ 137 $relationLines = []; 138 /** @var Polygon[] */ 139 $relationPolygons = []; 140 141 static $polygonalTypes = ['multipolygon', 'boundary']; 142 static $linearTypes = ['route', 'waterway']; 143 $relationType = null; 144 foreach ($relation->getElementsByTagName('tag') as $tag) { 145 if ($tag->attributes->getNamedItem('k')->nodeValue == 'type') { 146 $relationType = $tag->attributes->getNamedItem('v')->nodeValue; 147 } 148 } 149 150 // Collect relation members 151 /** @var array[] $relationWays */ 152 $relationWays = []; 153 foreach ($relation->getElementsByTagName('member') as $member) { 154 $memberType = $member->attributes->getNamedItem('type')->nodeValue; 155 $ref = $member->attributes->getNamedItem('ref')->nodeValue; 156 157 if ($memberType === 'node' && isset($nodes[$ref])) { 158 $nodes[$ref]['assigned'] = true; 159 $relationPoints[] = $nodes[$ref]['point']; 160 } 161 if ($memberType === 'way' && isset($ways[$ref])) { 162 $ways[$ref]['assigned'] = true; 163 $relationWays[$ref] = $ways[$ref]['nodes']; 164 } 165 } 166 167 if (in_array($relationType, $polygonalTypes)) { 168 $relationPolygons = $this->processMultipolygon($relationWays, $nodes); 169 } 170 if (in_array($relationType, $linearTypes)) { 171 $relationLines = $this->processRoutes($relationWays, $nodes); 172 } 173 174 // Assemble relation geometries 175 $geometryCollection = []; 176 if (!empty($relationPolygons)) { 177 $geometryCollection[] = count($relationPolygons) == 1 ? $relationPolygons[0] : new MultiPolygon($relationPolygons); 178 } 179 if (!empty($relationLines)) { 180 $geometryCollection[] = count($relationLines) == 1 ? $relationLines[0] : new MultiLineString($relationLines); 181 } 182 if (!empty($relationPoints)) { 183 $geometryCollection[] = count($relationPoints) == 1 ? $relationPoints[0] : new MultiPoint($relationPoints); 184 } 185 186 if (!empty($geometryCollection)) { 187 $geometries[] = count($geometryCollection) == 1 ? $geometryCollection[0] : new GeometryCollection($geometryCollection); 188 } 189 } 190 191 // Process ways 192 foreach ($ways as $way) { 193 if ( 194 (!$way['assigned'] || !empty($way['tags'])) 195 && !isset($way['tags']['boundary']) 196 && (!isset($way['tags']['natural']) || $way['tags']['natural'] !== 'mountain_range') 197 ) { 198 $linePoints = []; 199 foreach ($way['nodes'] as $wayNode) { 200 $linePoints[] = $nodes[$wayNode]['point']; 201 } 202 $line = new LineString($linePoints); 203 if ($way['isRing']) { 204 $polygon = new Polygon([$line]); 205 if ($polygon->isSimple()) { 206 $geometries[] = $polygon; 207 } else { 208 $geometries[] = $line; 209 } 210 } else { 211 $geometries[] = $line; 212 } 213 } 214 } 215 216 foreach ($nodes as $node) { 217 if (!$node['assigned'] || !empty($node['tags'])) { 218 $geometries[] = $node['point']; 219 } 220 } 221 222 //var_dump($geometries); 223 return count($geometries) == 1 ? $geometries[0] : new GeometryCollection($geometries); 224 } 225 226 protected function processRoutes(&$relationWays, &$nodes) 227 { 228 229 // Construct lines 230 /** @var LineString[] $lineStrings */ 231 $lineStrings = []; 232 while (count($relationWays) > 0) { 233 $line = array_shift($relationWays); 234 if ($line[0] !== $line[count($line) - 1]) { 235 do { 236 $waysAdded = 0; 237 foreach ($relationWays as $id => $wayNodes) { 238 // Last node of ring = first node of way => put way to the end of ring 239 if ($line[count($line) - 1] === $wayNodes[0]) { 240 $line = array_merge($line, array_slice($wayNodes, 1)); 241 unset($relationWays[$id]); 242 $waysAdded++; 243 // Last node of ring = last node of way => reverse way and put to the end of ring 244 } elseif ($line[count($line) - 1] === $wayNodes[count($wayNodes) - 1]) { 245 $line = array_merge($line, array_slice(array_reverse($wayNodes), 1)); 246 unset($relationWays[$id]); 247 $waysAdded++; 248 // First node of ring = last node of way => put way to the beginning of ring 249 } elseif ($line[0] === $wayNodes[count($wayNodes) - 1]) { 250 $line = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $line); 251 unset($relationWays[$id]); 252 $waysAdded++; 253 // First node of ring = first node of way => reverse way and put to the beginning of ring 254 } elseif ($line[0] === $wayNodes[0]) { 255 $line = array_merge(array_reverse(array_slice($wayNodes, 1)), $line); 256 unset($relationWays[$id]); 257 $waysAdded++; 258 } 259 } 260 // If line members are not ordered, we need to repeat end matching some times 261 } while ($waysAdded > 0); 262 } 263 264 // Create the new LineString 265 $linePoints = []; 266 foreach ($line as $lineNode) { 267 $linePoints[] = $nodes[$lineNode]['point']; 268 } 269 $lineStrings[] = new LineString($linePoints); 270 } 271 272 return $lineStrings; 273 } 274 275 protected function processMultipolygon(&$relationWays, &$nodes) 276 { 277 /* TODO: what to do with broken rings? 278 * I propose to force-close if start -> end point distance is less then 10% of line length, otherwise drop it. 279 * But if dropped, its inner ring will be outers, which is not good. 280 * We should save the role for each ring (outer, inner, mixed) during ring creation and check it during ring grouping 281 */ 282 283 // Construct rings 284 /** @var Polygon[] $rings */ 285 $rings = []; 286 while (!empty($relationWays)) { 287 $ring = array_shift($relationWays); 288 if ($ring[0] !== $ring[count($ring) - 1]) { 289 do { 290 $waysAdded = 0; 291 foreach ($relationWays as $id => $wayNodes) { 292 // Last node of ring = first node of way => put way to the end of ring 293 if ($ring[count($ring) - 1] === $wayNodes[0]) { 294 $ring = array_merge($ring, array_slice($wayNodes, 1)); 295 unset($relationWays[$id]); 296 $waysAdded++; 297 // Last node of ring = last node of way => reverse way and put to the end of ring 298 } elseif ($ring[count($ring) - 1] === $wayNodes[count($wayNodes) - 1]) { 299 $ring = array_merge($ring, array_slice(array_reverse($wayNodes), 1)); 300 unset($relationWays[$id]); 301 $waysAdded++; 302 // First node of ring = last node of way => put way to the beginning of ring 303 } elseif ($ring[0] === $wayNodes[count($wayNodes) - 1]) { 304 $ring = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $ring); 305 unset($relationWays[$id]); 306 $waysAdded++; 307 // First node of ring = first node of way => reverse way and put to the beginning of ring 308 } elseif ($ring[0] === $wayNodes[0]) { 309 $ring = array_merge(array_reverse(array_slice($wayNodes, 1)), $ring); 310 unset($relationWays[$id]); 311 $waysAdded++; 312 } 313 } 314 // If ring members are not ordered, we need to repeat end matching some times 315 } while ($waysAdded > 0 && $ring[0] !== $ring[count($ring) - 1]); 316 } 317 318 // Create the new Polygon 319 if ($ring[0] === $ring[count($ring) - 1]) { 320 $ringPoints = []; 321 foreach ($ring as $ringNode) { 322 $ringPoints[] = $nodes[$ringNode]['point']; 323 } 324 $newPolygon = new Polygon([new LineString($ringPoints)]); 325 if ($newPolygon->isSimple()) { 326 $rings[] = $newPolygon; 327 } 328 } 329 } 330 331 // Calculate containment 332 $containment = array_fill(0, count($rings), array_fill(0, count($rings), false)); 333 foreach ($rings as $i => $ring) { 334 foreach ($rings as $j => $ring2) { 335 if ($i !== $j && $ring->contains($ring2)) { 336 $containment[$i][$j] = true; 337 } 338 } 339 } 340 $containmentCount = count($containment); 341 342 /* 343 print ' '; 344 for($i=0; $i<count($rings); $i++) { 345 print $rings[$i]->getNumberOfPoints() . ' '; 346 } 347 print "<br>"; 348 for($i=0; $i<count($rings); $i++) { 349 print $rings[$i]->getNumberOfPoints() . ' '; 350 for($j=0; $j<count($rings); $j++) { 351 print ($containment[$i][$j] ? '1' : '0') . ' '; 352 } 353 print "<br>"; 354 }*/ 355 356 // Group rings (outers and inners) 357 358 /** @var boolean[] $found */ 359 $found = array_fill(0, $containmentCount, false); 360 $foundCount = 0; 361 $round = 0; 362 /** @var int[][] $polygonsRingIds */ 363 $polygonsRingIds = []; 364 /** @var Polygon[] $relationPolygons */ 365 $relationPolygons = []; 366 while ($foundCount < $containmentCount && $round < 100) { 367 $ringsFound = []; 368 for ($i = 0; $i < $containmentCount; $i++) { 369 if ($found[$i]) { 370 continue; 371 } 372 $containCount = 0; 373 for ($j = 0; $j < count($containment[$i]); $j++) { 374 if (!$found[$j]) { 375 $containCount += $containment[$j][$i]; 376 } 377 } 378 if ($containCount === 0) { 379 $ringsFound[] = $i; 380 } 381 } 382 if ($round % 2 === 0) { 383 $polygonsRingIds = []; 384 } 385 foreach ($ringsFound as $ringId) { 386 $found[$ringId] = true; 387 $foundCount++; 388 if ($round % 2 === 1) { 389 foreach ($polygonsRingIds as $outerId => $polygon) { 390 if ($containment[$outerId][$ringId]) { 391 $polygonsRingIds[$outerId][] = $ringId; 392 } 393 } 394 } else { 395 $polygonsRingIds[$ringId] = [0 => $ringId]; 396 } 397 } 398 if ($round % 2 === 1 || $foundCount === $containmentCount) { 399 foreach ($polygonsRingIds as $k => $ringGroup) { 400 $linearRings = []; 401 foreach ($ringGroup as $polygonRing) { 402 $linearRings[] = $rings[$polygonRing]->exteriorRing(); 403 } 404 $relationPolygons[] = new Polygon($linearRings); 405 } 406 } 407 ++$round; 408 } 409 410 return $relationPolygons; 411 } 412 413 414 415 public function write(Geometry $geometry) 416 { 417 418 $this->processGeometry($geometry); 419 420 $osm = "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' upload='false' generator='geoPHP'>\n"; 421 foreach ($this->nodes as $latlon => $node) { 422 $latlon = explode('_', $latlon); 423 $osm .= " <node id='{$node['id']}' visible='true' lat='$latlon[0]' lon='$latlon[1]' />\n"; 424 } 425 foreach ($this->ways as $wayId => $way) { 426 $osm .= " <way id='{$wayId}' visible='true'>\n"; 427 foreach ($way as $nodeId) { 428 $osm .= " <nd ref='{$nodeId}' />\n"; 429 } 430 $osm .= " </way>\n"; 431 } 432 433 $osm .= "</osm>"; 434 return $osm; 435 } 436 437 /** 438 * @param Geometry $geometry 439 */ 440 protected function processGeometry($geometry) 441 { 442 if (!$geometry->isEmpty()) { 443 switch ($geometry->geometryType()) { 444 case Geometry::POINT: 445 /** @var Point $geometry */ 446 $this->processPoint($geometry); 447 break; 448 case Geometry::LINE_STRING: 449 /** @var LineString $geometry */ 450 $this->processLineString($geometry); 451 break; 452 case Geometry::POLYGON: 453 /** @var Polygon $geometry */ 454 $this->processPolygon($geometry); 455 break; 456 case Geometry::MULTI_POINT: 457 case Geometry::MULTI_LINE_STRING: 458 case Geometry::MULTI_POLYGON: 459 case Geometry::GEOMETRY_COLLECTION: 460 /** @var Collection $geometry */ 461 $this->processCollection($geometry); 462 break; 463 } 464 } 465 } 466 467 /** 468 * @param Point $point 469 * @param bool|false $isWayPoint 470 * @return int 471 */ 472 protected function processPoint($point, $isWayPoint = false) 473 { 474 $nodePosition = sprintf(self::OSM_COORDINATE_PRECISION . '_' . self::OSM_COORDINATE_PRECISION, $point->y(), $point->x()); 475 if (!isset($this->nodes[$nodePosition])) { 476 $this->nodes[$nodePosition] = ['id' => --$this->idCounter, "used" => $isWayPoint]; 477 return $this->idCounter; 478 } else { 479 if ($isWayPoint) { 480 $this->nodes[$nodePosition]['used'] = true; 481 } 482 return $this->nodes[$nodePosition]['id']; 483 } 484 } 485 486 /** 487 * @param LineString $line 488 */ 489 protected function processLineString($line) 490 { 491 $nodes = []; 492 foreach ($line->getPoints() as $point) { 493 $nodes[] = $this->processPoint($point, true); 494 } 495 $this->ways[--$this->idCounter] = $nodes; 496 } 497 498 /** 499 * @param Polygon $polygon 500 */ 501 protected function processPolygon($polygon) 502 { 503 // TODO: Support interior rings 504 $this->processLineString($polygon->exteriorRing()); 505 } 506 507 /** 508 * @param Collection $collection 509 */ 510 protected function processCollection($collection) 511 { 512 // TODO: multi geometries should be converted to relations 513 foreach ($collection->getComponents() as $component) { 514 $this->processGeometry($component); 515 } 516 } 517 518 public static function downloadFromOSMByBbox($left, $bottom, $right, $top) 519 { 520 /** @noinspection PhpUnusedParameterInspection */ 521 set_error_handler( 522 function ($errNO, $errStr, $errFile, $errLine, $errContext) { 523 if (isset($errContext['http_response_header'])) { 524 foreach ($errContext['http_response_header'] as $line) { 525 if (strpos($line, 'Error: ') > -1) { 526 throw new \Exception($line); 527 } 528 } 529 } 530 throw new \Exception('unknown error'); 531 }, 532 E_WARNING 533 ); 534 535 try { 536 $osmFile = file_get_contents(self::OSM_API_URL . "map?bbox={$left},{$bottom},{$right},{$top}"); 537 restore_error_handler(); 538 return $osmFile; 539 } catch (\Exception $e) { 540 restore_error_handler(); 541 throw new \Exception("Failed to download from OSM. " . $e->getMessage()); 542 } 543 } 544} 545