Passed
Push — master ( 0dee84...d773ea )
by Swen
03:19
created

Collection::distance()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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