Passed
Push — master ( 984a75...47ccf4 )
by Swen
06:21 queued 03:03
created

KML::parseGeometryCollection()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 19
rs 9.5555
cc 5
nc 9
nop 1
1
<?php
2
namespace geoPHP\Adapter;
3
4
use geoPHP\Geometry\Collection;
5
use geoPHP\geoPHP;
6
use geoPHP\Geometry\Geometry;
7
use geoPHP\Geometry\GeometryCollection;
8
use geoPHP\Geometry\Point;
9
use geoPHP\Geometry\LineString;
10
use geoPHP\Geometry\Polygon;
11
12
/*
13
 * Copyright (c) Patrick Hayes
14
 * Copyright (c) 2010-2011, Arnaud Renevier
15
 *
16
 * This code is open-source and licenced under the Modified BSD License.
17
 * For the full copyright and license information, please view the LICENSE
18
 * file that was distributed with this source code.
19
 */
20
21
/**
22
 * PHP Geometry/KML encoder/decoder
23
 *
24
 * Mainly inspired/adapted from OpenLayers( http://www.openlayers.org )
25
 */
26
class KML implements GeoAdapter
27
{
28
29
    /**
30
     * @var \DOMDocument
31
     */
32
    protected $xmlObject;
33
    
34
    /**
35
     * @var string Name-space string. eg 'georss:'
36
     */
37
    private $nss = '';
38
39
    /**
40
     * Read KML string into geometry objects
41
     *
42
     * @param string $kml A KML string
43
     * @return Geometry|GeometryCollection
44
     */
45
    public function read(string $kml): Geometry
46
    {
47
        return $this->geomFromText($kml);
48
    }
49
50
    /**
51
     * @param string $text
52
     * @return Geometry|GeometryCollection
53
     * @throws \Exception
54
     */
55
    public function geomFromText(string $text): Geometry
56
    {
57
        // Change to lower-case and strip all CDATA
58
        $text = mb_strtolower($text, mb_detect_encoding($text));
59
        $text = preg_replace('/<!\[cdata\[(.*?)\]\]>/s', '', $text);
60
61
        // Load into DOMDocument
62
        $xmlObject = new \DOMDocument();
63
        if ($xmlObject->loadXML($text) === false) {
64
            throw new \Exception("Invalid KML: " . $text);
65
        }
66
67
        $this->xmlObject = $xmlObject;
68
        try {
69
            $geom = $this->geomFromXML();
70
        } catch (\Exception $e) {
71
            throw new \Exception("Cannot read geometry from KML: " . $text . ' ' . $e->getMessage());
72
        }
73
74
        return $geom;
75
    }
76
77
    /**
78
     * @return Geometry|GeometryCollection
79
     */
80
    protected function geomFromXML(): Geometry
81
    {
82
        $geometries = [];
83
        $placemarkElements = $this->xmlObject->getElementsByTagName('placemark');
84
        
85
        if ($placemarkElements->length) {
86
            foreach ($placemarkElements as $placemark) {
87
                $data = [];
88
                /** @var Geometry|null $geometry */
89
                $geometry = null;
90
                foreach ($placemark->childNodes as $child) {
91
                    // Node names are all the same, except for MultiGeometry, which maps to GeometryCollection
92
                    $nodeName = $child->nodeName === 'multigeometry' ? 'geometrycollection' : $child->nodeName;
93
                    if (array_key_exists($nodeName, geoPHP::getGeometryList())) {
94
                        $function = 'parse' . geoPHP::getGeometryList()[$nodeName];
95
                        $geometry = $this->$function($child);
96
                    } elseif ($child->nodeType === 1) {
97
                        $data[$child->nodeName] = $child->nodeValue;
98
                    }
99
                }
100
                
101
                if (isset($geometry)) {
102
                    if (!empty($data)) {
103
                        $geometry->setData($data);
104
                    }
105
                    $geometries[] = $geometry;
106
                }
107
            }
108
            
109
            return new GeometryCollection($geometries);
110
        }
111
        
112
        // The document does not have a placemark, try to create a valid geometry from the root element
113
        $nodeName = $this->xmlObject->documentElement->nodeName === 'multigeometry' ?
114
                'geometrycollection' :
115
                $this->xmlObject->documentElement->nodeName;
116
117
        if (array_key_exists($nodeName, geoPHP::getGeometryList())) {
118
            $function = 'parse' . geoPHP::getGeometryList()[$nodeName];
119
            return $this->$function($this->xmlObject->documentElement);
120
        }
121
122
        return new GeometryCollection();
123
    }
124
125
    /**
126
     * @param \DOMNode $xml
127
     * @param string $nodeName
128
     * @return \DOMNode[]
129
     */
130
    protected function childElements(\DOMNode $xml, string $nodeName = ''): array
131
    {
132
        $children = [];
133
        foreach ($xml->childNodes as $child) {
134
            if ($child->nodeName == $nodeName) {
135
                $children[] = $child;
136
            }
137
        }
138
        
139
        return $children;
140
    }
141
142
    /**
143
     * @param \DOMNode $xml
144
     * @return Point
145
     */
146
    protected function parsePoint(\DOMNode $xml): Point
147
    {
148
        $coordinates = $this->extractCoordinates($xml);
149
        
150
        if (empty($coordinates)) {
151
            return new Point();
152
        }
153
        
154
        return new Point(
155
            $coordinates[0][0],
156
            $coordinates[0][1],
157
            (isset($coordinates[0][2]) ? $coordinates[0][2] : null),
158
            (isset($coordinates[0][3]) ? $coordinates[0][3] : null)
159
        );
160
    }
161
162
    /**
163
     * @param \DOMNode $xml
164
     * @return LineString
165
     */
166
    protected function parseLineString(\DOMNode $xml): LineString
167
    {
168
        $coordinates = $this->extractCoordinates($xml);
169
        $pointArray = [];
170
        $hasZ = false;
171
        $hasM = false;
172
173
        foreach ($coordinates as $set) {
174
            $hasZ = $hasZ || (isset($set[2]) && $set[2]);
175
            $hasM = $hasM || (isset($set[3]) && $set[3]);
176
        }
177
178
179
        if (count($coordinates) == 1) {
180
            $coordinates[1] = $coordinates[0];
181
        }
182
183
        foreach ($coordinates as $set) {
184
            $pointArray[] = new Point(
185
                $set[0],
186
                $set[1],
187
                ($hasZ ? (isset($set[2]) ? $set[2] : 0) : null),
188
                ($hasM ? (isset($set[3]) ? $set[3] : 0) : null)
189
            );
190
        }
191
192
        return new LineString($pointArray);
193
    }
194
195
    /**
196
     * @param \DOMNode $xml
197
     * @return Polygon
198
     * @throws \Exception
199
     */
200
    protected function parsePolygon(\DOMNode $xml): Polygon
201
    {
202
        $components = [];
203
204
        /** @noinspection SpellCheckingInspection */
205
        $outerBoundaryIs = $this->childElements($xml, 'outerboundaryis');
206
        if (empty($outerBoundaryIs)) {
207
            return new Polygon();
208
        }
209
        $outerBoundaryElement = $outerBoundaryIs[0];
210
        /** @noinspection SpellCheckingInspection */
211
        $outerRingElement = @$this->childElements($outerBoundaryElement, 'linearring')[0];
212
        $components[] = $this->parseLineString($outerRingElement);
213
214
        if (count($components) != 1) {
215
            throw new \Exception("Invalid KML");
216
        }
217
218
        /** @noinspection SpellCheckingInspection */
219
        $innerBoundaryElementIs = $this->childElements($xml, 'innerboundaryis');
220
        foreach ($innerBoundaryElementIs as $innerBoundaryElement) {
221
            /** @noinspection SpellCheckingInspection */
222
            foreach ($this->childElements($innerBoundaryElement, 'linearring') as $innerRingElement) {
223
                $components[] = $this->parseLineString($innerRingElement);
224
            }
225
        }
226
        
227
        return new Polygon($components);
228
    }
229
230
    /**
231
     * @param \DOMNode $xml
232
     * @return GeometryCollection
233
     */
234
    protected function parseGeometryCollection(\DOMNode $xml): GeometryCollection
235
    {
236
        $components = [];
237
        $geometryTypes = geoPHP::getGeometryList();
238
        
239
        foreach ($xml->childNodes as $child) {
240
            /** @noinspection SpellCheckingInspection */
241
            $nodeName = ($child->nodeName === 'linearring')
242
                    ? 'linestring'
243
                    : ($child->nodeName === 'multigeometry'
244
                            ? 'geometrycollection'
245
                            : $child->nodeName);
246
            if (array_key_exists($nodeName, $geometryTypes)) {
247
                $function = 'parse' . $geometryTypes[$nodeName];
248
                $components[] = $this->$function($child);
249
            }
250
        }
251
        
252
        return new GeometryCollection($components);
253
    }
254
255
    /**
256
     * @param \DOMNode $xml
257
     * @return array<array>
258
     */
259
    protected function extractCoordinates(\DOMNode $xml): array
260
    {
261
        $coordinateElements = $this->childElements($xml, 'coordinates');
262
        $coordinates = [];
263
        
264
        if (!empty($coordinateElements)) {
265
            $coordinateSets = explode(' ', preg_replace('/[\r\n\s\t]+/', ' ', $coordinateElements[0]->nodeValue));
266
267
            foreach ($coordinateSets as $setString) {
268
                $setString = trim($setString);
269
                if ($setString) {
270
                    $setArray = explode(',', $setString);
271
                    if (count($setArray) >= 2) {
272
                        $coordinates[] = $setArray;
273
                    }
274
                }
275
            }
276
        }
277
        
278
        return $coordinates;
279
    }
280
281
282
    /**
283
     * Serialize geometries into a KML string.
284
     *
285
     * @param Geometry $geometry
286
     * @param string $namespace
287
     * @return string The KML string representation of the input geometries
288
     */
289
    public function write(Geometry $geometry, string $namespace = ''): string
290
    {
291
        $namespace = trim($namespace);
292
        if (!empty($namespace)) {
293
            $this->nss = $namespace . ':';
294
        }
295
296
        return $this->geometryToKML($geometry);
297
    }
298
299
    /**
300
     * @param Geometry $geometry
301
     * @return string
302
     */
303
    private function geometryToKML(Geometry $geometry): string
304
    {
305
        $type = $geometry->geometryType();
306
        switch ($type) {
307
            case Geometry::POINT:
308
                /** @var Point $geometry */
309
                return $this->pointToKML($geometry);
310
            case Geometry::LINESTRING:
311
                /** @var LineString $geometry */
312
                return $this->linestringToKML($geometry);
313
            case Geometry::POLYGON:
314
                /** @var Polygon $geometry */
315
                return $this->polygonToKML($geometry);
316
            case Geometry::MULTI_POINT:
317
            case Geometry::MULTI_LINESTRING:
318
            case Geometry::MULTI_POLYGON:
319
            case Geometry::GEOMETRY_COLLECTION:
320
            /** @var Collection $geometry */
321
                return $this->collectionToKML($geometry);
322
        }
323
        return '';
324
    }
325
326
    /**
327
     * @param Point $geometry
328
     * @return string
329
     */
330
    private function pointToKML(Geometry $geometry): string
331
    {
332
        $str = '<' . $this->nss . "Point>\n<" . $this->nss . 'coordinates>';
333
        if ($geometry->isEmpty()) {
334
            $str .= "0,0";
335
        } else {
336
            $str .= $geometry->getX() . ',' . $geometry->getY() . ($geometry->hasZ() ? ',' . $geometry->getZ() : '');
337
        }
338
        return $str . '</' . $this->nss . 'coordinates></' . $this->nss . "Point>\n";
339
    }
340
341
    /**
342
     * @param LineString $geometry
343
     * @param string $type
344
     * @return string
345
     */
346
    private function linestringToKML(Geometry $geometry, $type = null): string
347
    {
348
        if (!isset($type)) {
349
            $type = $geometry->geometryType();
350
        }
351
352
        $str = '<' . $this->nss . $type . ">\n";
353
354
        if (!$geometry->isEmpty()) {
355
            $str .= '<' . $this->nss . 'coordinates>';
356
            $i = 0;
357
            foreach ($geometry->getComponents() as $comp) {
358
                if ($i != 0) {
359
                    $str .= ' ';
360
                }
361
                $str .= $comp->getX() . ',' . $comp->getY();
362
                $i++;
363
            }
364
365
            $str .= '</' . $this->nss . 'coordinates>';
366
        }
367
368
        $str .= '</' . $this->nss . $type . ">\n";
369
370
        return $str;
371
    }
372
373
    /**
374
     * @param Polygon $geometry
375
     * @return string
376
     */
377
    public function polygonToKML(Geometry $geometry): string
378
    {
379
        /** @var LineString[] $components */
380
        $components = $geometry->getComponents();
381
        $str = '';
382
        if (!empty($components)) {
383
            /** @noinspection PhpParamsInspection */
384
            $str = '<' . $this->nss . 'outerBoundaryIs>' . $this->linestringToKML($components[0], 'LinearRing') . '</' . $this->nss . 'outerBoundaryIs>';
385
            foreach (array_slice($components, 1) as $comp) {
386
                $str .= '<' . $this->nss . 'innerBoundaryIs>' . $this->linestringToKML($comp) . '</' . $this->nss . 'innerBoundaryIs>';
387
            }
388
        }
389
390
        return '<' . $this->nss . "Polygon>\n" . $str . '</' . $this->nss . "Polygon>\n";
391
    }
392
393
    /**
394
     * @param Collection $geometry
395
     * @return string
396
     */
397
    public function collectionToKML(Geometry $geometry): string
398
    {
399
        $components = $geometry->getComponents();
400
        $str = '<' . $this->nss . "MultiGeometry>\n";
401
        foreach ($components as $component) {
402
            $subAdapter = new KML();
403
            $str .= $subAdapter->write($component);
404
        }
405
406
        return $str . '</' . $this->nss . "MultiGeometry>\n";
407
    }
408
}
409