OSM::processMultipolygon()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 7
c 3
b 0
f 0
dl 0
loc 32
rs 9.6111
cc 5
nc 4
nop 2
1
<?php
2
3
/*
4
 * @author Báthory Péter <[email protected]>
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
12
namespace geoPHP\Adapter;
13
14
use geoPHP\Geometry\Collection;
15
use geoPHP\Geometry\Geometry;
16
use geoPHP\Geometry\GeometryCollection;
17
use geoPHP\Geometry\Point;
18
use geoPHP\Geometry\MultiPoint;
19
use geoPHP\Geometry\LineString;
20
use geoPHP\Geometry\MultiLineString;
21
use geoPHP\Geometry\Polygon;
22
use geoPHP\Geometry\MultiPolygon;
23
24
/**
25
 * PHP Geometry <-> OpenStreetMap XML encoder/decoder
26
 *
27
 * This adapter is not ready yet. It lacks a relation writer, and the reader has problems with invalid multipolygons
28
 * Since geoPHP doesn't support metadata, it cannot read and write OSM tags.
29
 */
30
class OSM implements GeoAdapter
31
{
32
33
    const OSM_COORDINATE_PRECISION = '%.7f';
34
    const OSM_API_URL = 'http://openstreetmap.org/api/0.6/';
35
36
    /**
37
     * @var \DOMDocument
38
     */
39
    protected $xmlObj;
40
    
41
    /**
42
     * @var array<mixed>
43
     */
44
    protected $nodes = [];
45
    
46
    /**
47
     * @var array<int, array>
48
     */
49
    protected $ways = [];
50
    
51
    /**
52
     * @var int
53
     */
54
    protected $idCounter = 0;
55
56
    /**
57
     * Read OpenStreetMap XML string into geometry objects
58
     *
59
     * @param string $osm An OSM XML string
60
     * @return Geometry|GeometryCollection
61
     * @throws \Exception
62
     */
63
    public function read(string $osm): Geometry
64
    {
65
        // Load into DOMDocument
66
        $xmlobj = new \DOMDocument();
67
        
68
        if ($xmlobj->loadXML($osm) === false) {
69
            throw new \Exception("Invalid OSM XML: " . substr($osm, 0, 100));
70
        }
71
72
        $this->xmlObj = $xmlobj;
73
        try {
74
            $geom = $this->geomFromXML();
75
        } catch (\Exception $e) {
76
            throw new \Exception("Cannot read geometries from OSM XML: " . $e->getMessage());
77
        }
78
79
        return $geom;
80
    }
81
    
82
    /**
83
     *
84
     * @return array<array>|array{} nodes
85
     */
86
    private function parseNodes(): array
87
    {
88
        $nodes = [];
89
        $nodeId = 0;
90
        foreach ($this->xmlObj->getElementsByTagName('node') as $node) {
91
            $lat = null;
92
            $lon = null;
93
            
94
            /** @var \DOMElement $node */
95
            if ($node->attributes !== null) {
96
                $lat = $node->attributes->getNamedItem('lat')->nodeValue;
97
                $lon = $node->attributes->getNamedItem('lon')->nodeValue;
98
                $nodeId = intval($node->attributes->getNamedItem('id')->nodeValue);
99
            } else {
100
                ++$nodeId;
101
            }
102
            
103
            $tags = [];
104
            foreach ($node->getElementsByTagName('tag') as $tag) {
105
                $key = $tag->attributes->getNamedItem('k')->nodeValue;
106
                if ($key === 'source' || $key === 'fixme' || $key === 'created_by') {
107
                    continue;
108
                }
109
                $tags[$key] = $tag->attributes->getNamedItem('v')->nodeValue;
110
            }
111
            $nodes[$nodeId] = [
112
                'point' => new Point($lon, $lat),
113
                'assigned' => false,
114
                'tags' => $tags
115
            ];
116
        }
117
        
118
        return $nodes;
119
    }
120
121
    /**
122
     *
123
     * @param array<array> $nodes
124
     * @return array<array>|array{} ways
125
     */
126
    private function parseWays(array &$nodes): array
127
    {
128
        $ways = [];
129
        $wayId = 0;
130
        foreach ($this->xmlObj->getElementsByTagName('way') as $way) {
131
            /** @var \DOMElement $way */
132
            if ($way->attributes !== null) {
133
                $wayId = intval($way->attributes->getNamedItem('id')->nodeValue);
134
            } else {
135
                ++$wayId;
136
            }
137
            $wayNodes = [];
138
            foreach ($way->getElementsByTagName('nd') as $node) {
139
                if ($node->attributes === null) {
140
                    continue;
141
                }
142
                $ref = intval($node->attributes->getNamedItem('ref')->nodeValue);
143
                if (isset($nodes[$ref])) {
144
                    $nodes[$ref]['assigned'] = true;
145
                    $wayNodes[] = $ref;
146
                }
147
            }
148
            $tags = [];
149
            foreach ($way->getElementsByTagName('tag') as $tag) {
150
                $key = $tag->attributes->getNamedItem('k')->nodeValue;
151
                if ($key === 'source' || $key === 'fixme' || $key === 'created_by') {
152
                    continue;
153
                }
154
                $tags[$key] = $tag->attributes->getNamedItem('v')->nodeValue;
155
            }
156
            if (count($wayNodes) >= 2) {
157
                $ways[$wayId] = [
158
                    'nodes' => $wayNodes,
159
                    'assigned' => false,
160
                    'tags' => $tags,
161
                    'isRing' => ($wayNodes[0] === $wayNodes[count($wayNodes) - 1])
162
                ];
163
            }
164
        }
165
        
166
        return $ways;
167
    }
168
    
169
    /**
170
     *
171
     * @param array<array> $nodes
172
     * @param array<array> $ways
173
     * @return Geometry[]|array{} geometries
174
     */
175
    private function parseRelations(array &$nodes, array &$ways): array
176
    {
177
        $geometries = [];
178
        
179
        /** @var \DOMElement $relation */
180
        foreach ($this->xmlObj->getElementsByTagName('relation') as $relation) {
181
            // Collect relation members
182
            list($relationPoints, $relationLines, $relationPolygons) = $this->parseRelationMembers($relation, $nodes, $ways);
183
184
            // Assemble relation geometries
185
            $geometryCollection = [];
186
            if (!empty($relationPolygons)) {
187
                $geometryCollection[] = count($relationPolygons) === 1 ? $relationPolygons[0] : new MultiPolygon($relationPolygons);
188
            }
189
            if (!empty($relationLines)) {
190
                $geometryCollection[] = count($relationLines) === 1 ? $relationLines[0] : new MultiLineString($relationLines);
191
            }
192
            if (!empty($relationPoints)) {
193
                $geometryCollection[] = count($relationPoints) === 1 ? $relationPoints[0] : new MultiPoint($relationPoints);
194
            }
195
196
            if (!empty($geometryCollection)) {
197
                $geometries[] = count($geometryCollection) === 1 ? $geometryCollection[0] : new GeometryCollection($geometryCollection);
198
            }
199
        }
200
        
201
        return $geometries;
202
    }
203
    
204
    /**
205
     *
206
     * @staticvar array<string> $polygonalTypes
207
     * @staticvar array<string> $linearTypes
208
     * @param \DOMElement $relation
209
     * @param array<array> $nodes
210
     * @param array<array> $ways
211
     * @return array<array>
212
     */
213
    private function parseRelationMembers(\DOMElement $relation, array &$nodes, array &$ways): array
214
    {
215
        /** @var Point[] $relationPoints */
216
        $relationPoints = [];
217
        
218
        /** @var array[] $relationWays */
219
        $relationWays = [];
220
        
221
        /** @var Polygon[] $relationPolygons */
222
        $relationPolygons = [];
223
        
224
        /** @var LineString[] $relationLines */
225
        $relationLines = [];
226
        
227
        // walk the members
228
        foreach ($relation->getElementsByTagName('member') as $member) {
229
            $memberType = $member->attributes->getNamedItem('type')->nodeValue;
230
            $ref = $member->attributes->getNamedItem('ref')->nodeValue;
231
232
            if ($memberType === 'node' && isset($nodes[$ref])) {
233
                $nodes[$ref]['assigned'] = true;
234
                $relationPoints[] = $nodes[$ref]['point'];
235
            }
236
            if ($memberType === 'way' && isset($ways[$ref])) {
237
                $ways[$ref]['assigned'] = true;
238
                $relationWays[$ref] = $ways[$ref]['nodes'];
239
            }
240
        }
241
        
242
        $relationType = $this->getRelationType($relation);
243
        
244
        // add polygons
245
        static $polygonalTypes = ['multipolygon', 'boundary'];
246
        if (in_array($relationType, $polygonalTypes)) {
247
            $relationPolygons = $this->processMultipolygon($relationWays, $nodes);
248
        }
249
250
        // add lines
251
        static $linearTypes = ['route', 'waterway'];
252
        if (in_array($relationType, $linearTypes)) {
253
            $relationLines = $this->processRoutes($relationWays, $nodes);
254
        }
255
            
256
        return [$relationPoints, $relationLines, $relationPolygons];
257
    }
258
    
259
    /**
260
     *
261
     * @param \DOMElement $relation
262
     * @return string|null
263
     */
264
    private function getRelationType(\DOMElement $relation)
265
    {
266
        foreach ($relation->getElementsByTagName('tag') as $tag) {
267
            if ($tag->attributes->getNamedItem('k')->nodeValue === 'type') {
268
                return $tag->attributes->getNamedItem('v')->nodeValue;
269
            }
270
        }
271
        
272
        return null;
273
    }
274
    
275
    /**
276
     * @staticvar array $polygonalTypes
277
     * @staticvar array $linearTypes
278
     * @return Geometry
279
     */
280
    protected function geomFromXML(): Geometry
281
    {
282
        // Processing OSM Nodes
283
        $nodes = $this->parseNodes();
284
        if (empty($nodes)) {
285
            return new GeometryCollection();
286
        }
287
288
        // Processing OSM Ways
289
        $ways = $this->parseWays($nodes);
290
291
        // Processing OSM Relations
292
        $geometries = $this->parseRelations($nodes, $ways);
293
294
        // add way-geometries
295
        foreach ($ways as $way) {
296
            if ((!$way['assigned'] || !empty($way['tags'])) &&
297
                    !isset($way['tags']['boundary']) &&
298
                    (!isset($way['tags']['natural']) || $way['tags']['natural'] !== 'mountain_range')
299
            ) {
300
                $linePoints = [];
301
                foreach ($way['nodes'] as $wayNode) {
302
                    $linePoints[] = $nodes[$wayNode]['point'];
303
                }
304
                $line = new LineString($linePoints);
305
                if ($way['isRing']) {
306
                    $polygon = new Polygon([$line]);
307
                    if ($polygon->isSimple()) {
308
                        $geometries[] = $polygon;
309
                    } else {
310
                        $geometries[] = $line;
311
                    }
312
                } else {
313
                    $geometries[] = $line;
314
                }
315
            }
316
        }
317
318
        // add node-geometries
319
        foreach ($nodes as $node) {
320
            if (!$node['assigned'] || !empty($node['tags'])) {
321
                $geometries[] = $node['point'];
322
            }
323
        }
324
325
        return count($geometries) === 1 ? $geometries[0] : new GeometryCollection($geometries);
326
    }
327
328
    /**
329
     * @param array<array> $relationWays
330
     * @param array<array> $nodes
331
     * @return LineString[] $lineStrings
332
     */
333
    protected function processRoutes(array &$relationWays, array $nodes): array
334
    {
335
        // Construct lines
336
        /** @var LineString[] $lineStrings */
337
        $lineStrings = [];
338
        while (count($relationWays) > 0) {
339
            $line = array_shift($relationWays);
340
            if ($line[0] !== $line[count($line) - 1]) {
341
                do {
342
                    $waysAdded = 0;
343
                    foreach ($relationWays as $id => $wayNodes) {
344
                        // Last node of ring = first node of way => put way to the end of ring
345
                        if ($line[count($line) - 1] === $wayNodes[0]) {
346
                            $line = array_merge($line, array_slice($wayNodes, 1));
347
                            unset($relationWays[$id]);
348
                            ++$waysAdded;
349
                            // Last node of ring = last node of way => reverse way and put to the end of ring
350
                        } elseif ($line[count($line) - 1] === $wayNodes[count($wayNodes) - 1]) {
351
                            $line = array_merge($line, array_slice(array_reverse($wayNodes), 1));
352
                            unset($relationWays[$id]);
353
                            ++$waysAdded;
354
                            // First node of ring = last node of way => put way to the beginning of ring
355
                        } elseif ($line[0] === $wayNodes[count($wayNodes) - 1]) {
356
                            $line = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $line);
357
                            unset($relationWays[$id]);
358
                            ++$waysAdded;
359
                            // First node of ring = first node of way => reverse way and put to the beginning of ring
360
                        } elseif ($line[0] === $wayNodes[0]) {
361
                            $line = array_merge(array_reverse(array_slice($wayNodes, 1)), $line);
362
                            unset($relationWays[$id]);
363
                            ++$waysAdded;
364
                        }
365
                    }
366
                    // If line members are not ordered, we need to repeat end matching some times
367
                } while ($waysAdded > 0);
368
            }
369
370
            // Create the new LineString
371
            $linePoints = [];
372
            foreach ($line as $lineNode) {
373
                $linePoints[] = $nodes[$lineNode]['point'];
374
            }
375
            $lineStrings[] = new LineString($linePoints);
376
        }
377
378
        return $lineStrings;
379
    }
