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\MultiPoint; |
10
|
|
|
use geoPHP\Geometry\LineString; |
11
|
|
|
use geoPHP\Geometry\MultiLineString; |
12
|
|
|
use geoPHP\Geometry\Polygon; |
13
|
|
|
use geoPHP\Geometry\MultiPolygon; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* WKT (Well Known Text) Adapter |
17
|
|
|
*/ |
18
|
|
|
class WKT implements GeoAdapter |
19
|
|
|
{ |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var bool true if geometry has z-values |
23
|
|
|
*/ |
24
|
|
|
protected $hasZ = false; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var bool true if geometry has m-values |
28
|
|
|
*/ |
29
|
|
|
protected $measured = false; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Determines if the given typeString is a valid WKT geometry type |
33
|
|
|
* |
34
|
|
|
* @param string $typeString Type to find, eg. "Point", or "LineStringZ" |
35
|
|
|
* @return string|bool The geometry type if found or false |
36
|
|
|
*/ |
37
|
|
|
public static function isWktType(string $typeString) |
38
|
|
|
{ |
39
|
|
|
foreach (geoPHP::getGeometryList() as $geom => $type) { |
40
|
|
|
if (strtolower((substr($typeString, 0, strlen($geom)))) == $geom) { |
41
|
|
|
return $type; |
42
|
|
|
} |
43
|
|
|
} |
44
|
|
|
return false; |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Read WKT string into geometry objects |
49
|
|
|
* |
50
|
|
|
* @param string $wkt A WKT string |
51
|
|
|
* @return Geometry |
52
|
|
|
* @throws \Exception |
53
|
|
|
*/ |
54
|
|
|
public function read(string $wkt): Geometry |
55
|
|
|
{ |
56
|
|
|
$this->hasZ = false; |
57
|
|
|
$this->measured = false; |
58
|
|
|
|
59
|
|
|
$wkt = trim(strtoupper($wkt)); |
60
|
|
|
$srid = null; |
61
|
|
|
$m = []; |
62
|
|
|
// If it contains a ';', then it contains additional SRID data |
63
|
|
|
if (preg_match('/^SRID=(\d+);/', $wkt, $m)) { |
64
|
|
|
$srid = $m[1]; |
65
|
|
|
$wkt = substr($wkt, strlen($m[0])); |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
// If geos is installed, then we take a shortcut and let it parse the WKT |
69
|
|
|
if (geoPHP::geosInstalled()) { |
70
|
|
|
/** @noinspection PhpUndefinedClassInspection */ |
71
|
|
|
$reader = new \GEOSWKTReader(); |
72
|
|
|
$geometry = geoPHP::geosToGeometry($reader->read($wkt)); |
73
|
|
|
} else { |
74
|
|
|
$geometry = $this->parseTypeAndGetData($wkt); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
if ($geometry !== null) { |
78
|
|
|
if ($srid) { |
79
|
|
|
$geometry->setSRID($srid); |
80
|
|
|
} |
81
|
|
|
return $geometry; |
82
|
|
|
} |
83
|
|
|
throw new \Exception('Invalid WKT given.'); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @param string $wkt |
88
|
|
|
* @return Geometry|null |
89
|
|
|
* @throws \Exception |
90
|
|
|
*/ |
91
|
|
|
private function parseTypeAndGetData(string $wkt) |
92
|
|
|
{ |
93
|
|
|
// geometry type is the first word |
94
|
|
|
$m = []; |
95
|
|
|
if (preg_match('#^([a-z]*)#i', $wkt, $m)) { |
96
|
|
|
$geometryType = $this->isWktType($m[1]); |
97
|
|
|
|
98
|
|
|
$dataString = 'EMPTY'; |
99
|
|
|
if ($geometryType) { |
100
|
|
|
if (preg_match('#(z{0,1})(m{0,1})\s*\((.*)\)$#i', $wkt, $m)) { |
101
|
|
|
$this->hasZ = $m[1]; |
102
|
|
|
$this->measured = $m[2] ?: null; |
103
|
|
|
$dataString = $m[3] ?: $dataString; |
104
|
|
|
} |
105
|
|
|
$method = 'parse' . $geometryType; |
106
|
|
|
return call_user_func([$this, $method], $dataString); |
107
|
|
|
} |
108
|
|
|
throw new \Exception('Invalid WKT type "' . $m[1] . '"'); |
109
|
|
|
} |
110
|
|
|
throw new \Exception('Cannot parse WKT'); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @param string $dataString |
115
|
|
|
* @return Point |
116
|
|
|
*/ |
117
|
|
|
private function parsePoint(string $dataString): Point |
118
|
|
|
{ |
119
|
|
|
$dataString = trim($dataString); |
120
|
|
|
|
121
|
|
|
// If it's marked as empty, then return an empty point |
122
|
|
|
if ($dataString === 'EMPTY') { |
123
|
|
|
return new Point(); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
$z = $m = null; |
127
|
|
|
$parts = explode(' ', $dataString); |
128
|
|
|
if (isset($parts[2])) { |
129
|
|
|
if ($this->measured) { |
130
|
|
|
$m = $parts[2]; |
131
|
|
|
} else { |
132
|
|
|
$z = $parts[2]; |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
if (isset($parts[3])) { |
136
|
|
|
$m = $parts[3]; |
137
|
|
|
} |
138
|
|
|
return new Point($parts[0], $parts[1], $z, $m); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* @param string $dataString |
143
|
|
|
* @return LineString |
144
|
|
|
*/ |
145
|
|
|
private function parseLineString(string $dataString): LineString |
146
|
|
|
{ |
147
|
|
|
// If it's marked as empty, then return an empty line |
148
|
|
|
if ($dataString === 'EMPTY') { |
149
|
|
|
return new LineString(); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$points = []; |
153
|
|
|
foreach (explode(',', $dataString) as $part) { |
154
|
|
|
$points[] = $this->parsePoint($part); |
155
|
|
|
} |
156
|
|
|
return new LineString($points); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* @param string $dataString |
161
|
|
|
* @return Polygon |
162
|
|
|
*/ |
163
|
|
|
private function parsePolygon(string $dataString): Polygon |
164
|
|
|
{ |
165
|
|
|
// If it's marked as empty, then return an empty polygon |
166
|
|
|
if ($dataString === 'EMPTY') { |
167
|
|
|
return new Polygon(); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
$lines = []; |
171
|
|
|
$m = []; |
172
|
|
|
if (preg_match_all('/\(([^)(]*)\)/', $dataString, $m)) { |
173
|
|
|
foreach ($m[1] as $part) { |
174
|
|
|
$lines[] = $this->parseLineString($part); |
175
|
|
|
} |
176
|
|
|
} |
177
|
|
|
return new Polygon($lines); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* @noinspection PhpUnusedPrivateMethodInspection |
182
|
|
|
* @param string $dataString |
183
|
|
|
* @return MultiPoint |
184
|
|
|
*/ |
185
|
|
|
private function parseMultiPoint(string $dataString): MultiPoint |
186
|
|
|
{ |
187
|
|
|
// If it's marked as empty, then return an empty MultiPoint |
188
|
|
|
if ($dataString === 'EMPTY') { |
189
|
|
|
return new MultiPoint(); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$points = []; |
193
|
|
|
/* Should understand both forms: |
194
|
|
|
* MULTIPOINT ((1 2), (3 4)) |
195
|
|
|
* MULTIPOINT (1 2, 3 4) |
196
|
|
|
*/ |
197
|
|
|
foreach (explode(',', $dataString) as $part) { |
198
|
|
|
$points[] = $this->parsePoint(trim($part, ' ()')); |
199
|
|
|
} |
200
|
|
|
return new MultiPoint($points); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @noinspection PhpUnusedPrivateMethodInspection |
205
|
|
|
* @param string $dataString |
206
|
|
|
* @return MultiLineString |
207
|
|
|
*/ |
208
|
|
|
private function parseMultiLineString(string $dataString): MultiLineString |
209
|
|
|
{ |
210
|
|
|
// If it's marked as empty, then return an empty multi-linestring |
211
|
|
|
if ($dataString === 'EMPTY') { |
212
|
|
|
return new MultiLineString(); |
213
|
|
|
} |
214
|
|
|
$lines = []; |
215
|
|
|
$m = []; |
216
|
|
|
if (preg_match_all('/(\([^(]+?\)|EMPTY)/', $dataString, $m)) { |
217
|
|
|
foreach ($m[1] as $part) { |
218
|
|
|
$lines[] = $this->parseLineString(trim($part, ' ()')); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
return new MultiLineString($lines); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* @noinspection PhpUnusedPrivateMethodInspection |
226
|
|
|
* @param string $dataString |
227
|
|
|
* @return MultiPolygon |
228
|
|
|
*/ |
229
|
|
|
private function parseMultiPolygon(string $dataString): MultiPolygon |
230
|
|
|
{ |
231
|
|
|
// If it's marked as empty, then return an empty multi-polygon |
232
|
|
|
if ($dataString === 'EMPTY') { |
233
|
|
|
return new MultiPolygon(); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
$polygons = []; |
237
|
|
|
$m = []; |
238
|
|
|
if (preg_match_all('/(\(\([^(].+?\)\)|EMPTY)/', $dataString, $m)) { |
239
|
|
|
foreach ($m[0] as $part) { |
240
|
|
|
$polygons[] = $this->parsePolygon($part); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
return new MultiPolygon($polygons); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @noinspection PhpUnusedPrivateMethodInspection |
248
|
|
|
* @param string $dataString |
249
|
|
|
* @return GeometryCollection |
250
|
|
|
*/ |
251
|
|
|
private function parseGeometryCollection(string $dataString): GeometryCollection |
252
|
|
|
{ |
253
|
|
|
// If it's marked as empty, then return an empty geom-collection |
254
|
|
|
if ($dataString === 'EMPTY') { |
255
|
|
|
return new GeometryCollection(); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
$geometries = []; |
259
|
|
|
$m = []; |
260
|
|
|
while (strlen($dataString) > 0) { |
261
|
|
|
if ($dataString[0] === ',') { |
262
|
|
|
$dataString = substr($dataString, 1); |
263
|
|
|
} |
264
|
|
|
// Matches the first balanced parenthesis group (or term EMPTY) |
265
|
|
|
preg_match( |
266
|
|
|
'/\((?>[^()]+|(?R))*\)|EMPTY/', |
267
|
|
|
$dataString, |
268
|
|
|
$m, |
269
|
|
|
PREG_OFFSET_CAPTURE |
270
|
|
|
); |
271
|
|
|
if (!isset($m[0])) { |
272
|
|
|
// something weird happened, we stop here before running in an infinite loop |
273
|
|
|
break; |
274
|
|
|
} |
275
|
|
|
$cutPosition = strlen($m[0][0]) + $m[0][1]; |
276
|
|
|
$geometries[] = $this->parseTypeAndGetData(trim(substr($dataString, 0, $cutPosition))); |
277
|
|
|
$dataString = trim(substr($dataString, $cutPosition)); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
return new GeometryCollection($geometries); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Serialize geometries into a WKT string. |
285
|
|
|
* |
286
|
|
|
* @param Geometry $geometry |
287
|
|
|
* @return string The WKT string representation of the input geometries |
288
|
|
|
*/ |
289
|
|
|
public function write(Geometry $geometry): string |
290
|
|
|
{ |
291
|
|
|
// If geos is installed, then we take a shortcut and let it write the WKT |
292
|
|
|
if (geoPHP::geosInstalled()) { |
293
|
|
|
/** @noinspection PhpUndefinedClassInspection */ |
294
|
|
|
$writer = new \GEOSWKTWriter(); |
295
|
|
|
/** @noinspection PhpUndefinedMethodInspection */ |
296
|
|
|
$writer->setRoundingPrecision(14); |
297
|
|
|
/** @noinspection PhpUndefinedMethodInspection */ |
298
|
|
|
$writer->setTrim(true); |
299
|
|
|
/** @noinspection PhpUndefinedMethodInspection */ |
300
|
|
|
return $writer->write($geometry->getGeos()); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
$this->measured = $geometry->isMeasured(); |
304
|
|
|
$this->hasZ = $geometry->hasZ(); |
305
|
|
|
|
306
|
|
|
if ($geometry->isEmpty()) { |
307
|
|
|
return strtoupper($geometry->geometryType()) . ' EMPTY'; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
$data = $this->extractData($geometry); |
311
|
|
|
if (!empty($data)) { |
312
|
|
|
$extension = ''; |
313
|
|
|
if ($this->hasZ) { |
314
|
|
|
$extension .= 'Z'; |
315
|
|
|
} |
316
|
|
|
if ($this->measured) { |
317
|
|
|
$extension .= 'M'; |
318
|
|
|
} |
319
|
|
|
return strtoupper($geometry->geometryType()) . ($extension ? ' ' . $extension : '') . ' (' . $data . ')'; |
320
|
|
|
} |
321
|
|
|
return ''; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Extract geometry to a WKT string |
326
|
|
|
* |
327
|
|
|
* @param Geometry|Collection $geometry A Geometry object |
328
|
|
|
* @return string |
329
|
|
|
*/ |
330
|
|
|
public function extractData($geometry): string |
331
|
|
|
{ |
332
|
|
|
$parts = []; |
333
|
|
|
switch ($geometry->geometryType()) { |
334
|
|
|
case Geometry::POINT: |
335
|
|
|
$p = $geometry->getX() . ' ' . $geometry->getY(); |
|
|
|
|
336
|
|
|
if ($geometry->hasZ()) { |
337
|
|
|
$p .= ' ' . $geometry->getZ(); |
|
|
|
|
338
|
|
|
$this->hasZ = $this->hasZ || $geometry->hasZ(); |
339
|
|
|
} |
340
|
|
|
if ($geometry->isMeasured()) { |
341
|
|
|
$p .= ' ' . $geometry->getM(); |
|
|
|
|
342
|
|
|
$this->measured = $this->measured || $geometry->isMeasured(); |
343
|
|
|
} |
344
|
|
|
return $p; |
345
|
|
|
case Geometry::LINESTRING: |
346
|
|
|
foreach ($geometry->getComponents() as $component) { |
347
|
|
|
$parts[] = $this->extractData($component); |
348
|
|
|
} |
349
|
|
|
return implode(', ', $parts); |
350
|
|
|
case Geometry::POLYGON: |
351
|
|
|
case Geometry::MULTI_POINT: |
352
|
|
|
case Geometry::MULTI_LINESTRING: |
353
|
|
|
case Geometry::MULTI_POLYGON: |
354
|
|
|
foreach ($geometry->getComponents() as $component) { |
355
|
|
|
if ($component->isEmpty()) { |
356
|
|
|
$parts[] = 'EMPTY'; |
357
|
|
|
} else { |
358
|
|
|
$parts[] = '(' . $this->extractData($component) . ')'; |
359
|
|
|
} |
360
|
|
|
} |
361
|
|
|
return implode(', ', $parts); |
362
|
|
|
case Geometry::GEOMETRY_COLLECTION: |
363
|
|
|
foreach ($geometry->getComponents() as $component) { |
364
|
|
|
$this->hasZ = $this->hasZ || $geometry->hasZ(); |
365
|
|
|
$this->measured = $this->measured || $geometry->isMeasured(); |
366
|
|
|
|
367
|
|
|
$extension = ''; |
368
|
|
|
if ($this->hasZ) { |
369
|
|
|
$extension .= 'Z'; |
370
|
|
|
} |
371
|
|
|
if ($this->measured) { |
372
|
|
|
$extension .= 'M'; |
373
|
|
|
} |
374
|
|
|
$data = $this->extractData($component); |
375
|
|
|
$parts[] = strtoupper($component->geometryType()) |
376
|
|
|
. ($extension ? ' ' . $extension : '') |
377
|
|
|
. ($data ? ' (' . $data . ')' : ' EMPTY'); |
378
|
|
|
} |
379
|
|
|
return implode(', ', $parts); |
380
|
|
|
} |
381
|
|
|
return ''; |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
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.