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