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