| Total Complexity | 82 | 
| Total Lines | 436 | 
| Duplicated Lines | 0 % | 
| Changes | 4 | ||
| Bugs | 0 | Features | 1 | 
Complex classes like Polygon often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Polygon, and based on these observations, apply Extract Interface, too.
| 1 | <?php  | 
            ||
| 16 | class Polygon extends Surface  | 
            ||
| 17 | { | 
            ||
| 18 | |||
| 19 | /**  | 
            ||
| 20 | * @param LineString[] $components  | 
            ||
| 21 | * @param bool $forceCreate force creation even if polygon is invalid because it is not closed.  | 
            ||
| 22 | * Default false.  | 
            ||
| 23 | * @throws InvalidGeometryException  | 
            ||
| 24 | */  | 
            ||
| 25 | public function __construct(array $components = [], bool $forceCreate = false)  | 
            ||
| 26 |     { | 
            ||
| 27 | parent::__construct($components, true, LineString::class);  | 
            ||
| 28 | |||
| 29 |         foreach ($this->getComponents() as $i => $component) { | 
            ||
| 30 |             if ($component->numPoints() < 4) { | 
            ||
| 31 | throw new InvalidGeometryException(  | 
            ||
| 32 | 'Cannot create Polygon: Invalid number of points in LinearRing. Found ' .  | 
            ||
| 33 | $component->numPoints() . ', but expected more than 3.'  | 
            ||
| 34 | );  | 
            ||
| 35 | }  | 
            ||
| 36 |             if (!$component->isClosed()) { | 
            ||
| 37 |                 if ($forceCreate) { | 
            ||
| 38 | $this->components[$i] = new LineString(  | 
            ||
| 39 | array_merge($component->getComponents(), [$component->startPoint()])  | 
            ||
| 40 | );  | 
            ||
| 41 |                 } else { | 
            ||
| 42 | $startPt = $component->startPoint();  | 
            ||
| 43 | $endPt = $component->endPoint();  | 
            ||
| 44 | throw new InvalidGeometryException(  | 
            ||
| 45 | 'Cannot create Polygon: contains a non-closed ring (first point: '  | 
            ||
| 46 |                             . implode(' ', $startPt->asArray()) . ', last point: ' | 
            ||
| 47 |                             . implode(' ', $endPt->asArray()) . ')' | 
            ||
| 48 | );  | 
            ||
| 49 | }  | 
            ||
| 50 | }  | 
            ||
| 51 | // This check is tooo expensive  | 
            ||
| 52 |             //if (!$component->isSimple() && !$forceCreate) { | 
            ||
| 53 |             //    throw new \Exception('Cannot create Polygon: geometry should be simple'); | 
            ||
| 54 | //}  | 
            ||
| 55 | }  | 
            ||
| 56 | }  | 
            ||
| 57 | |||
| 58 | /**  | 
            ||
| 59 | * @return string "Polygon"  | 
            ||
| 60 | */  | 
            ||
| 61 | public function geometryType(): string  | 
            ||
| 62 |     { | 
            ||
| 63 | return Geometry::POLYGON;  | 
            ||
| 64 | }  | 
            ||
| 65 | |||
| 66 | /**  | 
            ||
| 67 | * @return int 2  | 
            ||
| 68 | */  | 
            ||
| 69 | public function dimension(): int  | 
            ||
| 70 |     { | 
            ||
| 71 | return 2;  | 
            ||
| 72 | }  | 
            ||
| 73 | |||
| 74 | /**  | 
            ||
| 75 | * @param bool|false $exteriorOnly Calculate the area of exterior ring only, or the polygon with holes  | 
            ||
| 76 | * @param bool|false $signed Usually we want to get positive area, but vertices order (CW or CCW) can be  | 
            ||
| 77 | * determined from signed area.  | 
            ||
| 78 | *  | 
            ||
| 79 | * @return float  | 
            ||
| 80 | */  | 
            ||
| 81 | public function getArea(bool $exteriorOnly = false, bool $signed = false): float  | 
            ||
| 82 |     { | 
            ||
| 83 |         if ($this->isEmpty()) { | 
            ||
| 84 | return 0.0;  | 
            ||
| 85 | }  | 
            ||
| 86 | |||
| 87 | $geosObj = $this->getGeos();  | 
            ||
| 88 |         if (is_object($geosObj) && $exteriorOnly === false) { | 
            ||
| 89 | // @codeCoverageIgnoreStart  | 
            ||
| 90 | /** @noinspection PhpUndefinedMethodInspection */  | 
            ||
| 91 | return (float) $geosObj->area();  | 
            ||
| 92 | // @codeCoverageIgnoreEnd  | 
            ||
| 93 | }  | 
            ||
| 94 | |||
| 95 | $exteriorRing = $this->components[0];  | 
            ||
| 96 | $points = $exteriorRing->getComponents();  | 
            ||
| 97 | |||
| 98 | $numPoints = count($points);  | 
            ||
| 99 |         if ($numPoints === 0) { | 
            ||
| 100 | return 0.0;  | 
            ||
| 101 | }  | 
            ||
| 102 | $a = 0;  | 
            ||
| 103 |         foreach ($points as $k => $p) { | 
            ||
| 104 | $j = ($k + 1) % $numPoints;  | 
            ||
| 105 | $a = $a + ($p->getX() * $points[$j]->getY()) - ($p->getY() * $points[$j]->getX());  | 
            ||
| 106 | }  | 
            ||
| 107 | |||
| 108 | $area = $signed ? ($a / 2) : abs(($a / 2));  | 
            ||
| 109 | |||
| 110 |         if ($exteriorOnly === true) { | 
            ||
| 111 | return (float) $area;  | 
            ||
| 112 | }  | 
            ||
| 113 |         foreach ($this->components as $delta => $component) { | 
            ||
| 114 |             if ($delta != 0) { | 
            ||
| 115 | $innerPoly = new Polygon([$component]);  | 
            ||
| 116 | $area -= $innerPoly->getArea();  | 
            ||
| 117 | }  | 
            ||
| 118 | }  | 
            ||
| 119 | return (float) $area;  | 
            ||
| 120 | }  | 
            ||
| 121 | |||
| 122 | /**  | 
            ||
| 123 | * @return Point  | 
            ||
| 124 | */  | 
            ||
| 125 | public function getCentroid(): Point  | 
            ||
| 126 |     { | 
            ||
| 127 |         if ($this->isEmpty()) { | 
            ||
| 128 | return new Point();  | 
            ||
| 129 | }  | 
            ||
| 130 | |||
| 131 | $geosObj = $this->getGeos();  | 
            ||
| 132 |         if (is_object($geosObj)) { | 
            ||
| 133 | // @codeCoverageIgnoreStart  | 
            ||
| 134 | /** @noinspection PhpUndefinedMethodInspection */  | 
            ||
| 135 | /** @var Point|null $geometry */  | 
            ||
| 136 | $geometry = geoPHP::geosToGeometry($geosObj->centroid());  | 
            ||
| 137 | return $geometry !== null ? $geometry : new Point();  | 
            ||
| 138 | // @codeCoverageIgnoreEnd  | 
            ||
| 139 | }  | 
            ||
| 140 | |||
| 141 | $x = 0;  | 
            ||
| 142 | $y = 0;  | 
            ||
| 143 | $totalArea = 0.0;  | 
            ||
| 144 |         foreach ($this->getComponents() as $i => $component) { | 
            ||
| 145 | $ca = $this->getRingCentroidAndArea($component);  | 
            ||
| 146 |             if ($i === 0) { | 
            ||
| 147 | $totalArea += $ca['area'];  | 
            ||
| 148 | $x += $ca['x'] * $ca['area'];  | 
            ||
| 149 | $y += $ca['y'] * $ca['area'];  | 
            ||
| 150 |             } else { | 
            ||
| 151 | $totalArea -= $ca['area'];  | 
            ||
| 152 | $x -= $ca['x'] * $ca['area'];  | 
            ||
| 153 | $y -= $ca['y'] * $ca['area'];  | 
            ||
| 154 | }  | 
            ||
| 155 | }  | 
            ||
| 156 | |||
| 157 | return $totalArea !== 0.0 ? new Point($x / $totalArea, $y / $totalArea) : new Point();  | 
            ||
| 158 | }  | 
            ||
| 159 | |||
| 160 | /**  | 
            ||
| 161 | * @param LineString $ring  | 
            ||
| 162 | * @return array<string, int|float|null>  | 
            ||
| 163 | */  | 
            ||
| 164 | protected function getRingCentroidAndArea(LineString $ring): array  | 
            ||
| 186 | ];  | 
            ||
