Passed
Branch master (403190)
by Swen
03:38
created

OSM::processPoint()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 11
rs 10
cc 3
nc 3
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
     * @staticvar array<string> $polygonalTypes
172
     * @staticvar array<string> $linearTypes
173
     * @param array<array> $nodes
174
     * @param array<array> $ways
175
     * @return Geometry[]|array{} geometries
176
     */
177
    private function parseRelations(array &$nodes, array &$ways): array
178
    {
179
        $geometries = [];
180
        
181
        /** @var \DOMElement $relation */
182
        foreach ($this->xmlObj->getElementsByTagName('relation') as $relation) {
183
            
184
            // Collect relation members
185
            list($relationPoints, $relationLines, $relationPolygons) = $this->parseRelationMembers($relation, $nodes, $ways);
186
187
            // Assemble relation geometries
188
            $geometryCollection = [];
189
            if (!empty($relationPolygons)) {
190
                $geometryCollection[] = count($relationPolygons) === 1 ? $relationPolygons[0] : new MultiPolygon($relationPolygons);
191
            }
192
            if (!empty($relationLines)) {
193
                $geometryCollection[] = count($relationLines) === 1 ? $relationLines[0] : new MultiLineString($relationLines);
194
            }
195
            if (!empty($relationPoints)) {
196
                $geometryCollection[] = count($relationPoints) === 1 ? $relationPoints[0] : new MultiPoint($relationPoints);
197
            }
198
199
            if (!empty($geometryCollection)) {
200
                $geometries[] = count($geometryCollection) === 1 ? $geometryCollection[0] : new GeometryCollection($geometryCollection);
201
            }
202
        }
203
        
204
        return $geometries;
205
    }
206
    
207
    /**
208
     * 
209
     * @param \DOMElement $relation
210
     * @param array<array> $nodes
211
     * @param array<array> $ways
212
     * @return array<array>
213
     */
214
    private function parseRelationMembers(\DOMElement $relation, array &$nodes, array &$ways): array
215
    {
216
        /** @var Point[] $relationPoints */
217
        $relationPoints = [];
218
        
219
        /** @var array[] $relationWays */
220
        $relationWays = [];
221
        
222
        /** @var Polygon[] $relationPolygons */
223
        $relationPolygons = [];
224
        
225
        /** @var LineString[] $relationLines */
226
        $relationLines = [];
227
        
228
        // walk the members
229
        foreach ($relation->getElementsByTagName('member') as $member) {
230
            $memberType = $member->attributes->getNamedItem('type')->nodeValue;
231
            $ref = $member->attributes->getNamedItem('ref')->nodeValue;
232
233
            if ($memberType === 'node' && isset($nodes[$ref])) {
234
                $nodes[$ref]['assigned'] = true;
235
                $relationPoints[] = $nodes[$ref]['point'];
236
            }
237
            if ($memberType === 'way' && isset($ways[$ref])) {
238
                $ways[$ref]['assigned'] = true;
239
                $relationWays[$ref] = $ways[$ref]['nodes'];
240
            }
241
        }
242
        
243
        $relationType = $this->getRelationType($relation);
244
        
245
        // add polygons
246
        static $polygonalTypes = ['multipolygon', 'boundary'];
247
        if (in_array($relationType, $polygonalTypes)) {
248
            $relationPolygons = $this->processMultipolygon($relationWays, $nodes);
249
        }
250
251
        // add lines
252
        static $linearTypes = ['route', 'waterway'];
253
        if (in_array($relationType, $linearTypes)) {
254
            $relationLines = $this->processRoutes($relationWays, $nodes);
255
        }
256
            
257
        return [$relationPoints, $relationLines, $relationPolygons];
258
    }
259
    
260
    /**
261
     * 
262
     * @param \DOMElement $relation
263
     * @return string|null
264
     */
265
    private function getRelationType(\DOMElement $relation)