380
381
    /**
382
     * @param array<array> $relationWays
383
     * @param array<array> $nodes
384
     * @return Polygon[] $relationPolygons
385
     */
386
    protected function processMultipolygon(array &$relationWays, array $nodes): array
387
    {
388
        // create polygons
389
        $rings = $this->constructRings($relationWays, $nodes);
390
        
391
        // Calculate containment
392
        $containment = array_fill(0, count($rings), array_fill(0, count($rings), false));
393
        foreach ($rings as $i => $ring) {
394
            foreach ($rings as $j => $ring2) {
395
                if ($i !== $j && $ring->contains($ring2)) {
396
                    $containment[$i][$j] = true;
397
                }
398
            }
399
        }
400
        
401
402
        /*
403
          print '&nbsp; &nbsp;';
404
          for($i=0; $i<count($rings); $i++) {
405
          print $rings[$i]->getNumberOfPoints() . ' ';
406
          }
407
          print "<br>";
408
          for($i=0; $i<count($rings); $i++) {
409
          print $rings[$i]->getNumberOfPoints() . ' ';
410
          for($j=0; $j<count($rings); $j++) {
411
          print ($containment[$i][$j] ? '1' : '0') . ' ';
412
          }
413
          print "<br>";
414
        }*/
415
416
        // Group rings (outers and inners)
417
        return $this->createRelationPolygons($containment, $rings);
418
    }