| 187 | }  | 
            ||
| 188 | |||
| 189 | /**  | 
            ||
| 190 | * Find the outermost point from the centroid  | 
            ||
| 191 | *  | 
            ||
| 192 | * @return Point the outermost point  | 
            ||
| 193 | */  | 
            ||
| 194 | public function outermostPoint(): Point  | 
            ||
| 195 |     { | 
            ||
| 196 | $centroid = $this->getCentroid();  | 
            ||
| 197 |         if ($centroid->isEmpty()) { | 
            ||
| 198 | return $centroid;  | 
            ||
| 199 | }  | 
            ||
| 200 | |||
| 201 | $maxDistance = 0;  | 
            ||
| 202 | $maxPoint = new Point;  | 
            ||
| 203 | |||
| 204 |         foreach ($this->exteriorRing()->getPoints() as $point) { | 
            ||
| 205 | $distance = $centroid->distance($point);  | 
            ||
| 206 | |||
| 207 |             if ($distance > $maxDistance) { | 
            ||
| 208 | $maxDistance = $distance;  | 
            ||
| 209 | $maxPoint = $point;  | 
            ||
| 210 | }  | 
            ||
| 211 | }  | 
            ||
| 212 | |||
| 213 | return $maxPoint;  | 
            ||
| 214 | }  | 
            ||
