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