266
    {
267
        foreach ($relation->getElementsByTagName('tag') as $tag) {
268
            if ($tag->attributes->getNamedItem('k')->nodeValue === 'type') {
269
                return $tag->attributes->getNamedItem('v')->nodeValue;
270
            }
271
        }
272
        
273
        return null;
274
    }
275
    
276
    /**
277
     * @staticvar array $polygonalTypes
278
     * @staticvar array $linearTypes
279
     * @return Geometry
280
     */
281
    protected function geomFromXML(): Geometry
282
    {
283
        // Processing OSM Nodes
284
        $nodes = $this->parseNodes();
285
        if (empty($nodes)) {
286
            return new GeometryCollection();
287
        }
288
289
        // Processing OSM Ways
290
        $ways = $this->parseWays($nodes);
291
292
        // Processing OSM Relations
293
        $geometries = $this->parseRelations($nodes, $ways);
294
295
        // add way-geometries
296
        foreach ($ways as $way) {
297
            if ((!$way['assigned'] || !empty($way['tags'])) &&
298
                    !isset($way['tags']['boundary']) &&
299
                    (!isset($way['tags']['natural']) || $way['tags']['natural'] !== 'mountain_range')
300
            ) {
301
                $linePoints = [];
302
                foreach ($way['nodes'] as $wayNode) {
303
                    $linePoints[] = $nodes[$wayNode]['point'];
304
                }
305
                $line = new LineString($linePoints);
306
                if ($way['isRing']) {
307
                    $polygon = new Polygon([$line]);
308
                    if ($polygon->isSimple()) {
309
                        $geometries[] = $polygon;
310
                    } else {
311
                        $geometries[] = $line;
312
                    }
313
                } else {
314
                    $geometries[] = $line;
315
                }
316
            }
317
        }
318
319
        // add node-geometries
320
        foreach ($nodes as $node) {
321
            if (!$node['assigned'] || !empty($node['tags'])) {
322
                $geometries[] = $node['point'];
323
            }
324
        }
325
326
        return count($geometries) === 1 ? $geometries[0] : new GeometryCollection($geometries);
327
    }
328
329
    /**
330
     * @param array<array> $relationWays
331
     * @param array<array> $nodes
332
     * @return LineString[] $lineStrings
333
     */
334
    protected function processRoutes(array &$relationWays, array $nodes): array
335
    {
336
        // Construct lines
337
        /** @var LineString[] $lineStrings */
338
        $lineStrings = [];
339
        while (count($relationWays) > 0) {
340
            $line = array_shift($relationWays);
341
            if ($line[0] !== $line[count($line) - 1]) {
342
                do {
343
                    $waysAdded = 0;
344
                    foreach ($relationWays as $id => $wayNodes) {
345
                        // Last node of ring = first node of way => put way to the end of ring
346
                        if ($line[count($line) - 1] === $wayNodes[0]) {
347
                            $line = array_merge($line, array_slice($wayNodes, 1));
348
                            unset($relationWays[$id]);
349
                            ++$waysAdded;
350
                            // Last node of ring = last node of way => reverse way and put to the end of ring
351
                        } elseif ($line[count($line) - 1] === $wayNodes[count($wayNodes) - 1]) {
352
                            $line = array_merge($line, array_slice(array_reverse($wayNodes), 1));
353
                            unset($relationWays[$id]);
354
                            ++$waysAdded;
355
                            // First node of ring = last node of way => put way to the beginning of ring
356
                        } elseif ($line[0] === $wayNodes[count($wayNodes) - 1]) {
357
                            $line = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $line);
358
                            unset($relationWays[$id]);
359
                            ++$waysAdded;
360
                            // First node of ring = first node of way => reverse way and put to the beginning of ring
361
                        } elseif ($line[0] === $wayNodes[0]) {
362
                            $line = array_merge(array_reverse(array_slice($wayNodes, 1)), $line);
363
                            unset($relationWays[$id]);
364
                            ++$waysAdded;
365
                        }
366
                    }
367
                    // If line members are not ordered, we need to repeat end matching some times
368
                } while ($waysAdded > 0);
369
            }
370
371
            // Create the new LineString
372
            $linePoints = [];
373
            foreach ($line as $lineNode) {
374
                $linePoints[] = $nodes[$lineNode]['point'];
375
            }
376
            $lineStrings[] = new LineString($linePoints);
377
        }
