Passed
Push — master ( 5e6b25...984a75 )
by Swen
03:28
created

Collection   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 65
eloc 131
c 1
b 0
f 0
dl 0
loc 400
rs 3.2

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getComponents() 0 3 1
B __construct() 0 33 10
A invertXY() 0 7 2
A explode() 0 9 3
A asArray() 0 7 2
A getPoints() 0 7 1
A isMeasured() 0 3 1
A getBBoxWithGeos() 0 20 2
A hasZ() 0 3 1
B equals() 0 52 8
B getBBox() 0 41 10
A translate() 0 4 2
A getPointsRecursive() 0 8 3
A geometryN() 0 3 2
A numPoints() 0 7 2
A isEmpty() 0 8 3
A flatten() 0 9 4
B distance() 0 28 7
A numGeometries() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Collection 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 Collection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace geoPHP\Geometry;
4
5
use geoPHP\Exception\InvalidGeometryException;
6
use geoPHP\geoPHP;
7
8
/**
9
 * Collection: Abstract class for compound geometries
10
 *
11
 * A geometry is a collection if it is made up of other component geometries. Therefore everything except a Point
12
 * is a Collection. For example a LingString is a collection of Points. A Polygon is a collection of LineStrings etc.
13
 *
14
 * @package GeoPHPGeometry
15
 */
