1<?php
2
3namespace geoPHP\Geometry;
4
5use geoPHP\Exception\InvalidGeometryException;
6use 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
12 * component geometries. Therefore everything but a Point
13 * is a Collection. For example a LingString is a collection
14 * of Points. A Polygon is a collection of LineStrings etc.
15 */
16abstract class Collection extends Geometry
17{
18
19    /** @var Geometry[]|Collection[] */
20    protected $components = [];
21
22    /**
23     * Constructor: Checks and sets component geometries
24     *
25     * @param Geometry[] $components Array of geometries
26     * @param bool|true  $allowEmptyComponents Allow creating geometries with empty components
27     * @param string     $allowedComponentType A class the components must be instance of
28     *
29     * @throws \Exception
30     */
31    public function __construct(
32        $components = [],
33        $allowEmptyComponents = true,
34        $allowedComponentType = Geometry::class
35    ) {
36        if (!is_array($components)) {
37            throw new InvalidGeometryException("Component geometries must be passed as array");
38        }
39        $componentCount = count($components);
40        for ($i = 0; $i < $componentCount; ++$i) { // foreach is too memory-intensive here in PHP 5.*
41            if ($components[$i] instanceof $allowedComponentType) {
42                if (!$allowEmptyComponents && $components[$i]->isEmpty()) {
43                    throw new InvalidGeometryException(
44                        'Cannot create a collection of empty ' .
45                        $components[$i]->geometryType() . 's (' . ($i + 1) . '. component)'
46                    );
47                }
48                if ($components[$i]->hasZ() && !$this->hasZ) {
49                    $this->hasZ = true;
50                }
51                if ($components[$i]->isMeasured() && !$this->isMeasured) {
52                    $this->isMeasured = true;
53                }
54            } else {
55                $componentType = gettype($components[$i]) !== 'object'
56                    ? gettype($components[$i])
57                    : get_class($components[$i]);
58                throw new InvalidGeometryException(
59                    'Cannot create a collection of ' . $componentType .
60                    ' components, expected type is ' . $allowedComponentType
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()
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()
83    {
84        return $this->isMeasured;
85    }
86
87    /**
88     * Returns Collection component geometries
89     *
90     * @return Geometry[]
91     */
92    public function getComponents()
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     * */
104    public function invertXY()
105    {
106        foreach ($this->components as $component) {
107            $component->invertXY();
108        }
109        $this->setGeos(null);
110        return $this;
111    }
112
113    public function getBBox()
114    {
115        if ($this->isEmpty()) {
116            return null;
117        }
118
119        if ($this->getGeos()) {
120            // @codeCoverageIgnoreStart
121            /** @noinspection PhpUndefinedMethodInspection */
122            $envelope = $this->getGeos()->envelope();
123            /** @noinspection PhpUndefinedMethodInspection */
124            if ($envelope->typeName() == 'Point') {
125                return geoPHP::geosToGeometry($envelope)->getBBox();
126            }
127
128            /** @noinspection PhpUndefinedMethodInspection */
129            $geosRing = $envelope->exteriorRing();
130            /** @noinspection PhpUndefinedMethodInspection */
131            return [
132                    'maxy' => $geosRing->pointN(3)->getY(),
133                    'miny' => $geosRing->pointN(1)->getY(),
134                    'maxx' => $geosRing->pointN(1)->getX(),
135                    'minx' => $geosRing->pointN(3)->getX(),
136            ];
137            // @codeCoverageIgnoreEnd
138        }
139
140        // Go through each component and get the max and min x and y
141        $maxX = $maxY = $minX = $minY = 0;
142        foreach ($this->components as $i => $component) {
143            $componentBoundingBox = $component->getBBox();
144            if ($componentBoundingBox === null) {
145                continue;
146            }
147
148            // On the first run through, set the bounding box to the component's bounding box
149            if ($i == 0) {
150                $maxX = $componentBoundingBox['maxx'];
151                $maxY = $componentBoundingBox['maxy'];
152                $minX = $componentBoundingBox['minx'];
153                $minY = $componentBoundingBox['miny'];
154            }
155
156            // Do a check and replace on each boundary, slowly growing the bounding box
157            $maxX = $componentBoundingBox['maxx'] > $maxX ? $componentBoundingBox['maxx'] : $maxX;
158            $maxY = $componentBoundingBox['maxy'] > $maxY ? $componentBoundingBox['maxy'] : $maxY;
159            $minX = $componentBoundingBox['minx'] < $minX ? $componentBoundingBox['minx'] : $minX;
160            $minY = $componentBoundingBox['miny'] < $minY ? $componentBoundingBox['miny'] : $minY;
161        }
162
163        return [
164                'maxy' => $maxY,
165                'miny' => $minY,
166                'maxx' => $maxX,
167                'minx' => $minX,
168        ];
169    }
170
171    /**
172     * Returns every sub-geometry as a multidimensional array
173     *
174     * @return array
175     */
176    public function asArray()
177    {
178        $array = [];
179        foreach ($this->components as $component) {
180            $array[] = $component->asArray();
181        }
182        return $array;
183    }
184
185    /**
186     * @return int
187     */
188    public function numGeometries()
189    {
190        return count($this->components);
191    }
192
193    /**
194     * Returns the 1-based Nth geometry.
195     *
196     * @param int $n 1-based geometry number
197     * @return Geometry|null
198     */
199    public function geometryN($n)
200    {
201        return isset($this->components[$n - 1]) ? $this->components[$n - 1] : null;
202    }
203
204    /**
205     * A collection is not empty if it has at least one non empty component.
206     *
207     * @return bool
208     */
209    public function isEmpty()
210    {
211        foreach ($this->components as $component) {
212            if (!$component->isEmpty()) {
213                return false;
214            }
215        }
216        return true;
217    }
218
219    /**
220     * @return int
221     */
222    public function numPoints()
223    {
224        $num = 0;
225        foreach ($this->components as $component) {
226            $num += $component->numPoints();
227        }
228        return $num;
229    }
230
231    /**
232     * @return Point[]
233     */
234    public function getPoints()
235    {
236        $points = [];
237        // Same as array_merge($points, $component->getPoints()), but 500× faster
238        static::getPointsRecursive($this, $points);
239        return $points;
240    }
241
242    /**
243     * @param Collection $geometry The geometry from which points will be extracted
244     * @param Point[] $points Result array as reference
245     */
246    private static function getPointsRecursive($geometry, &$points)
247    {
248        foreach ($geometry->components as $component) {
249            if ($component instanceof Point) {
250                $points[] = $component;
251            } else {
252                static::getPointsRecursive($component, $points);
253            }
254        }
255    }
256
257    /**
258     * @param Geometry $geometry
259     * @return bool
260     */
261    public function equals($geometry)
262    {
263        if ($this->getGeos()) {
264            // @codeCoverageIgnoreStart
265            /** @noinspection PhpUndefinedMethodInspection */
266            return $this->getGeos()->equals($geometry->getGeos());
267            // @codeCoverageIgnoreEnd
268        }
269
270        // To test for equality we check to make sure that there is a matching point
271        // in the other geometry for every point in this geometry.
272        // This is slightly more strict than the standard, which
273        // uses Within(A,B) = true and Within(B,A) = true
274        // @@TODO: Eventually we could fix this by using some sort of simplification
275        // method that strips redundant vertices (that are all in a row)
276
277        $thisPoints = $this->getPoints();
278        $otherPoints = $geometry->getPoints();
279
280        // First do a check to make sure they have the same number of vertices
281        if (count($thisPoints) != count($otherPoints)) {
282            return false;
283        }
284
285        foreach ($thisPoints as $point) {
286            $foundMatch = false;
287            foreach ($otherPoints as $key => $testPoint) {
288                if ($point->equals($testPoint)) {
289                    $foundMatch = true;
290                    unset($otherPoints[$key]);
291                    break;
292                }
293            }
294            if (!$foundMatch) {
295                return false;
296            }
297        }
298
299        // All points match, return TRUE
300        return true;
301    }
302
303    /**
304     * Get all line segments
305     * @param bool $toArray return segments as LineString or array of start and end points. Explode(true) is faster
306     *
307     * @return LineString[] | Point[][]
308     */
309    public function explode($toArray = false)
310    {
311        $parts = [];
312        foreach ($this->components as $component) {
313            foreach ($component->explode($toArray) as $part) {
314                $parts[] = $part;
315            }
316        }
317        return $parts;
318    }
319
320    public function flatten()
321    {
322        if ($this->hasZ() || $this->isMeasured()) {
323            foreach ($this->components as $component) {
324                $component->flatten();
325            }
326            $this->hasZ = false;
327            $this->isMeasured = false;
328            $this->setGeos(null);
329        }
330    }
331
332    public function distance($geometry)
333    {
334        if ($this->getGeos()) {
335            // @codeCoverageIgnoreStart
336            /** @noinspection PhpUndefinedMethodInspection */
337            return $this->getGeos()->distance($geometry->getGeos());
338            // @codeCoverageIgnoreEnd
339        }
340        $distance = null;
341        foreach ($this->components as $component) {
342            $checkDistance = $component->distance($geometry);
343            if ($checkDistance === 0) {
344                return 0;
345            }
346            if ($checkDistance === null) {
347                return null;
348            }
349            if ($distance === null) {
350                $distance = $checkDistance;
351            }
352            if ($checkDistance < $distance) {
353                $distance = $checkDistance;
354            }
355        }
356        return $distance;
357    }
358
359    // Not valid for this geometry type
360    // --------------------------------
361    public function x()
362    {
363        return null;
364    }
365
366    public function y()
367    {
368        return null;
369    }
370
371    public function z()
372    {
373        return null;
374    }
375
376    public function m()
377    {
378        return null;
379    }
380}
381