Passed
Pull Request — master (#4)
by Mark
01:50
created

WKT   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 344
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 154
dl 0
loc 344
rs 2.7199
c 0
b 0
f 0
wmc 71

12 Methods

Rating   Name   Duplication   Size   Complexity  
B read() 0 37 8
A parseMultiLineString() 0 13 4
A parseMultiPoint() 0 16 3
D extractData() 0 52 22
A parseLineString() 0 12 3
A parsePolygon() 0 14 4
A parseGeometryCollection() 0 27 4
A parseTypeAndGetData() 0 17 4
A parsePoint() 0 20 5
B write() 0 31 7
A parseMultiPolygon() 0 14 4
A isWktType() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like WKT often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WKT, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace geoPHP\Adapter;
4
5
use geoPHP\Geometry\Collection;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Collection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use geoPHP\geoPHP;
7
use geoPHP\Geometry\Geometry;
8
use geoPHP\Geometry\GeometryCollection;
9
use geoPHP\Geometry\Point;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use geoPHP\Geometry\MultiPoint;
11
use geoPHP\Geometry\LineString;
12
use geoPHP\Geometry\MultiLineString;
13
use geoPHP\Geometry\Polygon;
14
use geoPHP\Geometry\MultiPolygon;
15
16
/**
17
 * WKT (Well Known Text) Adapter
18
 */
19
class WKT implements GeoAdapter
20
{
21
22
    protected $hasZ      = false;
23
24
    protected $measured  = false;
25
26
    /**
27
     * Determines if the given typeString is a valid WKT geometry type
28
     *
29
     * @param string $typeString Type to find, eg. "Point", or "LineStringZ"
30
     * @return string|bool The geometry type if found or false
31
     */
32
    public static function isWktType($typeString)
33
    {
34
        foreach (geoPHP::getGeometryList() as $geom => $type) {
35
            if (strtolower((substr($typeString, 0, strlen($geom)))) == $geom) {
36
                return $type;
37
            }
38
        }
39
        return false;
40
    }
41
42
    /**
43
     * Read WKT string into geometry objects
44
     *
45
     * @param string $wkt A WKT string
46
     * @return Geometry
47
     * @throws \Exception
48
     */
49
    public function read($wkt)
50
    {
51
        $this->hasZ = false;
52
        $this->measured = false;
53
54
        $wkt = trim(strtoupper($wkt));
55
        $srid = null;
56
        // If it contains a ';', then it contains additional SRID data
57
        if (preg_match('/^SRID=(\d+);/', $wkt, $m)) {
58
            $srid = $m[1];
59
            $wkt = substr($wkt, strlen($m[0]));
60
        }
61
62
        // If geos is installed, then we take a shortcut and let it parse the WKT
63
        if (geoPHP::geosInstalled()) {
64
            /** @noinspection PhpUndefinedClassInspection */
65
            $reader = new \GEOSWKTReader();
66
            try {
67
                $geom = geoPHP::geosToGeometry($reader->read($wkt));
68
                if ($srid) {
69
                    $geom->setSRID($srid);
70
                }
71
                return $geom;
72
            } catch (\Exception $e) {
73
//                if ($e->getMessage() !== 'IllegalArgumentException: Empty Points cannot be represented in WKB') {
74
//                    throw $e;
75
//                } // else try with GeoPHP' parser
76
            }
77
        }
78
79
        if ($geometry = $this->parseTypeAndGetData($wkt)) {
80
            if ($geometry && $srid) {
81
                $geometry->setSRID($srid);
82
            }
83
            return $geometry;
84
        }
85
        throw new \Exception('Invalid Wkt');
86
    }
87
88
    /**
89
     * @param string $wkt
90
     *
91
     * @return Geometry|null
92
     * @throws \Exception
93
     */
94
    private function parseTypeAndGetData($wkt)
95
    {
96
        // geometry type is the first word
97
        if (preg_match('/^(?<type>[A-Z]+)\s*(?<z>Z*)(?<m>M*)\s*(?:\((?<data>.+)\)|(?<data_empty>EMPTY))$/', $wkt, $m)) {
98
            $geometryType = $this->isWktType($m['type']);
99
            // Not used yet
100
            //$this->hasZ   = $this->hasZ || $m['z'];
101
            //$this->measured = $this->measured || $m['m'];
102
            $dataString = $m['data'] ?: $m['data_empty'];
103
104
            if ($geometryType) {
105
                $method = 'parse' . $geometryType;
0 ignored issues
show
Bug introduced by
Are you sure $geometryType of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

105
                $method = 'parse' . /** @scrutinizer ignore-type */ $geometryType;
Loading history...
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
    private function parsePoint($dataString)
114
    {
115
        $dataString = trim($dataString);
116
        // If it's marked as empty, then return an empty point
117
        if ($dataString == 'EMPTY') {
118
            return new Point();
119
        }
120
        $z = $m = null;
121
        $parts = explode(' ', $dataString);
122
        if (isset($parts[2])) {
123
            if ($this->measured) {
124
                $m = $parts[2];
125
            } else {
126
                $z = $parts[2];
127
            }
128
        }
129
        if (isset($parts[3])) {
130
            $m = $parts[3];
131
        }
132
        return new Point($parts[0], $parts[1], $z, $m);
133
    }
134
135
    private function parseLineString($dataString)
136
    {
137
        // If it's marked as empty, then return an empty line
138
        if ($dataString == 'EMPTY') {
139
            return new LineString();
140
        }
141
142
        $points = [];
143
        foreach (explode(',', $dataString) as $part) {
144
            $points[] = $this->parsePoint($part);
145
        }
146
        return new LineString($points);
147
    }
148
149
    private function parsePolygon($dataString)
150
    {
151
        // If it's marked as empty, then return an empty polygon
152
        if ($dataString == 'EMPTY') {
153
            return new Polygon();
154
        }
155
156
        $lines = [];
157
        if (preg_match_all('/\(([^)(]*)\)/', $dataString, $m)) {
158
            foreach ($m[1] as $part) {
159
                $lines[] = $this->parseLineString($part);
160
            }
161
        }
162
        return new Polygon($lines);
163
    }
164
165
    /** @noinspection PhpUnusedPrivateMethodInspection
166
     * @param string $dataString
167
     *
168
     * @return MultiPoint
169
     */
170
    private function parseMultiPoint($dataString)
171
    {
172
        // If it's marked as empty, then return an empty MultiPoint
173
        if ($dataString == 'EMPTY') {
174
            return new MultiPoint();
175
        }
176
177
        $points = [];
178
        /* Should understand both forms:
179
         * MULTIPOINT ((1 2), (3 4))
180
         * MULTIPOINT (1 2, 3 4)
181
         */
182
        foreach (explode(',', $dataString) as $part) {
183
            $points[] =  $this->parsePoint(trim($part, ' ()'));
184
        }
185
        return new MultiPoint($points);
186
    }
187
188
    /** @noinspection PhpUnusedPrivateMethodInspection
189
     * @param string $dataString
190
     *
191
     * @return MultiLineString
192
     */
193
    private function parseMultiLineString($dataString)
194
    {
195
        // If it's marked as empty, then return an empty multi-linestring
196
        if ($dataString == 'EMPTY') {
197
            return new MultiLineString();
198
        }
199
        $lines = [];
200
        if (preg_match_all('/(\([^(]+\)|EMPTY)/', $dataString, $m)) {
201
            foreach ($m[1] as $part) {
202
                $lines[] =  $this->parseLineString(trim($part, ' ()'));
203
            }
204
        }
205
        return new MultiLineString($lines);
206
    }
207
208
    /** @noinspection PhpUnusedPrivateMethodInspection
209
     * @param string $dataString
210
     *
211
     * @return MultiPolygon
212
     */
213
    private function parseMultiPolygon($dataString)
214
    {
215
        // If it's marked as empty, then return an empty multi-polygon
216
        if ($dataString == 'EMPTY') {
217
            return new MultiPolygon();
218
        }
219
220
        $polygons = [];
221
        if (preg_match_all('/(\(\([^(].+\)\)|EMPTY)/', $dataString, $m)) {
222
            foreach ($m[0] as $part) {
223
                $polygons[] =  $this->parsePolygon($part);
224
            }
225
        }
226
        return new MultiPolygon($polygons);
227
    }
228
229
    /** @noinspection PhpUnusedPrivateMethodInspection
230
     * @param string $dataString
231
     *
232
     * @return GeometryCollection
233
     */
234
    private function parseGeometryCollection($dataString)
235
    {
236
        // If it's marked as empty, then return an empty geom-collection
237
        if ($dataString == 'EMPTY') {
238
            return new GeometryCollection();
239
        }
240
241
        $geometries = [];
242
        while (strlen($dataString) > 0) {
243
            // Matches the first balanced parenthesis group (or term EMPTY)
244
            preg_match(
245
                '/\((?>[^()]+|(?R))*\)|EMPTY/',
246
                $dataString,
247
                $m,
248
                PREG_OFFSET_CAPTURE
249
            );
250
            if (!isset($m[0])) {
251
                // something weird happened, we stop here before running in an infinite loop
252
                break;
253
            }
254
            $cutPosition = strlen($m[0][0]) + $m[0][1];
255
            $geometry = $this->parseTypeAndGetData(trim(substr($dataString, 0, $cutPosition)));
256
            $geometries[] = $geometry;
257
            $dataString = trim(substr($dataString, $cutPosition + 1));
258
        }
259
260
        return new GeometryCollection($geometries);
261
    }
262
263
264
    /**
265
     * Serialize geometries into a WKT string.
266
     *
267
     * @param Geometry $geometry
268
     *
269
     * @return string The WKT string representation of the input geometries
270
     */
271
    public function write(Geometry $geometry)
272
    {
273
        // If geos is installed, then we take a shortcut and let it write the WKT
274
        if (geoPHP::geosInstalled()) {
275
            /** @noinspection PhpUndefinedClassInspection */
276
            $writer = new \GEOSWKTWriter();
277
            /** @noinspection PhpUndefinedMethodInspection */
278
            $writer->setRoundingPrecision(14);
279
            /** @noinspection PhpUndefinedMethodInspection */
280
            $writer->setTrim(true);
281
            /** @noinspection PhpUndefinedMethodInspection */
282
            return $writer->write($geometry->getGeos());
283
        }
284
        $this->measured = $geometry->isMeasured();
285
        $this->hasZ     = $geometry->hasZ();
286
287
        if ($geometry->isEmpty()) {
288
            return strtoupper($geometry->geometryType()) . ' EMPTY';
289
        }
290
291
        if ($data = $this->extractData($geometry)) {
292
            $extension = '';
293
            if ($this->hasZ) {
294
                $extension .= 'Z';
295
            }
296
            if ($this->measured) {
297
                $extension .= 'M';
298
            }
299
            return strtoupper($geometry->geometryType()) . ($extension ? ' ' . $extension : '') . ' (' . $data . ')';
300
        }
301
        return '';
302
    }
303
304
    /**
305
     * Extract geometry to a WKT string
306
     *
307
     * @param Geometry|Collection $geometry A Geometry object
308
     *
309
     * @return string
310
     */
311
    public function extractData($geometry)
312
    {
313
        $parts = [];
314
        switch ($geometry->geometryType()) {
315
            case Geometry::POINT:
316
                $p = $geometry->x() . ' ' . $geometry->y();
317
                if ($geometry->hasZ()) {
318
                    $p .= ' ' . $geometry->getZ();
319
                    $this->hasZ = $this->hasZ || $geometry->hasZ();
320
                }
321
                if ($geometry->isMeasured()) {
322
                    $p .= ' ' . $geometry->getM();
323
                    $this->measured = $this->measured || $geometry->isMeasured();
324
                }
325
                return $p;
326
            case Geometry::LINE_STRING:
327
                foreach ($geometry->getComponents() as $component) {
328
                    $parts[] = $this->extractData($component);
329
                }
330
                return implode(', ', $parts);
331
            case Geometry::POLYGON:
332
            case Geometry::MULTI_POINT:
333
            case Geometry::MULTI_LINE_STRING:
334
            case Geometry::MULTI_POLYGON:
335
                foreach ($geometry->getComponents() as $component) {
336
                    if ($component->isEmpty()) {
337
                        $parts[] = 'EMPTY';
338
                    } else {
339
                        $parts[] = '(' . $this->extractData($component) . ')';
340
                    }
341
                }
342
                return implode(', ', $parts);
343
            case Geometry::GEOMETRY_COLLECTION:
344
                foreach ($geometry->getComponents() as $component) {
345
                    $this->hasZ = $this->hasZ || $geometry->hasZ();
346
                    $this->measured = $this->measured || $geometry->isMeasured();
347
348
                    $extension = '';
349
                    if ($this->hasZ) {
350
                        $extension .= 'Z';
351
                    }
352
                    if ($this->measured) {
353
                        $extension .= 'M';
354
                    }
355
                    $data = $this->extractData($component);
356
                    $parts[] = strtoupper($component->geometryType())
357
                            . ($extension ? ' ' . $extension : '')
358
                            . ($data ? ' (' . $data . ')' : ' EMPTY');
359
                }
360
                return implode(', ', $parts);
361
        }
362
        return '';
363
    }
364
}
365