WKT::parseMultiPolygon()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
eloc 8
c 3
b 0
f 1
dl 0
loc 15
rs 10
cc 4
nc 3
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\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();
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->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...
336
                if ($geometry->hasZ()) {
337
                    $p .= ' ' . $geometry->getZ();
0 ignored issues
show
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...
338
                    $this->hasZ = $this->hasZ || $geometry->hasZ();
339
                }
340
                if ($geometry->isMeasured()) {
341
                    $p .= ' ' . $geometry->getM();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $geometry->getM() targeting geoPHP\Geometry\Geometry::getM() 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...
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