KML::linestringToKML()   A
last analyzed

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
c 0
b 0
f 0
dl 0
loc 25
rs 9.4888
cc 5
nc 4
nop 2
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' : $this->xmlObject->documentElement->nodeName;
115
116
        if (array_key_exists($nodeName, geoPHP::getGeometryList())) {
117
            $function = 'parse' . geoPHP::getGeometryList()[$nodeName];
118
            return $this->$function($this->xmlObject->documentElement);
119
        }
120
121
        return new GeometryCollection();
122
    }
123
124
    /**
125
     * @param \DOMNode $xml
126
     * @param string $nodeName
127
     * @return \DOMNode[]
128
     */
129
    protected function childElements(\DOMNode $xml, string $nodeName = ''): array
130
    {
131
        $children = [];
132
        foreach ($xml->childNodes as $child) {
133
            if ($child->nodeName == $nodeName) {
134
                $children[] = $child;
135
            }
136
        }
137
        
138
        return $children;
139
    }
140
141
    /**
142
     * @param \DOMNode $xml
143
     * @return Point
144
     */
145
    protected function parsePoint(\DOMNode $xml): Point
146
    {
147
        $coordinates = $this->extractCoordinates($xml);
148
        
149
        if (empty($coordinates)) {
150
            return new Point();
151
        }
152
        
153
        return new Point(
154
            $coordinates[0][0],
155
            $coordinates[0][1],
156
            (isset($coordinates[0][2]) ? $coordinates[0][2] : null),
157
            (isset($coordinates[0][3]) ? $coordinates[0][3] : null)
158
        );
159
    }
160
161
    /**
162
     * @param \DOMNode $xml
163
     * @return LineString
164
     */
165
    protected function parseLineString(\DOMNode $xml): LineString
166
    {
167
        $coordinates = $this->extractCoordinates($xml);
168
        $pointArray = [];
169
        $hasZ = false;
170
        $hasM = false;
171
172
        foreach ($coordinates as $set) {
173
            $hasZ = $hasZ || (isset($set[2]) && $set[2]);
174
            $hasM = $hasM || (isset($set[3]) && $set[3]);
175
        }
176
177
        if (count($coordinates) == 1) {
178
            $coordinates[1] = $coordinates[0];
179
        }
180
181
        foreach ($coordinates as $set) {
182
            $pointArray[] = new Point(
183
                $set[0],
184
                $set[1],
185
                ($hasZ ? (isset($set[2]) ? $set[2] : 0) : null),
186
                ($hasM ? (isset($set[3]) ? $set[3] : 0) : null)
187
            );
188
        }
189
190
        return new LineString($pointArray);
191
    }
192
193
    /**
194
     * @param \DOMNode $xml
195
     * @return Polygon
196
     * @throws \Exception
197
     */
198
    protected function parsePolygon(\DOMNode $xml): Polygon
199
    {
200
        $components = [];
201
202
        /** @noinspection SpellCheckingInspection */
203
        $outerBoundaryIs = $this->childElements($xml, 'outerboundaryis');
204
        if (empty($outerBoundaryIs)) {
205
            return new Polygon();
206
        }
207
        $outerBoundaryElement = $outerBoundaryIs[0];
208
        /** @noinspection SpellCheckingInspection */
209
        $outerRingElement = @$this->childElements($outerBoundaryElement, 'linearring')[0];
210
        $components[] = $this->parseLineString($outerRingElement);
211
212
        if (count($components) != 1) {
213
            throw new \Exception("Invalid KML");
214
        }
215
216
        /** @noinspection SpellCheckingInspection */
217
        $innerBoundaryElementIs = $this->childElements($xml, 'innerboundaryis');
218
        foreach ($innerBoundaryElementIs as $innerBoundaryElement) {
219
            /** @noinspection SpellCheckingInspection */
220
            foreach ($this->childElements($innerBoundaryElement, 'linearring') as $innerRingElement) {
221
                $components[] = $this->parseLineString($innerRingElement);
222
            }
223
        }
224
        
225
        return new Polygon($components);
226
    }
227
228
    /**
229
     * @param \DOMNode $xml
230
     * @return GeometryCollection
231
     */
232
    protected function parseGeometryCollection(\DOMNode $xml): GeometryCollection
233
    {
234
        $components = [];
235
        $geometryTypes = geoPHP::getGeometryList();
236
        
237
        foreach ($xml->childNodes as $child) {
238
            /** @noinspection SpellCheckingInspection */
239
            $nodeName = ($child->nodeName === 'linearring')
240
                    ? 'linestring'
241
                    : ($child->nodeName === 'multigeometry'
242
                            ? 'geometrycollection'
243
                            : $child->nodeName);
244
            if (array_key_exists($nodeName, $geometryTypes)) {
245
                $function = 'parse' . $geometryTypes[$nodeName];
246
                $components[] = $this->$function($child);
247
            }
248
        }
249
        
250
        return new GeometryCollection($components);
251
    }
252
253
    /**
254
     * @param \DOMNode $xml
255
     * @return array<array>
256
     */
257
    protected function extractCoordinates(\DOMNode $xml): array