419
    
420
    /**
421
     * Group rings (outers and inners)
422
     *
423
     * @param array<int,array> $containment
424
     * @param Polygon[] $rings
425
     * @return Polygon[]
426
     */
427
    private function createRelationPolygons(array $containment, array $rings)
428
    {
429
        $containmentCount = count($containment);
430
        
431
        /** @var bool[] $found */
432
        $found = array_fill(0, $containmentCount, false);
433
        
434
        /** @var int[][] $polygonsRingIds */
435
        $polygonsRingIds = [];
436
        
437
        /** @var Polygon[] $relationPolygons */
438
        $relationPolygons = [];
439
        
440
        $foundCount = 0;
441
        $round = 0;
442
        while ($foundCount < $containmentCount && $round < 100) {
443
            $ringsFound = [];
444
            for ($i = 0; $i < $containmentCount; $i++) {
445
                if ($found[$i]) {
446
                    continue;
447
                }
448
                $containCount = 0;
449
                $numCurContainment = count($containment[$i]);
450
                for ($j = 0; $j < $numCurContainment; $j++) {
451
                    if (!$found[$j]) {
452
                        $containCount += $containment[$j][$i];
453
                    }
454
                }
455
                if ($containCount === 0) {
456
                    $ringsFound[] = $i;
457
                }
458
            }
459
            if ($round % 2 === 0) {
460
                $polygonsRingIds = [];
461
            }
462
            foreach ($ringsFound as $ringId) {
463
                $found[$ringId] = true;
464
                ++$foundCount;
465
                if ($round % 2 === 1) {
466
                    foreach ($polygonsRingIds as $outerId => $polygon) {
467
                        if ($containment[$outerId][$ringId]) {
468
                            $polygonsRingIds[$outerId][] = $ringId;
469
                        }
470
                    }
471
                } else {
472
                    $polygonsRingIds[$ringId] = [0 => $ringId];
473
                }
474
            }
475
            if ($round % 2 === 1 || $foundCount === $containmentCount) {
476
                foreach ($polygonsRingIds as $k => $ringGroup) {
477
                    $linearRings = [];
478
                    foreach ($ringGroup as $polygonRing) {
479
                        $linearRings[] = $rings[$polygonRing]->exteriorRing();
480
                    }
481
                    $relationPolygons[] = new Polygon($linearRings);
482
                }
483
            }
484
            ++$round;
485
        }
486
        
487
        return $relationPolygons;
488
    }
