Passed
Pull Request — master (#4)
by Mark
01:50
created

OSM::geomFromXML()   F

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 154
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 46
eloc 93
nc 17732932
nop 0
dl 0
loc 154
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 */
11
namespace geoPHP\Adapter;
12
13
use geoPHP\Geometry\Collection;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use geoPHP\Geometry\Geometry;
15
use geoPHP\Geometry\GeometryCollection;
16
use geoPHP\Geometry\Point;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use geoPHP\Geometry\MultiPoint;
18
use geoPHP\Geometry\LineString;
19
use geoPHP\Geometry\MultiLineString;
20
use geoPHP\Geometry\Polygon;
21
use 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
 */
29
class 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) {
0 ignored issues
show
introduced by
The condition $xmlobj === false is always false.
Loading history...
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;
0 ignored issues
show
Bug introduced by
The method getNamedItem() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

78
            $lat = $node->attributes->/** @scrutinizer ignore-call */ getNamedItem('lat')->nodeValue;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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);
0 ignored issues
show
Bug introduced by
The method getNamedItem() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

103
            $id = intval($way->attributes->/** @scrutinizer ignore-call */ getNamedItem('id')->nodeValue);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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 '&nbsp; &nbsp;';
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++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
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