Passed
Push — master ( bb1b1f...168ada )
by Swen
03:02
created

Collection::is3D()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 0
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
     * Constructor: Checks and sets component geometries
26
     *
27
     * @param  Geometry[] $components           array of geometries
28
     * @param  bool       $allowEmptyComponents Allow creating geometries with empty components. Default false.
29
     * @param  string     $allowedComponentType A class-type the components have to be instance of.
30
     * @throws InvalidGeometryException
31
     */
32
    public function __construct(
33
        array $components = [],
34
        bool $allowEmptyComponents = false,
35
        string $allowedComponentType = Geometry::class
36
    ) {
37
        $componentCount = count($components);
38
        for ($i = 0; $i < $componentCount; ++$i) {
39
            if ($components[$i] instanceof $allowedComponentType) {
40
                if (!$allowEmptyComponents && $components[$i]->isEmpty()) {
41
                    throw new InvalidGeometryException(
42
                        'Cannot create a collection of empty ' .
43
                            $components[$i]->geometryType() . 's (' . ($i + 1) . '. component)'
44
                    );
45
                }
46
                if ($components[$i]->hasZ() && !$this->hasZ) {
47
                    $this->hasZ = true;
0 ignored issues
show
Bug Best Practice introduced by
The property hasZ does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
48
                }
49
                if ($components[$i]->isMeasured() && !$this->isMeasured) {
50
                    $this->isMeasured = true;
0 ignored issues
show
Bug Best Practice introduced by
The property isMeasured does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
51
                }
52
            } else {
53
                $componentType = gettype($components[$i]) !== 'object' ?
54
                    gettype($components[$i]) :
55
                    get_class($components[$i]);
56
                
57
                throw new InvalidGeometryException(
58
                    'Cannot create a collection of ' . $componentType . ' components, ' .
59
                        'expected type is ' . $allowedComponentType
60
                );
61
            }
62
        }
63
        
64
        $this->components = $components;
65
    }
66
67
    /**
68
     * Returns Collection component geometries
69
     *
70
     * @return Geometry[]
71
     */
72
    public function getComponents(): array
73
    {
74
        return $this->components;
75
    }
76
77
    /**
78
     * Inverts x and y coordinates
79
     * Useful for old data still using lng lat
80
     *
81
     * @return self
82
     * */
83
    public function invertXY(): self
84
    {
85
        foreach ($this->components as $component) {
86
            $component->invertXY();
87
        }
88
        $this->setGeos(null);
89
        return $this;
90
    }
91
92
    /**
93
     * @return array{'minx'?:float|null, 'miny'?:float|null, 'maxx'?:float|null, 'maxy'?:float|null}
94
     */
95
    public function getBBox(): array
96
    {
97
        if ($this->isEmpty()) {
98
            return [];
99
        }
100
101
        $geosObj = $this->getGeos();
102
        if (is_object($geosObj)) {
103
            // @codeCoverageIgnoreStart
104
            /** @noinspection PhpUndefinedMethodInspection */
105
            $envelope = $geosObj->envelope();
106
            /** @noinspection PhpUndefinedMethodInspection */
107
            if ($envelope->typeName() === 'Point') {
108
                return geoPHP::geosToGeometry($envelope)->getBBox();
109
            }
110
111
            /** @noinspection PhpUndefinedMethodInspection */
112
            $geosRing = $envelope->exteriorRing();
113
            /** @noinspection PhpUndefinedMethodInspection */
114
            return [
115
                'maxy' => $geosRing->pointN(3)->getY(),
116
                'miny' => $geosRing->pointN(1)->getY(),
117
                'maxx' => $geosRing->pointN(1)->getX(),
118
                'minx' => $geosRing->pointN(3)->getX(),
119
            ];
120
            // @codeCoverageIgnoreEnd
121
        }
122
123
        // Go through each component and get the max and min x and y
124
        $maxX = $maxY = $minX = $minY = 0;
125
        foreach ($this->components as $i => $component) {
126
            $componentBoundingBox = $component->getBBox();
127
128
            if (empty($componentBoundingBox)) {
129
                continue;
130
            }
131
            
132
            // On the first run through, set the bounding box to the component's bounding box
133
            if ($i === 0) {
134
                $maxX = $componentBoundingBox['maxx'];
135
                $maxY = $componentBoundingBox['maxy'];
136
                $minX = $componentBoundingBox['minx'];
137
                $minY = $componentBoundingBox['miny'];
138
            }
139
140
            // Do a check and replace on each boundary, slowly growing the bounding box
141
            $maxX = $componentBoundingBox['maxx'] > $maxX ? $componentBoundingBox['maxx'] : $maxX;
142
            $maxY = $componentBoundingBox['maxy'] > $maxY ? $componentBoundingBox['maxy'] : $maxY;
143
            $minX = $componentBoundingBox['minx'] < $minX ? $componentBoundingBox['minx'] : $minX;
144
            $minY = $componentBoundingBox['miny'] < $minY ? $componentBoundingBox['miny'] : $minY;
145
        }
146
147
        return [
148
            'maxy' => $maxY,
149
            'miny' => $minY,
150
            'maxx' => $maxX,
151
            'minx' => $minX,
152
        ];
153
    }
154
155
    /**
156
     * Returns every sub-geometry as a multidimensional array
157
     *
158
     * @return array<int, array>
159
     */
160
    public function asArray(): array
161
    {
162
        $array = [];
163
        foreach ($this->components as $component) {
164
            $array[] = $component->asArray();
165
        }
166
        return $array;
167
    }
168
169
    /**
170
     * @return int
171
     */
172
    public function numGeometries(): int
173
    {
174
        return count($this->components);
175
    }