| 215 | |||
| 216 | /**  | 
            ||
| 217 | * @return LineString  | 
            ||
| 218 | */  | 
            ||
| 219 | public function exteriorRing()  | 
            ||
| 220 |     { | 
            ||
| 221 |         if ($this->isEmpty()) { | 
            ||
| 222 | return new LineString();  | 
            ||
| 223 | }  | 
            ||
| 224 | return $this->components[0];  | 
            ||
| 225 | }  | 
            ||
| 226 | |||
| 227 | /**  | 
            ||
| 228 | * @return int  | 
            ||
| 229 | */  | 
            ||
| 230 | public function numInteriorRings(): int  | 
            ||
| 231 |     { | 
            ||
| 232 | return $this->isEmpty() ? 0 : $this->numGeometries() - 1;  | 
            ||
| 233 | }  | 
            ||
| 234 | |||
| 235 | /**  | 
            ||
| 236 | * Returns the linestring for the nth interior ring of the polygon. Interior rings are holes in the polygon.  | 
            ||
| 237 | *  | 
            ||
| 238 | * @param int $n 1-based geometry number  | 
            ||
| 239 | * @return Geometry|null  | 
            ||
| 240 | */  | 
            ||
| 241 | public function interiorRingN(int $n)  | 
            ||
| 242 |     { | 
            ||
| 243 | return $n > $this->numInteriorRings() ? new LineString : $this->geometryN($n + 1);  | 
            ||
| 244 | }  | 
            ||
| 245 | |||
| 246 | /**  | 
            ||
| 247 | * @return bool  | 
            ||
| 248 | */  | 
            ||
| 249 | public function isSimple(): bool  | 
            ||
| 250 |     { | 
            ||
| 251 | $geosObj = $this->getGeos();  | 
            ||
| 252 |         if (is_object($geosObj)) { | 
            ||
| 253 | // @codeCoverageIgnoreStart  | 
            ||
| 254 | /** @noinspection PhpUndefinedMethodInspection */  | 
            ||
| 255 | return $geosObj->isSimple();  | 
            ||
| 256 | // @codeCoverageIgnoreEnd  | 
            ||
| 257 | }  | 
            ||
| 258 | |||
| 259 | /** @var array<array> $segments */  | 
            ||
| 260 | $segments = $this->explode(true);  | 
            ||
| 261 | |||
| 262 | //TODO: instead of this O(n^2) algorithm implement Shamos-Hoey Algorithm which is only O(n*log(n))  | 
            ||
| 263 |         foreach ($segments as $i => $segment) { | 
            ||
| 264 |             foreach ($segments as $j => $checkSegment) { | 
            ||
| 265 |                 if ($i != $j) { | 
            ||
| 266 |                     if (Geometry::segmentIntersects($segment[0], $segment[1], $checkSegment[0], $checkSegment[1])) { | 
            ||
| 267 | return false;  | 
            ||
| 268 | }  | 
            ||
| 269 | }  | 
            ||
| 270 | }  | 
            ||
| 271 | }  | 
            ||
| 272 | |||
| 273 | return true;  | 
            ||
| 274 | }  | 
            ||
| 275 | |||
| 276 | /**  | 
            ||
| 277 | * If GEOS is not available, it is still a quite simple test of validity for polygons.  | 
            ||
| 278 | * E.g. a test for self-intersections is missing.  | 
            ||
| 279 | *  | 
            ||
| 280 | * @return bool  | 
            ||
| 281 | */  | 
            ||
| 282 | public function isValid(): bool  | 
            ||
| 310 | }  | 
            ||
| 311 | |||
| 312 | /**  | 
            ||
| 313 | * For a given point, determine whether it's bounded by the given polygon.  | 
            ||
| 314 | * Adapted from @source http://www.assemblysys.com/dataServices/php_pointinpolygon.php  | 
            ||
| 315 | *  | 
            ||
| 316 | * @see http://en.wikipedia.org/wiki/Point%5Fin%5Fpolygon  | 
            ||
| 317 | *  | 
            ||
| 318 | * @param Point $point  | 
            ||
| 319 | * @param boolean $pointOnBoundary - whether a boundary should be considered "in" or not  | 
            ||
| 320 | * @param boolean $pointOnVertex - whether a vertex should be considered "in" or not  | 
            ||
| 321 | * @return bool  | 
            ||
| 322 | */  | 
            ||