378
379
        return $lineStrings;
380
    }
381
382
    /**
383
     * @param array<array> $relationWays
384
     * @param array<array> $nodes
385
     * @return Polygon[] $relationPolygons
386
     */
387
    protected function processMultipolygon(array &$relationWays, array $nodes): array
388
    {
389
        // create polygons
390
        $rings = $this->constructRings($relationWays, $nodes);
391
        
392
        // Calculate containment
393
        $containment = array_fill(0, count($rings), array_fill(0, count($rings), false));
394
        foreach ($rings as $i => $ring) {
395
            foreach ($rings as $j => $ring2) {
396
                if ($i !== $j && $ring->contains($ring2)) {
397
                    $containment[$i][$j] = true;
398
                }
399
            }
400
        }
401
        $containmentCount = count($containment);
402
403
        /*
404
          print '&nbsp; &nbsp;';
405
          for($i=0; $i<count($rings); $i++) {
406
          print $rings[$i]->getNumberOfPoints() . ' ';
407
          }
408
          print "<br>";
409
          for($i=0; $i<count($rings); $i++) {
410
          print $rings[$i]->getNumberOfPoints() . ' ';
411
          for($j=0; $j<count($rings); $j++) {
412
          print ($containment[$i][$j] ? '1' : '0') . ' ';
413
          }
414
          print "<br>";
415
        }*/
416
417
        // Group rings (outers and inners)
418
419
        /** @var boolean[] $found */
420
        $found = array_fill(0, $containmentCount, false);
421
        $foundCount = 0;
422
        $round = 0;
423
        /** @var int[][] $polygonsRingIds */
424
        $polygonsRingIds = [];
425
        /** @var Polygon[] $relationPolygons */
426
        $relationPolygons = [];
427
        while ($foundCount < $containmentCount && $round < 100) {
428
            $ringsFound = [];
429
            for ($i = 0; $i < $containmentCount; $i++) {
430
                if ($found[$i]) {
431
                    continue;
432
                }
433
                $containCount = 0;
434
                $numCurContainment = count($containment[$i]);
435
                for ($j = 0; $j < $numCurContainment; $j++) {
436
                    if (!$found[$j]) {
437
                        $containCount += $containment[$j][$i];
438
                    }
439
                }
440
                if ($containCount === 0) {
441
                    $ringsFound[] = $i;
442
                }
443
            }
444
            if ($round % 2 === 0) {
445
                $polygonsRingIds = [];
446
            }
447
            foreach ($ringsFound as $ringId) {
448
                $found[$ringId] = true;
449
                ++$foundCount;
450
                if ($round % 2 === 1) {
451
                    foreach ($polygonsRingIds as $outerId => $polygon) {
452
                        if ($containment[$outerId][$ringId]) {
453
                            $polygonsRingIds[$outerId][] = $ringId;
454
                        }
455
                    }
456
                } else {
457
                    $polygonsRingIds[$ringId] = [0 => $ringId];
458
                }
459
            }
460
            if ($round % 2 === 1 || $foundCount === $containmentCount) {
461
                foreach ($polygonsRingIds as $k => $ringGroup) {
462
                    $linearRings = [];
463
                    foreach ($ringGroup as $polygonRing) {
464
                        $linearRings[] = $rings[$polygonRing]->exteriorRing();
465
                    }
466
                    $relationPolygons[] = new Polygon($linearRings);
467
                }
468
            }
469
            ++$round;
470
        }
471
472
        return $relationPolygons;
473
    }
474
    
