Collection::equals()   B
last analyzed

Complexity

Conditions 8
Paths 10

Size

Total Lines 52
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 18
c 2
b 0
f 0
dl 0
loc 52
rs 8.4444
cc 8
nc 10
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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