| 323 | public function pointInPolygon(Point $point, bool $pointOnBoundary = true, bool $pointOnVertex = true): bool  | 
            ||
| 366 | }  | 
            ||
| 367 | |||
| 368 | /**  | 
            ||
| 369 | * @param Point $point  | 
            ||
| 370 | * @return bool  | 
            ||
| 371 | */  | 
            ||
| 372 | public function pointOnVertex(Point $point): bool  | 
            ||
| 380 | }  | 
            ||
| 381 | |||
| 382 | /**  | 
            ||
| 383 | * Checks whether the given geometry is spatially inside the Polygon  | 
            ||
| 384 | * TODO: rewrite this. Currently supports point, linestring and polygon with only outer ring  | 
            ||
| 385 | *  | 
            ||
| 386 | * @param Geometry $geometry  | 
            ||
| 387 | * @return bool  | 
            ||
| 388 | */  | 
            ||
| 389 | public function contains(Geometry $geometry): bool  | 
            ||
| 390 |     { | 
            ||
| 391 | $geosObj = $this->getGeos();  | 
            ||
| 392 |         if (is_object($geosObj)) { | 
            ||
| 393 | // @codeCoverageIgnoreStart  | 
            ||
| 394 | /** @noinspection PhpUndefinedMethodInspection */  | 
            ||
| 395 | $geosObj2 = $geometry->getGeos();  | 
            ||
| 396 | return $geosObj2 !== false ? $geosObj->contains($geosObj2) : false;  | 
            ||
| 397 | // @codeCoverageIgnoreEnd  | 
            ||
| 398 | }  | 
            ||
| 399 | |||
| 400 | $isInside = false;  | 
            ||
| 401 |         foreach ($geometry->getPoints() as $p) { | 
            ||
| 402 |             if ($this->pointInPolygon($p)) { | 
            ||
| 403 | $isInside = true; // at least one point of the innerPoly is inside the outerPoly  | 
            ||
| 404 | break;  | 
            ||
| 405 | }  | 
            ||
| 406 | }  | 
            ||
| 407 | // none of the vertices intersects  | 
            ||
| 408 |         if (!$isInside) { | 
            ||
| 409 | return false;  | 
            ||
| 410 | }  | 
            ||
| 411 | |||
| 412 |         if ($geometry->geometryType() === Geometry::LINESTRING) { | 
            ||
| 413 | // do nothing  | 
            ||
| 414 |         } elseif ($geometry->geometryType() === Geometry::POINT) { | 
            ||
| 415 | return true;  | 
            ||
| 416 |         } elseif ($geometry->geometryType() === Geometry::POLYGON) { | 
            ||
| 417 | $geometry = $geometry->exteriorRing();  | 
            ||
| 418 |         } else { | 
            ||
| 419 | return false;  | 
            ||
| 420 | }  | 
            ||
| 421 | |||
| 422 | /** @var array<array> $outerRing */  | 
            ||
| 423 | $outerRing = $this->exteriorRing()->explode(true);  | 
            ||
| 424 | |||
| 425 | /** @var Point[] $innerEdge */  | 
            ||
| 426 |         foreach ($geometry->explode(true) as $innerEdge) { | 
            ||
| 427 |             foreach ($outerRing as $outerEdge) { | 
            ||
| 428 |                 if (Geometry::segmentIntersects($innerEdge[0], $innerEdge[1], $outerEdge[0], $outerEdge[1])) { | 
            ||
| 429 | return false;  | 
            ||
| 430 | }  | 
            ||
| 431 | }  | 
            ||
| 432 | }  | 
            ||
| 433 | |||
| 434 | return true;  | 
            ||
| 435 | }  | 
            ||
| 436 | |||
| 437 | /**  | 
            ||
| 438 |      * @return array{'minx'?:float|null, 'miny'?:float|null, 'maxx'?:float|null, 'maxy'?:float|null} | 
            ||
| 439 | */  | 
            ||
| 440 | public function getBBox(): array  | 
            ||
| 441 |     { | 
            ||
| 442 | return $this->exteriorRing()->getBBox();  | 
            ||
| 443 | }  | 
            ||
| 444 | |||
| 445 | /**  | 
            ||
| 446 | * @return LineString|MultiLineString  | 
            ||
| 447 | */  | 
            ||
| 448 | public function boundary(): Geometry  | 
            ||
| 452 | }  | 
            ||
| 453 | }  | 
            ||
| 454 |