1<?php
2
3/**
4 * Collection: Abstract class for compound geometries
5 *
6 * A geometry is a collection if it is made up of other
7 * component geometries. Therefore everything but a Point
8 * is a Collection. For example a LingString is a collection
9 * of Points. A Polygon is a collection of LineStrings etc.
10 */
11abstract class Collection extends Geometry
12{
13  public $components = array();
14
15  /**
16   * Constructor: Checks and sets component geometries
17   *
18   * @param array $components array of geometries
19   */
20  public function __construct($components = array()) {
21    if (!is_array($components)) {
22      throw new Exception("Component geometries must be passed as an array");
23    }
24    foreach ($components as $component) {
25      if ($component instanceof Geometry) {
26        $this->components[] = $component;
27      }
28      else {
29        throw new Exception("Cannot create a collection with non-geometries");
30      }
31    }
32  }
33
34  /**
35   * Returns Collection component geometries
36   *
37   * @return array
38   */
39  public function getComponents() {
40    return $this->components;
41  }
42
43  /*
44   * Author : Adam Cherti
45   *
46   * inverts x and y coordinates
47   * Useful for old data still using lng lat
48   *
49   * @return void
50   *
51   * */
52  public function invertxy()
53  {
54	for($i=0;$i<count($this->components);$i++)
55	{
56		if( method_exists($this->components[$i], 'invertxy' ) )
57			$this->components[$i]->invertxy();
58	}
59  }
60
61  public function centroid() {
62    if ($this->isEmpty()) return NULL;
63
64    if ($this->geos()) {
65      $geos_centroid = $this->geos()->centroid();
66      if ($geos_centroid->typeName() == 'Point') {
67        return geoPHP::geosToGeometry($this->geos()->centroid());
68      }
69    }
70
71    // As a rough estimate, we say that the centroid of a colletion is the centroid of it's envelope
72    // @@TODO: Make this the centroid of the convexHull
73    // Note: Outside of polygons, geometryCollections and the trivial case of points, there is no standard on what a "centroid" is
74    $centroid = $this->envelope()->centroid();
75
76    return $centroid;
77  }
78
79  public function getBBox() {
80    if ($this->isEmpty()) return NULL;
81
82    if ($this->geos()) {
83      $envelope = $this->geos()->envelope();
84      if ($envelope->typeName() == 'Point') {
85        return geoPHP::geosToGeometry($envelope)->getBBOX();
86      }
87
88      $geos_ring = $envelope->exteriorRing();
89      return array(
90        'maxy' => $geos_ring->pointN(3)->getY(),
91        'miny' => $geos_ring->pointN(1)->getY(),
92        'maxx' => $geos_ring->pointN(1)->getX(),
93        'minx' => $geos_ring->pointN(3)->getX(),
94      );
95    }
96
97    // Go through each component and get the max and min x and y
98    $i = 0;
99    foreach ($this->components as $component) {
100      $component_bbox = $component->getBBox();
101
102      // On the first run through, set the bbox to the component bbox
103      if ($i == 0) {
104        $maxx = $component_bbox['maxx'];
105        $maxy = $component_bbox['maxy'];
106        $minx = $component_bbox['minx'];
107        $miny = $component_bbox['miny'];
108      }
109
110      // Do a check and replace on each boundary, slowly growing the bbox
111      $maxx = $component_bbox['maxx'] > $maxx ? $component_bbox['maxx'] : $maxx;
112      $maxy = $component_bbox['maxy'] > $maxy ? $component_bbox['maxy'] : $maxy;
113      $minx = $component_bbox['minx'] < $minx ? $component_bbox['minx'] : $minx;
114      $miny = $component_bbox['miny'] < $miny ? $component_bbox['miny'] : $miny;
115      $i++;
116    }
117
118    return array(
119      'maxy' => $maxy,
120      'miny' => $miny,
121      'maxx' => $maxx,
122      'minx' => $minx,
123    );
124  }
125
126  public function asArray() {
127    $array = array();
128    foreach ($this->components as $component) {
129      $array[] = $component->asArray();
130    }
131    return $array;
132  }
133
134  public function area() {
135    if ($this->geos()) {
136      return $this->geos()->area();
137    }
138
139    $area = 0;
140    foreach ($this->components as $component) {
141      $area += $component->area();
142    }
143    return $area;
144  }
145
146  // By default, the boundary of a collection is the boundary of it's components
147  public function boundary() {
148    if ($this->isEmpty()) return new LineString();
149
150    if ($this->geos()) {
151      return $this->geos()->boundary();
152    }
153
154    $components_boundaries = array();
155    foreach ($this->components as $component) {
156      $components_boundaries[] = $component->boundary();
157    }
158    return geoPHP::geometryReduce($components_boundaries);
159  }
160
161  public function numGeometries() {
162    return count($this->components);
163  }
164
165  // Note that the standard is 1 based indexing
166  public function geometryN($n) {
167    $n = intval($n);
168    if (array_key_exists($n-1, $this->components)) {
169      return $this->components[$n-1];
170    }
171    else {
172      return NULL;
173    }
174  }
175
176  public function length() {
177    $length = 0;
178    foreach ($this->components as $delta => $component) {
179      $length += $component->length();
180    }
181    return $length;
182  }
183
184  public function greatCircleLength($radius = 6378137) {
185    $length = 0;
186    foreach ($this->components as $component) {
187      $length += $component->greatCircleLength($radius);
188    }
189    return $length;
190  }
191
192  public function haversineLength() {
193    $length = 0;
194    foreach ($this->components as $component) {
195      $length += $component->haversineLength();
196    }
197    return $length;
198  }
199
200  public function dimension() {
201    $dimension = 0;
202    foreach ($this->components as $component) {
203      if ($component->dimension() > $dimension) {
204        $dimension = $component->dimension();
205      }
206    }
207    return $dimension;
208  }
209
210  // A collection is empty if it has no components OR all it's components are empty
211  public function isEmpty() {
212    if (!count($this->components)) {
213      return TRUE;
214    }
215    else {
216      foreach ($this->components as $component) {
217        if (!$component->isEmpty()) return FALSE;
218      }
219      return TRUE;
220    }
221  }
222
223  public function numPoints() {
224    $num = 0;
225    foreach ($this->components as $component) {
226      $num += $component->numPoints();
227    }
228    return $num;
229  }
230
231  public function getPoints() {
232    $points = array();
233    foreach ($this->components as $component) {
234      $points = array_merge($points, $component->getPoints());
235    }
236    return $points;
237  }
238
239  public function equals($geometry) {
240    if ($this->geos()) {
241      return $this->geos()->equals($geometry->geos());
242    }
243
244    // To test for equality we check to make sure that there is a matching point
245    // in the other geometry for every point in this geometry.
246    // This is slightly more strict than the standard, which
247    // uses Within(A,B) = true and Within(B,A) = true
248    // @@TODO: Eventually we could fix this by using some sort of simplification
249    // method that strips redundant vertices (that are all in a row)
250
251    $this_points = $this->getPoints();
252    $other_points = $geometry->getPoints();
253
254    // First do a check to make sure they have the same number of vertices
255    if (count($this_points) != count($other_points)) {
256      return FALSE;
257    }
258
259    foreach ($this_points as $point) {
260      $found_match = FALSE;
261      foreach ($other_points as $key => $test_point) {
262        if ($point->equals($test_point)) {
263          $found_match = TRUE;
264          unset($other_points[$key]);
265          break;
266        }
267      }
268      if (!$found_match) {
269        return FALSE;
270      }
271    }
272
273    // All points match, return TRUE
274    return TRUE;
275  }
276
277  public function isSimple() {
278    if ($this->geos()) {
279      return $this->geos()->isSimple();
280    }
281
282    // A collection is simple if all it's components are simple
283    foreach ($this->components as $component) {
284      if (!$component->isSimple()) return FALSE;
285    }
286
287    return TRUE;
288  }
289
290  public function explode() {
291    $parts = array();
292    foreach ($this->components as $component) {
293      $parts = array_merge($parts, $component->explode());
294    }
295    return $parts;
296  }
297
298  // Not valid for this geometry type
299  // --------------------------------
300  public function x()                { return NULL; }
301  public function y()                { return NULL; }
302  public function startPoint()       { return NULL; }
303  public function endPoint()         { return NULL; }
304  public function isRing()           { return NULL; }
305  public function isClosed()         { return NULL; }
306  public function pointN($n)         { return NULL; }
307  public function exteriorRing()     { return NULL; }
308  public function numInteriorRings() { return NULL; }
309  public function interiorRingN($n)  { return NULL; }
310  public function pointOnSurface()   { return NULL; }
311}
312
313