489
    
490
    /**
491
     * @TODO: what to do with broken rings?
492
     * I propose to force-close if start -> end point distance is less then 10% of line length, otherwise drop it.
493
     * But if dropped, its inner ring will be outers, which is not good.
494
     * We should save the role for each ring (outer, inner, mixed) during ring creation and check it during ring grouping
495
     *
496
     * @param array<array> $relationWays
497
     * @param array<array> $nodes
498
     * @return Polygon[]
499
     */
500
    private function constructRings(array &$relationWays, array $nodes): array
501
    {
502
        /** @var Polygon[] $rings */
503
        $rings = [];
504
        while (!empty($relationWays)) {
505
            $ring = array_shift($relationWays);
506
            
507
            // ring is not closed
508
            if ($ring[0] !== $ring[count($ring) - 1]) {
509
                $this->closeRing($relationWays, $ring);
510
            }
511
512
            // Create the new Polygon
513
            if ($ring[0] === $ring[count($ring) - 1]) {
514
                $ringPoints = [];
515
                foreach ($ring as $ringNode) {
516
                    $ringPoints[] = $nodes[$ringNode]['point'];
517
                }
518
                $newPolygon = new Polygon([new LineString($ringPoints)]);
519
                if ($newPolygon->isSimple()) {
520
                    $rings[] = $newPolygon;
521
                }
522
            }
523
        }
524
        
525
        return $rings;
526
    }