475
    /**
476
     * @TODO: what to do with broken rings? 
477
     * I propose to force-close if start -> end point distance is less then 10% of line length, otherwise drop it.
478
     * But if dropped, its inner ring will be outers, which is not good.
479
     * We should save the role for each ring (outer, inner, mixed) during ring creation and check it during ring grouping
480
     * 
481
     * @param array<array> $relationWays
482
     * @param array<array> $nodes
483
     * @return Polygon[]
484
     */
485
    private function constructRings(array &$relationWays, array $nodes): array
486
    {
487
        /** @var Polygon[] $rings */
488
        $rings = [];
489
        while (!empty($relationWays)) {
490
            $ring = array_shift($relationWays);
491
            if ($ring[0] !== $ring[count($ring) - 1]) {
492
                do {
493
                    $waysAdded = 0;
494
                    foreach ($relationWays as $id => $wayNodes) {
495
                        // Last node of ring = first node of way => put way to the end of ring
496
                        if ($ring[count($ring) - 1] === $wayNodes[0]) {
497
                            $ring = array_merge($ring, array_slice($wayNodes, 1));
498
                            unset($relationWays[$id]);
499
                            ++$waysAdded;
500
                            // Last node of ring = last node of way => reverse way and put to the end of ring
501
                        } elseif ($ring[count($ring) - 1] === $wayNodes[count($wayNodes) - 1]) {
502
                            $ring = array_merge($ring, array_slice(array_reverse($wayNodes), 1));
503
                            unset($relationWays[$id]);
504
                            ++$waysAdded;
505
                            // First node of ring = last node of way => put way to the beginning of ring
506
                        } elseif ($ring[0] === $wayNodes[count($wayNodes) - 1]) {
507
                            $ring = array_merge(array_slice($wayNodes, 0, count($wayNodes) - 1), $ring);
508
                            unset($relationWays[$id]);
509
                            ++$waysAdded;
510
                            // First node of ring = first node of way => reverse way and put to the beginning of ring
511
                        } elseif ($ring[0] === $wayNodes[0]) {
512
                            $ring = array_merge(array_reverse(array_slice($wayNodes, 1)), $ring);
513
                            unset($relationWays[$id]);
514
                            ++$waysAdded;
515
                        }
516
                    }
517
                    // If ring members are not ordered, we need to repeat end matching some times
518
                } while ($waysAdded > 0 && $ring[0] !== $ring[count($ring) - 1]);
519
            }
520
521
            // Create the new Polygon
522
            if ($ring[0] === $ring[count($ring) - 1]) {
523
                $ringPoints = [];
524
                foreach ($ring as $ringNode) {
525
                    $ringPoints[] = $nodes[$ringNode]['point'];
526
                }
527
                $newPolygon = new Polygon([new LineString($ringPoints)]);
528
                if ($newPolygon->isSimple()) {
529
                    $rings[] = $newPolygon;
530
                }
531
            }
532
        }
533
        
534
        return $rings;
535
    }
536
537
    /**
538
     * @param Geometry $geometry
539
     * @return string
540
     */
541
    public function write(Geometry $geometry): string
542
    {
543
        $this->processGeometry($geometry);
544
545
        $osm = "<?xml version='1.0' encoding='UTF-8'?>\n<osm version='0.6' upload='false' generator='geoPHP'>\n";
546
        foreach ($this->nodes as $latlon => $node) {
547
            $latlon = explode('_', $latlon);
548
            $osm .= "  <node id='{$node['id']}' visible='true' lat='$latlon[0]' lon='$latlon[1]' />\n";
549
        }
550
        foreach ($this->ways as $wayId => $way) {
551
            $osm .= "  <way id='{$wayId}' visible='true'>\n";
552
            foreach ($way as $nodeId) {
553
                $osm .= "    <nd ref='{$nodeId}' />\n";
554
            }
555
            $osm .= "  </way>\n";
556
        }
557
558
        $osm .= "</osm>";
559
        
560
        return $osm;
561
    }
562
563
    /**
564
     * @param Geometry $geometry
565
     * @return void
566
     */
