diff --git a/composer.json b/composer.json index 00f8e8f..48d8130 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,13 @@ "swf\\": "deps/swf-4real/src/" } }, + "repositories": [ + { + "type": "path", + "url": "deps/martinez-rueda-php" + } + ], + "minimum-stability": "dev", "require": { "ext-mbstring": "*", "ext-dom": "*", @@ -15,7 +22,7 @@ "ext-zlib": "*", "ext-gmp": "*", "markrogoyski/math-php": "2.*", - "kudm761/martinez-rueda-php": "^0.1.2", + "kudm761/martinez-rueda-php": "*", "ext-json": "*", "ext-decimal": "*" } diff --git a/composer.lock b/composer.lock index 0c42868..4a63c57 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,15 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f628e429199d537ddecb7543bc12ad6e", + "content-hash": "3c542c4bd71882bf97e7a7ed73957a07", "packages": [ { "name": "kudm761/martinez-rueda-php", - "version": "0.1.2", - "source": { - "type": "git", - "url": "https://github.com/BardoQi/polygon_utils.git", - "reference": "b55ba0520eedf9dda48e2874539df86b4abc94e4" - }, + "version": "dev-master", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/BardoQi/polygon_utils/zipball/b55ba0520eedf9dda48e2874539df86b4abc94e4", - "reference": "b55ba0520eedf9dda48e2874539df86b4abc94e4", - "shasum": "" + "type": "path", + "url": "deps/martinez-rueda-php", + "reference": "e295acd4ae5ef2dc894e66b80830e0c2b9a3fb1a" }, "require": { "php": ">=7.0" @@ -32,7 +26,6 @@ "MartinezRueda\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", "authors": [ { "name": "Dmitry Kubitsky", @@ -52,10 +45,9 @@ "polygon union", "polygon xor" ], - "support": { - "source": "https://github.com/BardoQi/polygon_utils/tree/0.1.2Release" - }, - "time": "2020-06-11T07:53:13+00:00" + "transport-options": { + "relative": true + } }, { "name": "markrogoyski/math-php", @@ -135,7 +127,7 @@ ], "packages-dev": [], "aliases": [], - "minimum-stability": "stable", + "minimum-stability": "dev", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, @@ -144,7 +136,9 @@ "ext-dom": "*", "ext-imagick": "*", "ext-zlib": "*", - "ext-gmp": "*" + "ext-gmp": "*", + "ext-json": "*", + "ext-decimal": "*" }, "platform-dev": [], "plugin-api-version": "2.0.0" diff --git a/deps/martinez-rueda-php/.gitignore b/deps/martinez-rueda-php/.gitignore new file mode 100644 index 0000000..e0caea8 --- /dev/null +++ b/deps/martinez-rueda-php/.gitignore @@ -0,0 +1,2 @@ +.idea/ +vendor/ \ No newline at end of file diff --git a/deps/martinez-rueda-php/README.md b/deps/martinez-rueda-php/README.md new file mode 100644 index 0000000..eefaa4a --- /dev/null +++ b/deps/martinez-rueda-php/README.md @@ -0,0 +1,42 @@ +## Martinez-Rueda polygon boolean operations algorithm +PHP implementation of original algorithm + +Algorithm is used for computing Boolean operations on polygons: +- union +- difference +- intersection +- xor +## Usage +Input parameter is a multipolygon - an array of polygons. And each polygon is an array of points x,y. +``` + $data = [[[-1, 4], [-3, 4], [-3, 0], [-3, -1], [-1, -1], [-1, -2], [2, -2], [2, 1], [-1, 1], [-1, 4]]]; + $subject = new \MartinezRueda\Polygon($data); + + $data = [[[-2, 5], [-2, 0], [3, 0], [3, 3], [2, 3], [2, 2], [0, 2], [0, 5], [-2, 5]]]; + $clipping = new \MartinezRueda\Polygon($data); + + $result = (new \MartinezRueda\Algorithm())->getUnion($subject, $clipping); + + echo json_encode($result->toArray()), PHP_EOL; + + // Result is: + // [[[2,3],[2,2],[0,2],[0,5],[-2,5],[-2,4],[-3,4],[-3,0],[-3,-1],[-1,-1],[-1,-2],[2,-2],[2,0],[3,0],[3,3],[2,3]]] +``` +## Some visual examples +Let's consider two polygons: green multipolygon of two polygons and yellow polygon. + +Snow-white polygon is result of Boolean operation on two polygons. + + + +###### Union + + +###### Difference (green NOT yellow) + + +###### Intersection + + +###### Xor + diff --git a/deps/martinez-rueda-php/composer.json b/deps/martinez-rueda-php/composer.json new file mode 100644 index 0000000..d3c39bc --- /dev/null +++ b/deps/martinez-rueda-php/composer.json @@ -0,0 +1,25 @@ +{ + "name": "kudm761/martinez-rueda-php", + "description": "Martinez-Rueda algorithm for polygon boolean operations", + "keywords": ["polygon clipping", "polygon boolean operations", "polygon union", "polygon intersection", + "polygon difference", "polygon xor", "geography", "martinez polygon algorithm", "martinez php"], + "type": "library", + "authors": [ + { + "name": "Dmitry Kubitsky", + "email": "kudm761@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "5.5.*" + }, + "autoload": { + "psr-4": { + "MartinezRueda\\": "src/" + } + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/phpunit.xml b/deps/martinez-rueda-php/phpunit.xml new file mode 100644 index 0000000..faf0fca --- /dev/null +++ b/deps/martinez-rueda-php/phpunit.xml @@ -0,0 +1,28 @@ + + + + + ./tests/ + + + + + src + + vendor + + + + \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Algorithm.php b/deps/martinez-rueda-php/src/Algorithm.php new file mode 100644 index 0000000..4e66515 --- /dev/null +++ b/deps/martinez-rueda-php/src/Algorithm.php @@ -0,0 +1,649 @@ +eq = new PriorityQueue(); + } + + /** + * @param Polygon $subject + * @param Polygon $clipping + * @return Polygon + */ + public function getDifference(Polygon $subject, Polygon $clipping) : Polygon + { + return $this->compute($subject, $clipping, self::OPERATION_DIFFERENCE); + } + + /** + * @param Polygon $subject + * @param Polygon $clipping + * @return Polygon + */ + public function getUnion(Polygon $subject, Polygon $clipping) : Polygon + { + return $this->compute($subject, $clipping, self::OPERATION_UNION); + } + + /** + * @param Polygon $subject + * @param Polygon $clipping + * @return Polygon + */ + public function getIntersection(Polygon $subject, Polygon $clipping) : Polygon + { + return $this->compute($subject, $clipping, self::OPERATION_INTERSECTION); + } + + /** + * @param Polygon $subject + * @param Polygon $clipping + * @return Polygon + */ + public function getXor(Polygon $subject, Polygon $clipping) : Polygon + { + return $this->compute($subject, $clipping, self::OPERATION_XOR); + } + + /** + * @param Polygon $subject + * @param Polygon $clipping + * @param string $operation + * @return Polygon + */ + protected function compute(Polygon $subject, Polygon $clipping, string $operation) : Polygon + { + // Test for 1 trivial result case + if ($subject->ncontours() * $clipping->ncontours() == 0) { + if ($operation == self::OPERATION_DIFFERENCE) { + $result = $subject; + } + + if ($operation == self::OPERATION_UNION || $operation == self::OPERATION_XOR) { + $result = ($subject->ncontours() == 0) ? $clipping : $subject; + } + + return $result; + } + + // Test 2 for trivial result case + $box = $subject->getBoundingBox(); + $minsubj = $box['min']; + $maxsubj = $box['max']; + + $box = $clipping->getBoundingBox(); + $minclip = $box['min']; + $maxclip = $box['max']; + + if ($minsubj->x > $maxclip->x || $minclip->x > $maxsubj->x + || $minsubj->y > $maxclip->y || $minclip->y > $maxsubj->y) { + // the bounding boxes do not overlap + if ($operation == self::OPERATION_DIFFERENCE) { + $result = $subject; + } + + if ($operation == self::OPERATION_UNION || $operation == self::OPERATION_XOR) { + $result = $subject; + + for ($i = 0; $i < $clipping->ncontours(); $i++) { + $result[] = $clipping->contour($i); + } + } + + return $result; + } + + // Boolean operation is not trivial + + // Insert all the endpoints associated to the line segments into the event queue + for ($i = 0; $i < $subject->ncontours(); $i++) { + for ($j = 0; $j < $subject->contour($i)->nvertices(); $j++) { + $this->processSegment($subject->contour($i)->segment($j), self::POLYGON_TYPE_SUBJECT); + } + } + + for ($i = 0; $i < $clipping->ncontours(); $i++) { + for ($j = 0; $j < $clipping->contour($i)->nvertices(); $j++) { + $this->processSegment($clipping->contour($i)->segment($j), self::POLYGON_TYPE_CLIPPING); + } + } + + $connector = new Connector(); + $sweepline = new SweepLine(); + + $min_max_x = min($maxsubj->x, $maxclip->x); + + Debug::debug( + function () { + echo 'Initial queue:', PHP_EOL; + + $i = 0; + + foreach ($this->eq->events as $sweep_event) { + echo "\t", ++$i, ' - ', Debug::gatherSweepEventData($sweep_event), PHP_EOL; + } + } + ); + + while (!$this->eq->isEmpty()) { + $e = $this->eq->dequeue(); + + Debug::debug( + function () use ($e) { + echo 'Process event:', PHP_EOL; + echo "\t", Debug::gatherSweepEventData($e), PHP_EOL; + } + ); + + if (($operation == self::OPERATION_INTERSECTION && ($e->p->x > $min_max_x)) + || ($operation == self::OPERATION_DIFFERENCE && ($e->p->x > $maxsubj->x))) { + $result = $connector->toPolygon(); + return $result; + } + + if ($e->is_left) { + $position = $sweepline->insert($e); + $prev = null; + + if ($position > 0) { + $prev = $sweepline->get($position - 1); + } + + $next = null; + + if ($position < $sweepline->size() - 1) { + $next = $sweepline->get($position + 1); + } + + if (is_null($prev)) { + $e->inside = false; + $e->in_out = false; + } elseif ($prev->edge_type != self::EDGE_TYPE_NORMAL) { + if ($position - 2 < 0) { + $e->inside = false; + $e->in_out = false; + + if ($prev->polygon_type != $e->polygon_type) { + $e->inside = true; + } else { + $e->in_out = true; + } + } else { + $prev2 = $sweepline->get($position - 2); + + if ($prev->polygon_type == $e->polygon_type) { + $e->in_out = !$prev->in_out; + $e->inside = !$prev2->in_out; + } else { + $e->in_out = !$prev2->in_out; + $e->inside = !$prev->in_out; + } + } + } elseif ($e->polygon_type == $prev->polygon_type) { + $e->inside = $prev->inside; + $e->in_out = !$prev->in_out; + } else { + $e->inside = !$prev->in_out; + $e->in_out = $prev->inside; + } + + Debug::debug( + function () use ($sweepline) { + echo 'Status line after insertion: ', PHP_EOL; + + $i = 0; + + foreach ($sweepline->events as $sweep_event) { + echo "\t", ++$i, ' - ', Debug::gatherSweepEventData($sweep_event), PHP_EOL; + } + } + ); + + if (!is_null($next)) { + $this->possibleIntersection($e, $next); + } + + if (!is_null($prev)) { + $this->possibleIntersection($prev, $e); + } + } else { // not left, the line segment must be removed from S + $other_pos = -1; + + foreach ($sweepline->events as $index => $item) { + if ($item->equalsTo($e->other)) { + $other_pos = $index; + break; + } + } + + if ($other_pos != -1) { + $prev = null; + + if ($other_pos > 0) { + $prev = $sweepline->get($other_pos - 1); + } + + $next = null; + + if ($other_pos < sizeof($sweepline->events) - 1) { + $next = $sweepline->get($other_pos + 1); + } + } + + // Check if the line segment belongs to the Boolean operation + switch ($e->edge_type) { + case self::EDGE_TYPE_NORMAL: + switch ($operation) { + case self::OPERATION_INTERSECTION: + if ($e->other->inside) { + $connector->add($e->segment()); + } + + break; + + case self::OPERATION_UNION: + if (!$e->other->inside) { + $connector->add($e->segment()); + } + + break; + + case self::OPERATION_DIFFERENCE: + if ($e->polygon_type == self::POLYGON_TYPE_SUBJECT && !$e->other->inside + || $e->polygon_type == self::POLYGON_TYPE_CLIPPING && $e->other->inside) { + $connector->add($e->segment()); + } + + break; + + case self::OPERATION_XOR: + $connector->add($e->segment()); + break; + } + + break; // end of EDGE_TYPE_NORMAL + + case self::EDGE_TYPE_SAME_TRANSITION: + if ($operation == self::OPERATION_INTERSECTION || $operation == self::OPERATION_UNION) { + $connector->add($e->segment()); + } + + break; + + case self::EDGE_TYPE_DIFFERENT_TRANSITION: + if ($operation == self::OPERATION_DIFFERENCE) { + $connector->add($e->segment()); + } + + break; + } // end switch ($e->edge_type) + + if ($other_pos != -1) { + $sweepline->remove($sweepline->get($other_pos)); + } + + if (!is_null($next) && !is_null($prev)) { + $this->possibleIntersection($next, $prev); + } + + Debug::debug( + function () use ($connector) { + echo 'Connector:', PHP_EOL; + echo Debug::gatherConnectorData($connector), PHP_EOL; + } + ); + } + } + + return $connector->toPolygon(); + } + + /** + * @param Segment $segment0 + * @param Segment $segment1 + * @param Point $pi0 + * @param Point $pi1 + * @return int + */ + protected function findIntersection(Segment $segment0, Segment $segment1, Point &$pi0, Point &$pi1) : int + { + $p0 = $segment0->begin(); + $d0 = new Point($segment0->end()->x - $p0->x, $segment0->end()->y - $p0->y); + + $p1 = $segment1->begin(); + $d1 = new Point($segment1->end()->x - $p1->x, $segment1->end()->y - $p1->y); + + $sqr_epsilon = 1e-7; // it was 1e-3 before + $E = new Point($p1->x - $p0->x, $p1->y - $p0->y); + $kross = $d0->x * $d1->y - $d0->y * $d1->x; + $sqr_kross = $kross * $kross; + $sqr_len0 = $d0->x * $d0->x + $d0->y * $d0->y; + $sqr_len1 = $d1->x * $d1->x + $d1->y * $d1->y; + + if ($sqr_kross > $sqr_epsilon * $sqr_len0 * $sqr_len1) { + $s = ($E->x * $d1->y - $E->y * $d1->x) / $kross; + + if ($s < 0 || $s > 1) { + return 0; + } + + $t = ($E->x * $d0->y - $E->y * $d0->x) / $kross; + + if ($t < 0 || $t > 1) { + return 0; + } + + // intersection of lines is a point an each segment + $pi0 = new Point($p0->x + $s * $d0->x, $p0->y + $s * $d0->y); + + if ($pi0->distanceTo($segment0->begin()) < 1e-8) { + $pi0 = $segment0->begin(); + } + + if ($pi0->distanceTo($segment0->end()) < 1e-8) { + $pi0 = $segment0->end(); + } + + if ($pi0->distanceTo($segment1->begin()) < 1e-8) { + $pi0 = $segment1->begin(); + } + + if ($pi0->distanceTo($segment1->end()) < 1e-8) { + $pi0 = $segment1->end(); + } + + return 1; + } + + $sqr_len_e = $E->x * $E->x + $E->y * $E->y; + $kross = $E->x * $d0->y - $E->y * $d0->x; + $sqr_kross = $kross * $kross; + + if ($sqr_kross > $sqr_epsilon * $sqr_len0 * $sqr_len_e) { + return 0; + } + + $s0 = ($d0->x * $E->x + $d0->y * $E->y) / $sqr_len0; + $s1 = $s0 + ($d0->x * $d1->x + $d0->y * $d1->y) / $sqr_len0; + + $smin = min($s0, $s1); + $smax = max($s0, $s1); + + $w = []; + $imax = $this->findIntersection2(0.0, 1.0, $smin, $smax, $w); + + if ($imax > 0) { + $pi0 = new Point($p0->x + $w[0] * $d0->x, $p0->y + $w[0] * $d0->y); + + if ($pi0->distanceTo($segment0->begin()) < 1e-8) { + $pi0 = $segment0->begin(); + } + + if ($pi0->distanceTo($segment0->end()) < 1e-8) { + $pi0 = $segment0->end(); + } + + if ($pi0->distanceTo($segment1->begin()) < 1e-8) { + $pi0 = $segment1->begin(); + } + + if ($pi0->distanceTo($segment1->end()) < 1e-8) { + $pi0 = $segment1->end(); + } + + if ($imax > 1) { + $pi1 = new Point($p0->x + $w[1] * $d0->x, $p0->y + $w[1] * $d0->y); + } + } + + return $imax; + } + + /** + * @param float $u0 + * @param float $u1 + * @param float $v0 + * @param float $v1 + * @param array $w + * @return int + */ + protected function findIntersection2(float $u0, float $u1, float $v0, float $v1, array &$w) : int + { + if ($u1 < $v0 || $u0 > $v1) { + return 0; + } + + if ($u1 > $v0) { + if ($u0 < $v1) { + $w[0] = $u0 < $v0 ? $v0 : $u0; + $w[1] = $u1 > $v1 ? $v1 : $u1; + + return 2; + } else { + $w[0] = $u0; + return 1; + } + } else { + $w[0] = $u1; + return 1; + } + } + + /** + * @param SweepEvent $event1 + * @param SweepEvent $event2 + * @throws \Exception + */ + protected function possibleIntersection(SweepEvent $event1, SweepEvent $event2) + { + // uncomment these two lines if self-intersecting polygons are not allowed + // if ($event1->polygon_type == $event2->polygon_type) { + // return false; + // } + + $ip1 = new Point(); + $ip2 = new Point(); + + $intersections = $this->findIntersection($event1->segment(), $event2->segment(), $ip1, $ip2); + + if (empty($intersections)) { + return; + } + + if ($intersections == 1 && ($event1->p->equalsTo($event2->p) || $event1->other->p->equalsTo($event2->other->p))) { + return; + } + + // the line segments overlap, but they belong to the same polygon + // the program does not work with this kind of polygon + if ($intersections == 2 && $event1->polygon_type == $event2->polygon_type) { + throw new \Exception('Polygon has overlapping edges.'); + } + + if ($intersections == 1) { + if (!$event1->p->equalsTo($ip1) && !$event1->other->p->equalsTo($ip1)) { + $this->divideSegment($event1, $ip1); + } + + if (!$event2->p->equalsTo($ip1) && !$event2->other->p->equalsTo($ip1)) { + $this->divideSegment($event2, $ip1); + } + + return; + } + + // The line segments overlap + $sorted_events = []; + + if ($event1->p->equalsTo($event2->p)) { + $sorted_events[] = 0; + } elseif (Helper::compareSweepEvents($event1, $event2)) { + $sorted_events[] = $event2; + $sorted_events[] = $event1; + } else { + $sorted_events[] = $event1; + $sorted_events[] = $event2; + } + + if ($event1->other->p->equalsTo($event2->other->p)) { + $sorted_events[] = 0; + } elseif (Helper::compareSweepEvents($event1->other, $event2->other)) { + $sorted_events[] = $event2->other; + $sorted_events[] = $event1->other; + } else { + $sorted_events[] = $event1->other; + $sorted_events[] = $event2->other; + } + + if (sizeof($sorted_events) == 2) { + $event1->edge_type = $event1->other->edge_type = self::EDGE_TYPE_NON_CONTRIBUTING; + $event2->edge_type = $event2->other->edge_type = ($event1->in_out == $event2->in_out) + ? self::EDGE_TYPE_SAME_TRANSITION + : self::EDGE_TYPE_DIFFERENT_TRANSITION; + + return; + } + + if (sizeof($sorted_events) == 3) { + $sorted_events[1]->edge_type = $sorted_events[1]->other->edge_type = self::EDGE_TYPE_NON_CONTRIBUTING; + + if ($sorted_events[0]) { + $sorted_events[0]->other->edge_type = ($event1->in_out == $event2->in_out) + ? self::EDGE_TYPE_SAME_TRANSITION + : self::EDGE_TYPE_DIFFERENT_TRANSITION; + } else { + $sorted_events[2]->other->edge_type = ($event1->in_out == $event2->in_out) + ? self::EDGE_TYPE_SAME_TRANSITION + : self::EDGE_TYPE_DIFFERENT_TRANSITION; + } + + $this->divideSegment($sorted_events[0] ? $sorted_events[0] : $sorted_events[2]->other, $sorted_events[1]->p); + + return; + } + + if (!$sorted_events[0]->equalsTo($sorted_events[3]->other)) { + $sorted_events[1]->type = self::EDGE_TYPE_NON_CONTRIBUTING; + $sorted_events[2]->type = ($event1->in_out == $event2->in_out) + ? self::EDGE_TYPE_SAME_TRANSITION + : self::EDGE_TYPE_DIFFERENT_TRANSITION; + + $this->divideSegment($sorted_events[0], $sorted_events[1]->p); + $this->divideSegment($sorted_events[1], $sorted_events[2]->p); + + return; + } + + $sorted_events[1]->type = $sorted_events[1]->other->type = self::EDGE_TYPE_NON_CONTRIBUTING; + $this->divideSegment($sorted_events[0], $sorted_events[1]->p); + + $sorted_events[3]->other->type = ($event1->in_out == $event2->in_out) + ? self::EDGE_TYPE_SAME_TRANSITION + : self::EDGE_TYPE_DIFFERENT_TRANSITION; + $this->divideSegment($sorted_events[3]->other, $sorted_events[2]->p); + } + + /** + * Add element to the end of dequeue + * + * @param SweepEvent $event + * @return SweepEvent + */ + protected function storeSweepEvent(SweepEvent $event) : SweepEvent + { + $this->event_holder[] = $event; + + return $this->event_holder[sizeof($this->event_holder) - 1]; + } + + protected function divideSegment(SweepEvent $event, Point $point) + { + $right = new SweepEvent($point, false, $event->polygon_type, $event, $event->edge_type); + $left = new SweepEvent($point, true, $event->polygon_type, $event->other, $event->other->edge_type); + + if (Helper::compareSweepEvents($left, $event->other)) { + $event->other->is_left = true; + $left->is_left = false; + } + + //if (Helper::compareSweepEvents($event, $right)) { + // nothing + //} + + $event->other->other = $left; + $event->other = $right; + + $this->eq->enqueue($left); + $this->eq->enqueue($right); + } + + /** + * @param Segment $segment + * @param int $polygon_type + * @return void + */ + protected function processSegment(Segment $segment, int $polygon_type) + { + // if the two edge endpoints are equal the segment is discarded + if ($segment->begin()->equalsTo($segment->end())) { + return; + } + + $e1 = new SweepEvent($segment->begin(), true, $polygon_type); + $e2 = new SweepEvent($segment->end(), true, $polygon_type, $e1); + $e1->other = $e2; + + if ($e1->p->x < $e2->p->x) { + $e2->is_left = false; + } elseif ($e1->p->x > $e2->p->x) { + $e1->is_left = false; + } elseif ($e1->p->y < $e2->p->y) { + $e2->is_left = false; + } else { + $e1->is_left = false; + } + + $this->eq->enqueue($e1); + $this->eq->enqueue($e2); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Connector.php b/deps/martinez-rueda-php/src/Connector.php new file mode 100644 index 0000000..9f63d83 --- /dev/null +++ b/deps/martinez-rueda-php/src/Connector.php @@ -0,0 +1,101 @@ +closed; + } + + /** + * @param Segment $segment + * @return void + */ + public function add(Segment $segment) + { + $size = sizeof($this->open_polygons); + + for ($j = 0; $j < $size; $j++) { + $chain = $this->open_polygons[$j]; + + if (!$chain->linkSegment($segment)) { + continue; + } + + if ($chain->closed) { + if (sizeof($chain->segments) == 2) { + $chain->closed = false; + + return; + } + + $this->closed_polygons[] = $this->open_polygons[$j]; + + Helper::removeElementWithShift($this->open_polygons, $j); + + return; + } + + // if chain not closed + $k = sizeof($this->open_polygons); + + for ($i = $j + 1; $i < $k; $i++) { + $v = $this->open_polygons[$i]; + + if ($chain->linkChain($v)) { + Helper::removeElementWithShift($this->open_polygons, $i); + + return; + } + } + + return; + } + + $new_chain = new PointChain(); + $new_chain->init($segment); + + $this->open_polygons[] = $new_chain; + } + + /** + * @return Polygon + */ + public function toPolygon() : Polygon + { + $contours = []; + + foreach ($this->closed_polygons as $closed_polygon) { + $contour_points = []; + + foreach ($closed_polygon->segments as $point) { + $contour_points[] = [$point->x, $point->y]; + } + + // close contour + $first = reset($contour_points); + $last = end($contour_points); + + if ($last != $first) { + $contour_points[] = $first; + } + + $contours[] = $contour_points; + } + + return new Polygon($contours); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Contour.php b/deps/martinez-rueda-php/src/Contour.php new file mode 100644 index 0000000..e9a344d --- /dev/null +++ b/deps/martinez-rueda-php/src/Contour.php @@ -0,0 +1,224 @@ +add($point); + } + } + + public function add(Point $p) + { + $this->points[] = $p; + } + + public function erase(int $index) + { + if (!isset($this->points[$index])) { + throw new \InvalidArgumentException(sprintf('Undefined points offset `%s`', $index)); + } + + unset($this->points[$index]); + } + + public function clear() + { + $this->points = []; + $this->holes = []; + } + + public function addHole(int $index) + { + $this->holes[] = $index; + } + + /** + * Get the p-th vertex of the external contour + * + * @param int $p + * @return Point + */ + public function vertex(int $p) : Point + { + if (!isset($this->points[$p])) { + throw new \InvalidArgumentException('Undefined index offset.'); + } + + return $this->points[$p]; + } + + /** + * @param int $p + * @return Segment + */ + public function segment(int $p) : Segment + { + if ($p == $this->nvertices() - 1) { + // last, first + return new Segment($this->points[sizeof($this->points) - 1], $this->points[0]); + } + + return new Segment($this->points[$p], $this->points[$p + 1]); + } + + /** + * @return int + */ + public function nvertices() : int + { + return sizeof($this->points); + } + + /** + * @return int + */ + public function nedges() : int + { + return sizeof($this->points); + } + + /** + * @return int + */ + public function nholes() : int + { + return sizeof($this->holes); + } + + /** + * @param int $index + * @return mixed + */ + public function hole(int $index) + { + if (!isset($this->holes[$index])) { + throw new \InvalidArgumentException(sprintf('Undefined holes offset `%s`', $index)); + } + + return $this->holes[$index]; + } + + /** + * Get minimum bounding rectangle + * + * @return array ['min' => Point, 'max' => Point] + */ + public function getBoundingBox() : array + { + $min_x = PHP_INT_MAX; + $min_y = PHP_INT_MAX; + + $max_x = PHP_INT_MIN; + $max_y = PHP_INT_MIN; + + foreach ($this->points as $k => $point) { + if ($point->x < $min_x) { + $min_x = $point->x; + } + + if ($point->x > $max_x) { + $max_x = $point->x; + } + + if ($point->y < $min_y) { + $min_y = $point->y; + } + + if ($point->y > $max_y) { + $max_y = $point->y; + } + } + + return [ + 'min' => new Point($min_x, $min_y), + 'max' => new Point($max_x, $max_y) + ]; + } + + public function counterClockwise() : bool + { + if (!is_null($this->precomputed_cc)) { + return $this->precomputed_cc; + } + + $this->precomputed_cc = true; + + $area = 0.0; + + for ($c = 0; $c < $this->nvertices() - 1; $c++) { + $area = $area + $this->vertex($c)->x * $this->vertex($c + 1)->y + - $this->vertex($c + 1)->x * $this->vertex($c)->y; + } + + $area = $area + $this->vertex($this->nvertices() - 1)->x * $this->vertex(0)->y + - $this->vertex(0)->x * $this->vertex($this->nvertices() - 1)->y; + + $this->cc = $area >= 0.0; + + return $this->cc; + } + + public function clockwise() : bool + { + return !$this->counterClockwise(); + } + + public function changeOrientation() + { + $this->points = array_reverse($this->points); + $this->cc = !$this->cc; + } + + public function setClockwise() + { + if ($this->counterClockwise()) { + $this->changeOrientation(); + } + } + + public function setCounterClockwise() + { + if ($this->clockwise()) { + $this->changeOrientation(); + } + } + + public function external() : bool + { + return $this->is_external; + } + + public function setExternal(bool $flag) + { + $this->is_external = $flag; + } + + /** + * @param float $x + * @param float $y + */ + public function move(float $x, float $y) + { + for ($i = 0; $i < $this->nvertices(); $i++) { + $this->points[$i]->x += $x; + $this->points[$i]->y += $y; + } + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Debug.php b/deps/martinez-rueda-php/src/Debug.php new file mode 100644 index 0000000..5f600fe --- /dev/null +++ b/deps/martinez-rueda-php/src/Debug.php @@ -0,0 +1,83 @@ + $event->id, + 'is_left' => $event->is_left ? 1 : 0, + 'x' => $event->p->x, + 'y' => $event->p->y, + 'other' => ['x' => $event->other->p->x, 'y' => $event->other->p->y] + ]; + + return json_encode($data); + } + + /** + * @param Connector $connector + * @return string + */ + public static function gatherConnectorData(Connector $connector) : string + { + $open_polygons = []; + $closed_polygons = []; + + foreach ($connector->open_polygons as $chain) { + $open_polygons[] = self::gatherPointChainData($chain); + } + + foreach ($connector->closed_polygons as $chain) { + $closed_polygons[] = self::gatherPointChainData($chain); + } + + $data = [ + 'closed' => $connector->isClosed() ? 1 : 0, + 'open_polygons' => $open_polygons, + 'closed_polygons' => $closed_polygons + ]; + + return json_encode($data); + } + + /** + * @param PointChain $chain + * @return array + */ + protected function gatherPointChainData(PointChain $chain) : array + { + $points = []; + + if (!empty($chain->segments)) { + foreach ($chain->segments as $point) { + $points[] = ['x' => $point->x, 'y' => $point->y]; + } + } + + $data = [ + 'closed' => $chain->closed ? 1 : 0, + 'elements' => $points + ]; + + return $data; + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Helper.php b/deps/martinez-rueda-php/src/Helper.php new file mode 100644 index 0000000..a131f42 --- /dev/null +++ b/deps/martinez-rueda-php/src/Helper.php @@ -0,0 +1,188 @@ +x - $p2->x) * ($p1->y - $p2->y) - ($p1->x - $p2->x) * ($p0->y - $p2->y); + } + + /** + * Used for sorting SweepEvents in PriorityQueue + * If same x coordinate - from bottom to top. + * If two endpoints share the same point - rights are before lefts. + * If two left endpoints share the same point then they must be processed + * in the ascending order of their associated edges in SweepLine + * + * + * @param SweepEvent $event1 + * @param SweepEvent $event2 + * @return bool + */ + public static function compareSweepEvents(SweepEvent $event1, SweepEvent $event2) : bool + { + // x is not the same + if ($event1->p->x > $event2->p->x) { + return true; + } + + // x is not the same too + if ($event2->p->x > $event1->p->x) { + return false; + } + + // x is the same, but y is not + // the event with lower y-coordinate is processed first + if (!$event1->p->equalsTo($event2->p)) { + return $event1->p->y > $event2->p->y; + } + + // x and y are the same, but one is a left endpoint and the other a right endpoint + // the right endpoint is processed first + if ($event1->is_left != $event2->is_left) { + return $event1->is_left; + } + + // x and y are the same and both points are left or right + return $event1->above($event2->other->p); + } + + /** + * @param SweepEvent $event1 + * @param SweepEvent $event2 + * @return bool + */ + public static function compareSegments(SweepEvent $event1, SweepEvent $event2) : bool + { + if ($event1->equalsTo($event2)) { + return false; + } + + if (self::signedArea($event1->p, $event1->other->p, $event2->p) != 0 + || self::signedArea($event1->p, $event1->other->p, $event2->other->p) != 0) { + if ($event1->p->equalsTo($event2->p)) { + return $event1->below($event2->other->p); + } + + if (self::compareSweepEvents($event1, $event2)) { + return $event2->above($event1->p); + //return $event1->below($event2->p); + } + + return $event1->below($event2->p); + //return $event2->above($event1->p); + } + + if ($event1->p->equalsTo($event2->p)) { + //return $event1->lessThan($event2); + return false; + } + + return self::compareSweepEvents($event1, $event2); + } + + /** + * Remove $index element and maintain indexing. + * + * @param array $array + * @param int $index + * @return void + */ + public static function removeElementWithShift(array &$array, int $index) + { + if (!isset($array[$index])) { + $message = sprintf('Undefined index offset: `%s` in array %s.', $index, print_r($array, true)); + throw new \InvalidArgumentException($message); + } + + unset($array[$index]); + $array = array_values($array); + + return; + } + + /** + * @param array $expected_multipolygon + * @param array $tested_multipolygon + * @return int + */ + public static function compareMultiPolygons(array $expected_multipolygon, array $tested_multipolygon) : array + { + if (sizeof($expected_multipolygon) != sizeof($tested_multipolygon)) { + return ['success' => false, 'reason' => 'different count of polygons']; + } + + if (sizeof($expected_multipolygon) == 0 && sizeof($tested_multipolygon) == 0) { + return ['success' => true, 'reason' => '']; + } + + for ($i = 0; $i < sizeof($expected_multipolygon); $i++) { + $expected_polygon = $expected_multipolygon[$i]; + + if (!isset($tested_multipolygon[$i])) { + return [ + 'success' => false, + 'reason' => sprintf('Tested multipolygon has not polygon with index: `%s`, check indexation', $i), + ]; + } + + $tested_polygon = $tested_multipolygon[$i]; + + // walk through the points + for ($j = 0, $size = sizeof($expected_polygon); $j < $size; $j++) { + if (!isset($tested_polygon[$j])) { + return [ + 'success' => false, + 'reason' => sprintf( + 'Tested polygon with index: `%d` has not point with index: `%d`, check indexation', + $i, + $j + ), + ]; + } + + $expected_point = $expected_polygon[$j]; + $tested_point = $tested_polygon[$j]; + + if (bccomp($expected_point[0], $tested_point[0], 6) !== 0) { + return [ + 'success' => false, + 'reason' => sprintf( + 'X coordinates of points are not equal: expected `%f` but `%s` given at index %d', + $expected_point[0], + $tested_point[0], + $j + ) + ]; + } + + if (bccomp($expected_point[1], $tested_point[1], 6) !== 0) { + return [ + 'success' => false, + 'reason' => sprintf( + 'Y coordinates of points are not equal: expected `%f` but `%s` given at index %d', + $expected_point[1], + $tested_point[1], + $j + ) + ]; + } + } + } + + return ['success' => true, 'reason' => '']; + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Point.php b/deps/martinez-rueda-php/src/Point.php new file mode 100644 index 0000000..379eb8f --- /dev/null +++ b/deps/martinez-rueda-php/src/Point.php @@ -0,0 +1,45 @@ +x = $x; + $this->y = $y; + } + + /** + * @param Point $p + * @return bool + */ + public function equalsTo(Point $p) : bool + { + return ($this->x === $p->x && $this->y === $p->y); + } + + /** + * @param Point $p + * @return float + */ + public function distanceTo(Point $p) : float + { + $dx = $this->x - $p->x; + $dy = $this->y - $p->y; + + return sqrt($dx * $dx + $dy * $dy); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/PointChain.php b/deps/martinez-rueda-php/src/PointChain.php new file mode 100644 index 0000000..51a1c0b --- /dev/null +++ b/deps/martinez-rueda-php/src/PointChain.php @@ -0,0 +1,160 @@ +segments = $segments; + } + + /** + * @param Segment $segment + */ + public function init(Segment $segment) + { + $this->segments[] = $segment->begin(); + $this->segments[] = $segment->end(); + } + + /** + * @return mixed + */ + public function begin() + { + return $this->segments[0]; + } + + /** + * @return mixed + */ + public function end() + { + return $this->segments[$this->size() - 1]; + } + + /** + * @return int + */ + public function size() : int + { + return sizeof($this->segments); + } + + /** + * @param Segment $segment + * @return bool + */ + public function linkSegment(Segment $segment) + { + $front = $this->begin(); + $back = $this->end(); + + if ($segment->begin()->equalsTo($front)) { + if ($segment->end()->equalsTo($back)) { + $this->closed = true; + } else { + array_unshift($this->segments, $segment->end()); + } + + return true; + } + + if ($segment->end()->equalsTo($back)) { + if ($segment->begin()->equalsTo($front)) { + $this->closed = true; + } else { + $this->segments[] = $segment->begin(); + } + + return true; + } + + if ($segment->end()->equalsTo($front)) { + if ($segment->begin()->equalsTo($back)) { + $this->closed = true; + } else { + array_unshift($this->segments, $segment->begin()); + } + + return true; + } + + if ($segment->begin()->equalsTo($back)) { + if ($segment->end()->equalsTo($front)) { + $this->closed = true; + } else { + $this->segments[] = $segment->end(); + } + + return true; + } + + return false; + } + + /** + * @param PointChain $other + * @return bool + */ + public function linkChain(PointChain $other) + { + $front = $this->begin(); + $back = $this->end(); + + $other_front = $other->begin(); + $other_back = $other->end(); + + if ($other_front->equalsTo($back)) { + array_shift($other->segments); + + // insert at the end of $this->segments + $this->segments = array_merge($this->segments, $other->segments); + + return true; + } + + if ($other_back->equalsTo($front)) { + array_shift($this->segments); + + // insert at the beginning of $this->segments + $this->segments = array_merge($other->segments, $this->segments); + + return true; + } + + if ($other_front->equalsTo($front)) { + array_shift($this->segments); + + $other->segments = array_reverse($other->segments); + // insert reversed at the beginning of $this->segments + $this->segments = array_merge($other->segments, $this->segments); + + return true; + } + + if ($other_back->equalsTo($back)) { + array_pop($this->segments); + + $other->segments = array_reverse($other->segments); + + // insert reversed at the end of $this->segments + $this->segments = array_merge($this->segments, $other->segments); + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Polygon.php b/deps/martinez-rueda-php/src/Polygon.php new file mode 100644 index 0000000..4846452 --- /dev/null +++ b/deps/martinez-rueda-php/src/Polygon.php @@ -0,0 +1,171 @@ +push_back(new Contour($contour_points)); + } + } + + /** + * @param int $index + * @return Contour + */ + public function contour(int $index) : Contour + { + return $this->contours[$index]; + } + + /** + * @return int + */ + public function ncontours() : int + { + return sizeof($this->contours); + } + + /** + * @return int + */ + public function nvertices() : int + { + $nv = 0; + + for ($i = 0; $i < $this->ncontours(); $i++) { + $nv += $this->contours[$i]->nvertices(); + } + + return $nv; + } + + /** + * Get minimum bounding rectangle + * + * @return array ['min' => Point, 'max' => Point] + */ + public function getBoundingBox() : array + { + $min_x = PHP_INT_MAX; + $min_y = PHP_INT_MAX; + + $max_x = PHP_INT_MIN; + $max_y = PHP_INT_MIN; + + for ($i = 0; $i < $this->ncontours(); $i++) { + $box = $this->contours[$i]->getBoundingBox(); + + $min_tmp = $box['min']; + $max_tmp = $box['max']; + + if ($min_tmp->x < $min_x) { + $min_x = $min_tmp->x; + } + + if ($max_tmp->x > $max_x) { + $max_x = $max_tmp->x; + } + + if ($min_tmp->y < $min_y) { + $min_y = $min_tmp->y; + } + + if ($max_tmp->y > $max_y) { + $max_y = $max_tmp->y; + } + } + + return [ + 'min' => new Point($min_x, $min_y), + 'max' => new Point($max_x, $max_y) + ]; + } + + /** + * @param float $x + * @param float $y + * @return void + */ + public function move(float $x, float $y) + { + for ($i = 0; $this->ncontours(); $i++) { + $this->contours[$i]->move($x, $y); + } + } + + /** + * @param Contour $contour + */ + public function push_back(Contour $contour) + { + $this->contours[] = $contour; + } + + public function pop_back() + { + array_pop($this->contours); + } + + /** + * @param int $index + * @return void + */ + public function erase(int $index) + { + unset($this->contours[$index]); + } + + /** + * Empty the polygon + */ + public function clear() + { + unset($this->contours); + } + + public function toArray() : array + { + if (empty($this->contours)) { + return []; + } + + $contours_xy = []; + + foreach ($this->contours as $contour) { + $points_xy = []; + + foreach ($contour->points as $point) { + $points_xy[] = [$point->x, $point->y]; + } + + $contours_xy[] = $points_xy; + } + + return $contours_xy; + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/PriorityQueue.php b/deps/martinez-rueda-php/src/PriorityQueue.php new file mode 100644 index 0000000..4d830b8 --- /dev/null +++ b/deps/martinez-rueda-php/src/PriorityQueue.php @@ -0,0 +1,107 @@ +events); + } + + /** + * @return bool + */ + public function isEmpty() : bool + { + return empty($this->events); + } + + /** + * @param SweepEvent $event + */ + public function enqueue(SweepEvent $event) + { + if (!$this->isSorted()) { + $this->events[] = $event; + return; + } + + if (sizeof($this->events) <= 0) { + $this->events[] = $event; + return; + } + + // priority queue is sorted, shift elements to the right and find place for event + for ($i = sizeof($this->events) - 1; $i >= 0 && $this->compare($event, $this->events[$i]); $i--) { + $this->events[$i + 1] = $this->events[$i]; + } + + $this->events[$i + 1] = $event; + } + + /** + * @return mixed + */ + public function dequeue() : SweepEvent + { + if (!$this->isSorted()) { + $this->sort(); + $this->sorted = true; + } + + return array_pop($this->events); + } + + /** + * @return void + */ + public function sort() + { + uasort( + $this->events, + function ($event1, $event2) { + return $this->compare($event1, $event2) ? -1 : 1; + } + ); + + // We should actualize indexes, because of hash-table nature. + // array_values() is faster than juggling with indexes. + $this->events = array_values($this->events); + } + + /** + * @return bool + */ + public function isSorted() : bool + { + return $this->sorted; + } + + /** + * @param SweepEvent $event1 + * @param SweepEvent $event2 + * @return bool + */ + protected function compare(SweepEvent $event1, SweepEvent $event2) : bool + { + return Helper::compareSweepEvents($event1, $event2); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/Segment.php b/deps/martinez-rueda-php/src/Segment.php new file mode 100644 index 0000000..ccbca09 --- /dev/null +++ b/deps/martinez-rueda-php/src/Segment.php @@ -0,0 +1,51 @@ +setBegin($p1); + $this->setEnd($p2); + } + + /** + * @param Point $p + */ + public function setBegin(Point $p) + { + $this->p1 = $p; + } + + /** + * @param Point $p + */ + public function setEnd(Point $p) + { + $this->p2 = $p; + } + + public function begin() : Point + { + return $this->p1; + } + + public function end() : Point + { + return $this->p2; + } + + public function changeOrientation() + { + $tmp = $this->p1; + $this->p1 = $this->p2; + $this->p2 = $tmp; + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/SweepEvent.php b/deps/martinez-rueda-php/src/SweepEvent.php new file mode 100644 index 0000000..203e047 --- /dev/null +++ b/deps/martinez-rueda-php/src/SweepEvent.php @@ -0,0 +1,154 @@ +p) + * + * @var bool|null + */ + public $is_left = null; + + /** + * Indicates if the edge belongs to subject or clipping polygon + * + * @var int|null + */ + public $polygon_type = null; + + /** + * Inside-outside transition into the polygon + * + * @var bool|null + */ + public $in_out = null; + + /** + * Is the edge (p, other->p) inside the other polygon + * + * @var bool|null + */ + public $inside = null; + + /** + * Used for overlapped edges + * + * @var int|null + */ + public $edge_type = null; + + /** + * For sorting, increases monotonically + * + * @var int + */ + public $id = 0; + + /** + * @deprecated + * @var null + */ + public $pos = null; // in s + + /** + * SweepEvent constructor. + * @param Point $p + * @param bool $is_left + * @param int $associated_polygon + * @param null $other + * @param int $edge_type + */ + public function __construct( + Point $p, + bool $is_left, + int $associated_polygon, + $other = null, + $edge_type = Algorithm::EDGE_TYPE_NORMAL + ) { + $this->p = $p; + $this->is_left = $is_left; + $this->polygon_type = $associated_polygon; + $this->other = $other; + $this->edge_type = $edge_type; + + static $id = 0; + + $this->id = ++$id; + } + + /** + * @return int + */ + public function getId() : int + { + return $this->id; + } + + /** + * @return Segment + */ + public function segment() : Segment + { + return new Segment($this->p, $this->other->p); + } + + /** + * @param Point $point + * @return bool + */ + public function below(Point $point) : bool + { + return $this->is_left + ? Helper::signedArea($this->p, $this->other->p, $point) > 0 + : Helper::signedArea($this->other->p, $this->p, $point) > 0; + } + + /** + * @param Point $point + * @return bool + */ + public function above(Point $point) : bool + { + return !$this->below($point); + } + + /** + * @param SweepEvent $event + * @return bool + */ + public function equalsTo(SweepEvent $event) : bool + { + return $this->getId() === $event->getId(); + } + + /** + * @param SweepEvent $event + * @return bool + */ + public function lessThan(SweepEvent $event) : bool + { + return $this->getId() < $event->getId(); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/src/SweepLine.php b/deps/martinez-rueda-php/src/SweepLine.php new file mode 100644 index 0000000..bebf2ef --- /dev/null +++ b/deps/martinez-rueda-php/src/SweepLine.php @@ -0,0 +1,87 @@ +events); + } + + /** + * @param $index + * @return SweepEvent + */ + public function get($index) : SweepEvent + { + if (!isset($this->events[$index])) { + throw new \InvalidArgumentException(sprintf('Undefined SweepLine->events offset `%s`', $index)); + } + + return $this->events[$index]; + } + + /** + * @param SweepEvent $removable + * @return void + */ + public function remove(SweepEvent $removable) + { + foreach ($this->events as $index => $item) { + if ($item->equalsTo($removable)) { + Helper::removeElementWithShift($this->events, $index); + break; + } + } + } + + /** + * @param SweepEvent $event + * @return int + */ + public function insert(SweepEvent $event) : int + { + if (sizeof($this->events) == 0) { + $this->events[] = $event; + return 0; + } + + // priority queue is sorted, shift elements to the right and find place for event + for ($i = sizeof($this->events) - 1; $i >= 0 && $this->compare($event, $this->events[$i]); $i--) { + $this->events[$i + 1] = $this->events[$i]; + } + + $this->events[$i + 1] = $event; + + return $i + 1; + } + + /** + * @param SweepEvent $event1 + * @param SweepEvent $event2 + * @return bool + */ + public function compare(SweepEvent $event1, SweepEvent $event2) : bool + { + return Helper::compareSegments($event1, $event2); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/tests/.gitkeep b/deps/martinez-rueda-php/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/deps/martinez-rueda-php/tests/AlgorithmIntersectionTest.php b/deps/martinez-rueda-php/tests/AlgorithmIntersectionTest.php new file mode 100644 index 0000000..9701517 --- /dev/null +++ b/deps/martinez-rueda-php/tests/AlgorithmIntersectionTest.php @@ -0,0 +1,45 @@ +implementation = new Algorithm(); + } + + /** + * Test simple intersection result of two intersected polygons. + * + * @link https://gist.github.com/kudm761/b4aeb62e5c36b596396df8503c01be38 + */ + public function testSimplePositiveCase() + { + $data = [[[-3.09814453125, 75.2250649237144], [-4.5703125, 75.12950410894491], [-7.822265625000001, 74.5081553020789], [-7.8662109375, 74.11003203722439], [-3.27392578125, 74.78737860165963], [-.263671875, 75.31445589169716], [-.63720703125, 75.55208098028335], [-1.8017578124999998, 75.53562529096112], [-3.09814453125, 75.2250649237144]]]; + $subject = new \MartinezRueda\Polygon($data); + + $data = [[[-6.26220703125, 75.29773546875684], [-6.17431640625, 75.17454893148678], [-5.09765625, 75.27541260821627], [-4.482421875, 75.03901279805076], [-6.04248046875, 74.9536886200003], [-5.625, 74.70065320517152], [-4.5263671875, 74.7180368083091], [-4.8779296875, 74.58426829888151], [-3.8232421874999996, 74.54332982677906], [-1.99951171875, 74.17008033257684], [-1.494140625, 74.58426829888151], [-1.2084960937499998, 75.13514201950775], [-3.75732421875, 75.18578927942626], [-4.72412109375, 75.40885422846455], [-6.26220703125, 75.29773546875684]]]; + $clipping = new \MartinezRueda\Polygon($data); + + $result = $this->implementation->getIntersection($subject, $clipping); + $tested = $result->toArray(); + + $this->assertNotEmpty($tested, 'Intersection result of two polygons is empty, array of arrays of points is expected.'); + + // correct result + $expected = [[[-1.2796928252687, 75.136556755688], [-3.27392578125, 74.78737860166], [-4.6982590500119, 74.577294250758], [-4.8779296875, 74.584268298882], [-4.5263671875, 74.718036808309], [-5.625, 74.700653205172], [-5.9101740444242, 74.873497536896], [-5.2691008760483, 74.99598701721], [-4.482421875, 75.039012798051], [-4.6689021667529, 75.110666638215], [-4.5703125, 75.129504108945], [-3.7158913374922, 75.184965974832], [-1.2796928252687, 75.136556755688]]]; + + $this->assertEquals( + sizeof($tested), + sizeof($expected), + sprintf('Result multipolygon should contain one polygon, but contains %d', sizeof($tested)) + ); + + $compare = Helper::compareMultiPolygons($expected, $tested); + + $this->assertTrue($compare['success'], $compare['reason']); + } +} \ No newline at end of file diff --git a/deps/martinez-rueda-php/tests/AlgorithmUnionTest.php b/deps/martinez-rueda-php/tests/AlgorithmUnionTest.php new file mode 100644 index 0000000..aa75940 --- /dev/null +++ b/deps/martinez-rueda-php/tests/AlgorithmUnionTest.php @@ -0,0 +1,91 @@ +implementation = new Algorithm(); + } + + /** + * Test simple union result of two intersected polygons. + * + * @link https://gist.github.com/kudm761/944eb9bbbd088e69f87421a1afa7218b + */ + public function testSimpleCase() + { + $data = [[[-5.69091796875, 75.50265886674975], [-6.218261718749999, 75.29215785826014], [-6.87744140625, 74.8219342035653], [-5.38330078125, 74.61344527005673], [-3.27392578125, 74.78737860165963], [-2.83447265625, 75.26423875224219], [-3.251953125, 75.59040636514479], [-5.69091796875, 75.50265886674975]]]; + $subject = new \MartinezRueda\Polygon($data); + + $data = [[[-1.4501953125, 75.1125778338579], [-1.9116210937499998, 75.40331785380344], [-3.2958984375, 75.49165372814439], [-3.80126953125, 75.33672086232664], [-5.5810546875, 74.95939165894974], [-7.31689453125, 74.62510096387147], [-5.515136718749999, 74.15208909789665], [-4.19677734375, 74.86215220305225], [-2.373046875, 74.55503734449476], [-1.4501953125, 75.1125778338579]]]; + $clipping = new \MartinezRueda\Polygon($data); + + $result = $this->implementation->getUnion($subject, $clipping); + $tested = $result->toArray(); + + $this->assertNotEmpty($tested, 'Union result of two polygons is empty, array of arrays of points is expected.'); + + // correct result + $expected = [[[-1.91162109375, 75.403317853803], [-3.1104029643672, 75.479816573632], [-3.251953125, 75.590406365145], [-5.69091796875, 75.50265886675], [-6.21826171875, 75.29215785826], [-6.87744140625, 74.821934203565], [-6.5396028340834, 74.774792989124], [-7.31689453125, 74.625100963871], [-5.51513671875, 74.152089097897], [-4.5275307386443, 74.684009742754], [-3.5953601631731, 74.760873995822], [-2.373046875, 74.555037344495], [-1.4501953125, 75.112577833858], [-1.91162109375, 75.403317853803]]]; + + $this->assertEquals( + sizeof($tested), + sizeof($expected), + sprintf('Result multipolygon should contain one polygon, but contains %d', sizeof($tested)) + ); + + // one polygon is expected in this union + $expected_size = sizeof($expected[0]); + $tested_size = sizeof($tested[0]); + + $this->assertEquals( + $expected_size, + $tested_size, + sprintf('Size of result polygon is %d, but %d expected', $tested_size, $expected_size) + ); + + $compare = Helper::compareMultiPolygons($expected, $tested); + + $this->assertTrue($compare['success'], $compare['reason']); + } + + /** + * Test simple union result of two intersected polygons + * with a hole between them. + * + * https://gist.github.com/kudm761/5b566e98698e8f8cdf2fe7cfdab04b58 + */ + public function testSimpleCaseWithHole() + { + $data = [[[-4.1748046875, 75.52464464250062], [-6.701660156249999, 75.52464464250062], [-6.74560546875, 74.44346576284508], [-3.75732421875, 74.44935750063425], [-3.7353515625, 74.76429887097666], [-4.8779296875, 74.76718570583334], [-4.866943359375, 75.30331101068566], [-3.8452148437499996, 75.30331101068566], [-3.8452148437499996, 75.52464464250062], [-4.1748046875, 75.52464464250062]]]; + $subject = new \MartinezRueda\Polygon($data); + + $data = [[[-4.383544921875, 75.59587329063447], [-4.427490234375, 74.36371391783985], [-2.6806640625, 74.36667478672423], [-2.65869140625, 75.59860599198842], [-4.383544921875, 75.59587329063447]]]; + $clipping = new \MartinezRueda\Polygon($data); + + $result = $this->implementation->getUnion($subject, $clipping); + $tested = $result->toArray(); + + $this->assertNotEmpty($tested, 'Union result of two polygons is empty, array of arrays of points is expected.'); + + // correct result + $expected = [ + [[-4.866943359375, 75.303311010686], [-4.8779296875, 74.767185705833], [-4.4131421817925, 74.766011374956], [-4.3939792383295, 75.303311010686], [-4.866943359375, 75.303311010686]], + [[-4.383544921875, 75.595873290634], [-4.386085311755, 75.524644642501], [-6.70166015625, 75.524644642501], [-6.74560546875, 74.443465762845], [-4.424482645139, 74.448042121598], [-4.427490234375, 74.36371391784], [-2.6806640625, 74.366674786724], [-2.65869140625, 75.598605991988], [-4.383544921875, 75.595873290634]] + ]; + + $this->assertEquals( + sizeof($tested), + sizeof($expected), + sprintf('Result multipolygon should contain two polygons, but contains %d', sizeof($tested)) + ); + + $compare = Helper::compareMultiPolygons($expected, $tested); + + $this->assertTrue($compare['success'], $compare['reason']); + } +} \ No newline at end of file