527
    
528
    /**
529
     *
530
     * @param array<int, array> $relationWays
531
     * @param array<int|float> $ring
532
     * @return void
533
     */
534
    private function closeRing(array &$relationWays, array &$ring)
535
    {
536
        do {
537
            $waysAdded = 0;
538
            foreach ($relationWays as $id => $wayNodes) {
539
                // Last node of ring = first node of way => put way to the end of ring
540
                if ($ring[count($ring) - 1] === $wayNodes[0]) {
541
                    $ring = array_merge($ring, array_slice($wayNodes, 1));
542
                    unset($relationWays[$id]);
543
                    ++$waysAdded;
544
                    // Last node of ring = last node of way => reverse way and put to the end of ring
545
                } elseif ($ring[count($ring) - 1] === $wayNodes[count($wayNodes) - 1]) {
546
                    $ring = array_merge($ring, array_slice(array_reverse($wayNodes), 1));
547
                    unset($relationWays[$id]);
548
                    ++$waysAdded;
549
                    // First node of ring = last node of way => put way to the beginning of ring
550
                } elseif ($ring[0] === $wayNodes[count($wayNodes) - 1]) {
551
                    $ring = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $ring);
552
                    unset($relationWays[$id]);
553
                    ++$waysAdded;
554
                    // First node of ring = first node of way => reverse way and put to the beginning of ring
555
                } elseif ($ring[0] === $wayNodes[0]) {
556
                    $ring = array_merge(array_reverse(array_slice($wayNodes, 1)), $ring);
557
                    unset($relationWays[$id]);
558
                    ++$waysAdded;
559
                }
560
            }
561
            // If ring members are not ordered, we need to repeat end matching some times
562
        } while ($waysAdded > 0 && $ring[0] !== $ring[count($ring) - 1]);
563
    }
564
565
    /**
566
     * @param Geometry $geometry
567
     * @return string
568
     */
569
    public function write(Geometry $geometry): string
570
    {
571
        $this->processGeometry($geometry);
572
573
        $osm = "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' upload='false' generator='geoPHP'>\n";
574
        foreach ($this->nodes as $latlon => $node) {
575
            $latlon = explode('_', $latlon);
576
            $osm .= "  <node id='{$node['id']}' visible='true' lat='$latlon[0]' lon='$latlon[1]' />\n";
577
        }
578
        foreach ($this->ways as $wayId => $way) {
579
            $osm .= "  <way id='{$wayId}' visible='true'>\n";
580
            foreach ($way as $nodeId) {
581
                $osm .= "    <nd ref='{$nodeId}' />\n";
582
            }
583
            $osm .= "  </way>\n";
584
        }
585
586
        $osm .= "</osm>";
587
        
588
        return $osm;
589
    }