258
    {
259
        $coordinateElements = $this->childElements($xml, 'coordinates');
260
        $coordinates = [];
261
        
262
        if (!empty($coordinateElements)) {
263
            $coordinateSets = explode(' ', preg_replace('/[\r\n\s\t]+/', ' ', $coordinateElements[0]->nodeValue));
264
265
            foreach ($coordinateSets as $setString) {
266
                $setString = trim($setString);
267
                if ($setString) {
268
                    $setArray = explode(',', $setString);
269
                    if (count($setArray) >= 2) {
270
                        $coordinates[] = $setArray;
271
                    }
272
                }
273
            }
274
        }
275
        
276
        return $coordinates;
277
    }
278
279
280
    /**
281
     * Serialize geometries into a KML string.
282
     *
283
     * @param Geometry $geometry
284
     * @param string $namespace
285
     * @return string The KML string representation of the input geometries
286
     */
287
    public function write(Geometry $geometry, string $namespace = ''): string
288
    {
289
        $namespace = trim($namespace);
290
        if (!empty($namespace)) {
291
            $this->nss = $namespace . ':';
292
        }
293
294
        return $this->geometryToKML($geometry);
295
    }
296
297
    /**
298
     * @param Geometry $geometry
299
     * @return string
300
     */
301
    private function geometryToKML(Geometry $geometry): string
302
    {
303
        $type = $geometry->geometryType();
304
        switch ($type) {
305
            case Geometry::POINT:
306
                /** @var Point $geometry */
307
                return $this->pointToKML($geometry);
308
            case Geometry::LINESTRING:
309
                /** @var LineString $geometry */
310
                return $this->linestringToKML($geometry);
311
            case Geometry::POLYGON:
312
                /** @var Polygon $geometry */
313
                return $this->polygonToKML($geometry);
314
            case Geometry::MULTI_POINT:
315
            case Geometry::MULTI_LINESTRING:
316
            case Geometry::MULTI_POLYGON:
317
            case Geometry::GEOMETRY_COLLECTION:
318
            /** @var Collection $geometry */
319
                return $this->collectionToKML($geometry);
320
        }
321
        return '';
322
    }
323
324
    /**
325
     * @param Point $geometry
326
     * @return string
327
     */
328
    private function pointToKML(Geometry $geometry): string
329
    {
330
        $str = '<' . $this->nss . "Point>\n<" . $this->nss . 'coordinates>';
331
        if ($geometry->isEmpty()) {
332
            $str .= "0,0";
333
        } else {
334
            $str .= $geometry->getX() . ',' . $geometry->getY() . ($geometry->hasZ() ? ',' . $geometry->getZ() : '');
0 ignored issues
show
Bug introduced by
Are you sure the usage of $geometry->getY() targeting geoPHP\Geometry\Geometry::getY() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure the usage of $geometry->getZ() targeting geoPHP\Geometry\Geometry::getZ() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure the usage of $geometry->getX() targeting geoPHP\Geometry\Geometry::getX() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
335
        }
336
        return $str . '</' . $this->nss . 'coordinates></' . $this->nss . "Point>\n";
337
    }
338
339
    /**
340
     * @param LineString $geometry
341
     * @param string $type
342
     * @return string
343
     */
344
    private function linestringToKML(Geometry $geometry, $type = null): string
345
    {
346
        if (!isset($type)) {
347
            $type = $geometry->geometryType();
348
        }
349
350
        $str = '<' . $this->nss . $type . ">\n";
351
352
        if (!$geometry->isEmpty()) {
353
            $str .= '<' . $this->nss . 'coordinates>';
354
            $i = 0;
355
            foreach ($geometry->getComponents() as $comp) {
356
                if ($i != 0) {
357
                    $str .= ' ';
358
                }
359
                $str .= $comp->getX() . ',' . $comp->getY();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $comp->getY() targeting geoPHP\Geometry\Geometry::getY() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
Are you sure the usage of $comp->getX() targeting geoPHP\Geometry\Geometry::getX() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
360
                $i++;
361
            }
362
363
            $str .= '</' . $this->nss . 'coordinates>';
364
        }
365
366
        $str .= '</' . $this->nss . $type . ">\n";
367
368
        return $str;
369
    }
370
371
    /**
372
     * @param Polygon $geometry
373
     * @return string
374
     */
375
    public function polygonToKML(Geometry $geometry): string
376
    {
377
        /** @var LineString[] $components */
378
        $components = $geometry->getComponents();
379
        $str = '';
380
        if (!empty($components)) {
381
            /** @noinspection PhpParamsInspection */
382
            $str = '<' . $this->nss . 'outerBoundaryIs>' . $this->linestringToKML($components[0], 'LinearRing') . '</' . $this->nss . 'outerBoundaryIs>';
383
            foreach (array_slice($components, 1) as $comp) {
384
                $str .= '<' . $this->nss . 'innerBoundaryIs>' . $this->linestringToKML($comp) . '</' . $this->nss . 'innerBoundaryIs>';
385
            }
386
        }
387
388
        return '<' . $this->nss . "Polygon>\n" . $str . '</' . $this->nss . "Polygon>\n";
389
    }
390
391
    /**
392
     * @param Collection $geometry
393
     * @return string
394
     */
395
    public function collectionToKML(Geometry $geometry): string
396
    {
397
        $components = $geometry->getComponents();
398
        $str = '<' . $this->nss . "MultiGeometry>\n";
399
        foreach ($components as $component) {
400
            $subAdapter = new KML();
401
            $str .= $subAdapter->write($component);
402
        }
403
404
        return $str . '</' . $this->nss . "MultiGeometry>\n";
405
    }
406
}
407