176
177
    /**
178
     * Returns the 1-based Nth geometry.
179
     *
180
     * @param  int $n 1-based geometry number
181
     * @return Geometry|null
182
     */
183
    public function geometryN(int $n)
184
    {
185
        return isset($this->components[$n - 1]) ? $this->components[$n - 1] : null;
186
    }
187
188
    /**
189
     * A collection is not empty if it has at least one non empty component.
190
     *
191
     * @return bool
192
     */
193
    public function isEmpty(): bool
194
    {
195
        foreach ($this->components as $component) {
196
            if (!$component->isEmpty()) {
197
                return false;
198
            }
199
        }
200
        return true;
201
    }
202
203
    /**
204
     * @return int
205
     */
206
    public function numPoints(): int
207
    {
208
        $num = 0;
209
        foreach ($this->components as $component) {
210
            $num += $component->numPoints();
211
        }
212
        return $num;
213
    }
214
215
    /**
216
     * @return Point[]
217
     */
218
    public function getPoints(): array
219
    {
220
        $points = [];
221
222
        // Same as array_merge($points, $component->getPoints()), but 500× faster
223
        static::getPointsRecursive($this, $points);
224
        return $points;
225
    }
226
227
    /**
228
     * @param  Collection $geometry The geometry from which points will be extracted
229
     * @param  Point[]    $points   Result array as reference
230
     * @return void
231
     */
232
    private static function getPointsRecursive(Geometry $geometry, array &$points)
233
    {
234
        foreach ($geometry->components as $component) {
235
            if ($component instanceof Point) {
236
                $points[] = $component;
237
            } else {
238
                /** @var Collection $component */
239
                static::getPointsRecursive($component, $points);
240
            }
241
        }
242
    }
243
244
    /**
245
     * @todo   speed it up with array_diff
246
     * @param  Geometry $geometry
247
     * @return bool
248
     */
249
    public function equals(Geometry $geometry): bool
250
    {
251
        $geosObj = $this->getGeos();
252
        if (is_object($geosObj)) {
253
            // @codeCoverageIgnoreStart
254
            /** @noinspection PhpUndefinedMethodInspection */
255
            $geosObj2 = $geometry->getGeos();
256
            return is_object($geosObj2) ? $geosObj->equals($geosObj2) : false;
257
            // @codeCoverageIgnoreEnd
258
        }
259
260
        // To test for equality we check to make sure that there is a matching point
261
        // in the other geometry for every point in this geometry.
262
        // This is slightly more strict than the standard, which
263
        // uses Within(A,B) = true and Within(B,A) = true
264
        // @@TODO: Eventually we could fix this by using some sort of simplification
265
        // method that strips redundant vertices (that are all in a row)
266
267
        $thisPoints = $this->getPoints();
268
        $otherPoints = $geometry->getPoints();
269
270
        // First do a check to make sure they have the same number of vertices
271
        if (count($thisPoints) !== count($otherPoints)) {
272
            return false;
273
        }
274
275
        foreach ($thisPoints as $point) {
276
            $foundMatch = false;
277
            foreach ($otherPoints as $key => $testPoint) {
278
                if ($point->equals($testPoint)) {
279
                    $foundMatch = true;
280
                    unset($otherPoints[$key]);
281
                    break;
282
                }
283
            }
284
            if (!$foundMatch) {
285
                return false;
286
            }
287
        }
288
289
        // All points match, return TRUE
290
        return true;
291
    }
292
293
    /**
294
     * Get all underlying components separated
295
     *
296
     * @param  bool $toArray return underlying components as LineStrings/Points or as array of coordinate values.
297
     * @return LineString[]|Point[]|array{}|array<array>
298
     */
299
    public function explode(bool $toArray = false): array
300
    {
301
        $parts = [];
302
        foreach ($this->getComponents() as $component) {
303
            foreach ($component->explode($toArray) as $part) {
304
                $parts[] = $part;
305
            }
306
        }
307
        return $parts;
308
    }
309
310
    /**
311
     * @return void
312
     */
313
    public function flatten()
314
    {
315
        if ($this->hasZ() || $this->isMeasured()) {
316
            foreach ($this->getComponents() as $component) {
317
                $component->flatten();
318
            }
319
            $this->hasZ = false;
0 ignored issues
show
Bug Best Practice introduced by
The property hasZ does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
320
            $this->isMeasured = false;
0 ignored issues
show
Bug Best Practice introduced by
The property isMeasured does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
321
            $this->setGeos(null);
322
        }
323
    }
324
325
    /**
326
     * @return float|null
327
     */
328
    public function distance(Geometry $geometry)
329
    {
330
        $geosObj = $this->getGeos();
331
        if (is_object($geosObj)) {
332
            // @codeCoverageIgnoreStart
333
            /** @noinspection PhpUndefinedMethodInspection */
334
            $geosObj2 = $geometry->getGeos();
335
            return is_object($geosObj2) ? $geosObj->distance($geosObj2) : null;
336
            // @codeCoverageIgnoreEnd
337
        }
338
        
339
        $distance = null;
340
        foreach ($this->getComponents() as $component) {
341
            $checkDistance = $component->distance($geometry);
342
            if ($checkDistance === 0.0) {
343
                return 0.0;
344
            }
345
            if ($checkDistance === null) {
346
                continue;
347
            }
348
            $distance = ($distance ?? $checkDistance);
349
350
            if ($checkDistance < $distance) {
351
                $distance = $checkDistance;
352
            }
353
        }
354
        
355
        return $distance;
356
    }
357
    
358
    public function translate($dx = 0, $dy = 0, $dz = 0)
359
    {
360
        foreach ($this->getComponents() as $component) {
361
            $component->translate($dx, $dy, $dz);
362
        }
363
    }
364
}
365