567
    protected function processGeometry($geometry)
568
    {
569
        if (!$geometry->isEmpty()) {
570
            switch ($geometry->geometryType()) {
571
                case Geometry::POINT:
572
                    /** @var Point $geometry */
573
                    $this->processPoint($geometry);
574
                    break;
575
                case Geometry::LINESTRING:
576
                    /** @var LineString $geometry */
577
                    $this->processLineString($geometry);
578
                    break;
579
                case Geometry::POLYGON:
580
                    /** @var Polygon $geometry */
581
                    $this->processPolygon($geometry);
582
                    break;
583
                case Geometry::MULTI_POINT:
584
                case Geometry::MULTI_LINESTRING:
585
                case Geometry::MULTI_POLYGON:
586
                case Geometry::GEOMETRY_COLLECTION:
587
                    /** @var Collection $geometry */
588
                    $this->processCollection($geometry);
589
                    break;
590
            }
591
        }
592
    }
593
594
    /**
595
     * @param Point $point
596
     * @param bool|false $isWayPoint
597
     * @return int
598
     */
599
    protected function processPoint($point, $isWayPoint = false)
600
    {
601
        $nodePosition = sprintf(self::OSM_COORDINATE_PRECISION . '_' . self::OSM_COORDINATE_PRECISION, $point->getY(), $point->getX());
602
        if (!isset($this->nodes[$nodePosition])) {
603
            $this->nodes[$nodePosition] = ['id' => --$this->idCounter, "used" => $isWayPoint];
604
            return $this->idCounter;
605
        } else {
606
            if ($isWayPoint) {
607
                $this->nodes[$nodePosition]['used'] = true;
608
            }
609
            return $this->nodes[$nodePosition]['id'];
610
        }
611
    }
612
613
    /**
614
     * @param LineString $line
615
     * @return void
616
     */
617
    protected function processLineString($line)
618
    {
619
        $nodes = [];
620
        foreach ($line->getPoints() as $point) {
621
            $nodes[] = $this->processPoint($point, true);
622
        }
623
        $this->ways[--$this->idCounter] = $nodes;
624
    }
625
626
    /**
627
     * @param Polygon $polygon
628
     * @return void
629
     */
630
    protected function processPolygon($polygon)
631
    {
632
        // TODO: Support interior rings
633
        $this->processLineString($polygon->exteriorRing());
634
    }
635
636
    /**
637
     * @param Collection $collection
638
     * @return void
639
     */
640
    protected function processCollection($collection)
641
    {
642
        // TODO: multi geometries should be converted to relations
643
        foreach ($collection->getComponents() as $component) {
644
            $this->processGeometry($component);
645
        }
646
    }
647
648
    /**
649
     *
650
     * @param int|float $left
651
     * @param int|float $bottom
652
     * @param int|float $right
653
     * @param int|float $top
654
     * @return string|false
655
     * @throws \Exception
656
     */
657
    public static function downloadFromOSMByBbox($left, $bottom, $right, $top)
658
    {
659
        /** @noinspection PhpUnusedParameterInspection */
660
        set_error_handler(
661
            function ($errNO, $errStr, $errFile, $errLine, $errContext) {
662
                if (isset($errContext['http_response_header'])) {
663
                    foreach ($errContext['http_response_header'] as $line) {
664
                        if (strpos($line, 'Error: ') > -1) {
665
                              throw new \Exception($line);
666
                        }
667
                    }
668
                }
669
                throw new \Exception('unknown error');
670
            },
671
            E_WARNING
672
        );
673
674
        try {
675
            $osmFile = file_get_contents(self::OSM_API_URL . "map?bbox={$left},{$bottom},{$right},{$top}");
676
            restore_error_handler();
677
            return $osmFile;
678
        } catch (\Exception $e) {
679
            restore_error_handler();
680
            throw new \Exception("Failed to download from OSM. " . $e->getMessage());
681
        }
682
    }
683
}
684