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() : ''); |
|
|
|
|
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(); |
|
|
|
|
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
|
|
|
|
This check looks for function or method calls that always return null and whose return value is used.
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.