16
abstract class Collection extends Geometry
17
{
18
19
    /**
20
     * @var Geometry[]|Collection[]
21
     */
22
    protected $components = [];
23
24
    /**
25
     * @var bool True if Geometry has Z (altitude) value
26
     */
27
    protected $hasZ = false;
28
29
    /**
30
     * @var bool True if Geometry has M (measure) value
31
     */
32
    protected $isMeasured = false;
33
    
34
    /**
35
     * Constructor: Checks and sets component geometries
36
     *
37
     * @param  Geometry[] $components           array of geometries
38
     * @param  bool       $allowEmptyComponents Allow creating geometries with empty components. Default false.
39
     * @param  string     $allowedComponentType A class-type the components have to be instance of.
40
     * @throws InvalidGeometryException
41
     */
42
    public function __construct(
43
        array $components = [],
44
        bool $allowEmptyComponents = false,
45
        string $allowedComponentType = Geometry::class
46
    ) {
47
        $componentCount = count($components);
48
        for ($i = 0; $i < $componentCount; ++$i) {
49
            if ($components[$i] instanceof $allowedComponentType) {
50
                if (!$allowEmptyComponents && $components[$i]->isEmpty()) {
51
                    throw new InvalidGeometryException(
52
                        'Cannot create a collection of empty ' .
53
                            $components[$i]->geometryType() . 's (' . ($i + 1) . '. component)'
54
                    );
55
                }
56
                if ($components[$i]->hasZ() && !$this->hasZ) {
57
                    $this->hasZ = true;
58
                }
59
                if ($components[$i]->isMeasured() && !$this->isMeasured) {
60
                    $this->isMeasured = true;
61
                }
62
            } else {
63
                $componentType = gettype($components[$i]) !== 'object' ?
64
                    gettype($components[$i]) :
65
                    get_class($components[$i]);
66
                
67
                throw new InvalidGeometryException(
68
                    'Cannot create a collection of ' . $componentType . ' components, ' .
69
                        'expected type is ' . $allowedComponentType
70
                );
71
            }
72
        }
73
        
74
        $this->components = $components;
75
    }
76
77
    /**
78
     * Returns Collection component geometries
79
     *
80
     * @return Geometry[]
81
     */
82
    public function getComponents(): array
83
    {
84
        return $this->components;
85
    }
86
87
    /**
88
     * Inverts x and y coordinates
89
     * Useful for old data still using lng lat
90
     *
91
     * @return self
92
     * */
93
    public function invertXY(): self
94
    {
95
        foreach ($this->components as $component) {
96
            $component->invertXY();
97
        }
98
        $this->setGeos(null);
99
        return $this;
100
    }
101
102
    /**
103
     * @return array{'minx'?:float|null, 'miny'?:float|null, 'maxx'?:float|null, 'maxy'?:float|null}
104
     */
105
    public function getBBox(): array
106
    {
107
        if ($this->isEmpty()) {
108
            return [];
109
        }
110
111
        // use GEOS library
112
        $geosObj = $this->getGeos();
113
        if (is_object($geosObj)) {
114
            return $this->getBBoxWithGeos($geosObj);
115
        }
116
117
        // Go through each component and get the max and min x and y
118
        $maxX = $maxY = $minX = $minY = 0;
119
        foreach ($this->components as $i => $component) {
120
            $componentBoundingBox = $component->getBBox();
121
122
            if (empty($componentBoundingBox)) {
123
                continue;
124
            }
125
            
126
            // On the first run through, set the bounding box to the component's bounding box
127
            if ($i === 0) {
128
                $maxX = $componentBoundingBox['maxx'];
129
                $maxY = $componentBoundingBox['maxy'];
130
                $minX = $componentBoundingBox['minx'];
131
                $minY = $componentBoundingBox['miny'];
132
            }
133
134
            // Do a check and replace on each boundary, slowly growing the bounding box
135
            $maxX = $componentBoundingBox['maxx'] > $maxX ? $componentBoundingBox['maxx'] : $maxX;
136
            $maxY = $componentBoundingBox['maxy'] > $maxY ? $componentBoundingBox['maxy'] : $maxY;
137
            $minX = $componentBoundingBox['minx'] < $minX ? $componentBoundingBox['minx'] : $minX;
138
            $minY = $componentBoundingBox['miny'] < $minY ? $componentBoundingBox['miny'] : $minY;
139
        }
140
141
        return [
142
            'maxy' => $maxY,
143
            'miny' => $minY,
144
            'maxx' => $maxX,
145
            'minx' => $minX,
146
        ];
147
    }
148
    
149
    /**
150
     * @param \GEOSGeometry $geosObj
151
     * @return array{'minx'?:float|null, 'miny'?:float|null, 'maxx'?:float|null, 'maxy'?:float|null}
152
     */
153
    private function getBBoxWithGeos($geosObj): array
154
    {
155
        // @codeCoverageIgnoreStart
156
        /** @noinspection PhpUndefinedMethodInspection */
157
        $envelope = $geosObj->envelope();
158
        
159
        /** @noinspection PhpUndefinedMethodInspection */
160
        if ($envelope->typeName() === 'Point') {
161
            return geoPHP::geosToGeometry($envelope)->getBBox();
162
        }
163
164
        /** @noinspection PhpUndefinedMethodInspection */
165
        $geosRing = $envelope->exteriorRing();
166
        
167
        /** @noinspection PhpUndefinedMethodInspection */
168
        return [
169
            'maxy' => $geosRing->pointN(3)->getY(),
170
            'miny' => $geosRing->pointN(1)->getY(),
171
            'maxx' => $geosRing->pointN(1)->getX(),
172
            'minx' => $geosRing->pointN(3)->getX(),
173
        ];
174
        // @codeCoverageIgnoreEnd
175
    }
176
    
177
    /**
178
     * check if Geometry has a measure value
179
     *
180
     * @return bool true if collection has measure values
181
     */
182
    public function isMeasured(): bool
183
    {
184
        return $this->isMeasured;
185
    }
186
187
    /**
188
     * check if Geometry has Z (altitude) coordinate
189
     *
190
     * @return bool true if geometry has a Z-value
191
     */
192
    public function hasZ(): bool
193
    {
194
        return $this->hasZ;
195
    }
196
    
197
    /**
198
     * Returns every sub-geometry as a multidimensional array
199
     *
200
     * @return array<int, array>
201
     */
202
    public function asArray(): array
203
    {
204
        $array = [];
205
        foreach ($this->components as $component) {
206
            $array[] = $component->asArray();
207
        }
208
        return $array;
209
    }
210
211
    /**
212
     * @return int
213
     */
214
    public function numGeometries(): int
215
    {
216
        return count($this->components);
217
    }
218
219
    /**
220
     * Returns the 1-based Nth geometry.
221
     *
222
     * @param  int $n 1-based geometry number
223
     * @return Geometry|null
224
     */
225
    public function geometryN(int $n)
226
    {
227
        return isset($this->components[$n - 1]) ? $this->components[$n - 1] : null;
228
    }
229
230
    /**
231
     * A collection is not empty if it has at least one non empty component.
232
     *
233
     * @return bool
234
     */
235
    public function isEmpty(): bool
236
    {
237
        foreach ($this->components as $component) {
238
            if (!$component->isEmpty()) {
239
                return false;
240
            }
241
        }
242
        return true;
243
    }
244
245
    /**
246
     * @return int
247
     */
248
    public function numPoints(): int
249
    {
250
        $num = 0;
251
        foreach ($this->components as $component) {
252
            $num += $component->numPoints();
253
        }
254
        return $num;
255
    }
256
257
    /**
258
     * @return Point[]
259
     */
260
    public function getPoints(): array
261
    {
262
        $points = [];
263
264
        // Same as array_merge($points, $component->getPoints()), but 500× faster
265
        static::getPointsRecursive($this, $points);
266
        return $points;
267
    }
268
269
    /**
270
     * @param  Collection $geometry The geometry from which points will be extracted
271
     * @param  Point[]    $points   Result array as reference
272
     * @return void
273
     */
274
    private static function getPointsRecursive(Geometry $geometry, array &$points)
275
    {
276
        foreach ($geometry->components as $component) {
277
            if ($component instanceof Point) {
278
                $points[] = $component;
279
            } else {
280
                /** @var Collection $component */
281
                static::getPointsRecursive($component, $points);
282
            }
283
        }
284
    }
285
286
    /**
287
     * Returns TRUE if the given Geometries are "spatially equal".
288
     * Ordering of points can be different but represent the same geometry structure
289
     * 
290
     * @param  Geometry $geometry
291
     * @return bool
292
     */
293
    public function equals(Geometry $geometry): bool
294
    {
295
        $geosObj = $this->getGeos();
296
        if (is_object($geosObj)) {
297
            // @codeCoverageIgnoreStart
298
            /** @noinspection PhpUndefinedMethodInspection */
299
            $geosObj2 = $geometry->getGeos();
300
            return is_object($geosObj2) ? $geosObj->equals($geosObj2) : false;
301
            // @codeCoverageIgnoreEnd
302
        }
303
304
        $thisPoints = $this->getPoints();
305
        $otherPoints = $geometry->getPoints();
306
307
        /*
308
        // using some sort of simplification method that strips redundant vertices (that are all in a row)
309
        // Hint: this mehtod is mostly slower as long as number of points is less than 1000
310
        $asWkt = function(Point $pt){
311
            return implode(' ', $pt->asArray());
312
        };
313
        $ptsA = array_unique(array_map($asWkt, $thisPoints));
314
        $ptsB = array_unique(array_map($asWkt, $otherPoints));
315
316
        return count(array_diff($ptsA, $ptsB)) === 0;
317
        */
318
        
319
        // To test for equality we check to make sure that there is a matching point
320
        // in the other geometry for every point in this geometry.
321
        // This is slightly more strict than the standard, which
322
        // uses Within(A,B) = true and Within(B,A) = true
323
        
324
        // First do a check to make sure they have the same number of vertices
325
        if (count($thisPoints) !== count($otherPoints)) {
326
            return false;
327
        }
328
329
        foreach ($thisPoints as $point) {
330
            $foundMatch = false;
331
            foreach ($otherPoints as $key => $testPoint) {
332
                if ($point->equals($testPoint)) {
333
                    $foundMatch = true;
334
                    unset($otherPoints[$key]);
335
                    break;
336
                }
337
            }
338
            if (!$foundMatch) {
339
                return false;
340
            }
341
        }
342
343
        // All points match, return TRUE
344
        return true;
345
    }
346
347
    /**
348
     * Get all underlying components separated
349
     *
350
     * @param  bool $toArray return underlying components as LineStrings/Points or as array of coordinate values.
351
     * @return LineString[]|Point[]|array{}|array<array>
352
     */
353
    public function explode(bool $toArray = false): array
354
    {
355
        $parts = [];
356
        foreach ($this->getComponents() as $component) {
357
            foreach ($component->explode($toArray) as $part) {
358
                $parts[] = $part;
359
            }
360
        }
361
        return $parts;
362
    }
363
364
    /**
365
     * @return void
366
     */
367
    public function flatten()
368
    {
369
        if ($this->hasZ() || $this->isMeasured()) {
370
            foreach ($this->getComponents() as $component) {
371
                $component->flatten();
372
            }
373
            $this->hasZ = false;
374
            $this->isMeasured = false;
375
            $this->setGeos(null);
376
        }
377
    }
378
379
    /**
380
     * @return float|null
381
     */
382
    public function distance(Geometry $geometry)
383
    {
384
        $geosObj = $this->getGeos();
385
        if (is_object($geosObj)) {
386
            // @codeCoverageIgnoreStart
387
            /** @noinspection PhpUndefinedMethodInspection */
388
            $geosObj2 = $geometry->getGeos();
389
            return is_object($geosObj2) ? $geosObj->distance($geosObj2) : null;
390
            // @codeCoverageIgnoreEnd
391
        }
392
        
393
        $distance = null;
394
        foreach ($this->getComponents() as $component) {
395
            $checkDistance = $component->distance($geometry);
396
            if ($checkDistance === 0.0) {
397
                return 0.0;
398
            }
399
            if ($checkDistance === null) {
400
                continue;
401
            }
402
            $distance = ($distance ?? $checkDistance);
403
404
            if ($checkDistance < $distance) {
405
                $distance = $checkDistance;
406
            }
407
        }
408
        
409
        return $distance;
410
    }
411
    
412
    public function translate($dx = 0, $dy = 0, $dz = 0)
413
    {
414
        foreach ($this->getComponents() as $component) {
415
            $component->translate($dx, $dy, $dz);
416
        }
417
    }
418
}
419