590
591
    /**
592
     * @param Geometry $geometry
593
     * @return void
594
     */
595
    protected function processGeometry($geometry)
596
    {
597
        if (!$geometry->isEmpty()) {
598
            switch ($geometry->geometryType()) {
599
                case Geometry::POINT:
600
                    /** @var Point $geometry */
601
                    $this->processPoint($geometry);
602
                    break;
603
                case Geometry::LINESTRING:
604
                    /** @var LineString $geometry */
605
                    $this->processLineString($geometry);
606
                    break;
607
                case Geometry::POLYGON:
608
                    /** @var Polygon $geometry */
609
                    $this->processPolygon($geometry);
610
                    break;
611
                case Geometry::MULTI_POINT:
612
                case Geometry::MULTI_LINESTRING:
613
                case Geometry::MULTI_POLYGON:
614
                case Geometry::GEOMETRY_COLLECTION:
615
                    /** @var Collection $geometry */
616
                    $this->processCollection($geometry);
617
                    break;
618
            }
619
        }
620
    }
621
622
    /**
623
     * @param Point $point
624
     * @param bool|false $isWayPoint
625
     * @return int
626
     */
627
    protected function processPoint($point, $isWayPoint = false)
628
    {
629
        $nodePosition = sprintf(self::OSM_COORDINATE_PRECISION . '_' . self::OSM_COORDINATE_PRECISION, $point->getY(), $point->getX());
630
        if (!isset($this->nodes[$nodePosition])) {
631
            $this->nodes[$nodePosition] = ['id' => --$this->idCounter, "used" => $isWayPoint];
632
            return $this->idCounter;
633
        } else {
634
            if ($isWayPoint) {
635
                $this->nodes[$nodePosition]['used'] = true;
636
            }
637
            return $this->nodes[$nodePosition]['id'];
638
        }
639
    }
640
641
    /**
642
     * @param LineString $line
643
     * @return void
644
     */
645
    protected function processLineString($line)
646
    {
647
        $nodes = [];
648
        foreach ($line->getPoints() as $point) {
649
            $nodes[] = $this->processPoint($point, true);
650
        }
651
        $this->ways[--$this->idCounter] = $nodes;
652
    }
653
654
    /**
655
     * @param Polygon $polygon
656
     * @return void
657
     */
658
    protected function processPolygon($polygon)
659
    {
660
        // TODO: Support interior rings
661
        $this->processLineString($polygon->exteriorRing());
662
    }
663
664
    /**
665
     * @param Collection $collection
666
     * @return void
667
     */
668
    protected function processCollection($collection)
669
    {
670
        // TODO: multi geometries should be converted to relations
671
        foreach ($collection->getComponents() as $component) {
672
            $this->processGeometry($component);
673
        }
674
    }
675
676
    /**
677
     *
678
     * @param int|float $left
679
     * @param int|float $bottom
680
     * @param int|float $right
681
     * @param int|float $top
682
     * @return string|false
683
     * @throws \Exception
684
     */
685
    public static function downloadFromOSMByBbox($left, $bottom, $right, $top)
686
    {
687
        /** @noinspection PhpUnusedParameterInspection */
688
        set_error_handler(
689
            function ($errNO, $errStr, $errFile, $errLine, $errContext) {
690
                if (isset($errContext['http_response_header'])) {
691
                    foreach ($errContext['http_response_header'] as $line) {
692
                        if (strpos($line, 'Error: ') > -1) {
693
                            throw new \Exception($line);
694
                        }
695
                    }
696
                }
697
                throw new \Exception('unknown error');
698
            },
699
            E_WARNING
700
        );
701
702
        try {
703
            $osmFile = file_get_contents(self::OSM_API_URL . "map?bbox={$left},{$bottom},{$right},{$top}");
704
            restore_error_handler();
705
            return $osmFile;
706
        } catch (\Exception $e) {
707
            restore_error_handler();
708
            throw new \Exception("Failed to download from OSM. " . $e->getMessage());
709
        }
710
    }
711
}
712