Use martinez-rueda-php locally from https://github.com/kudm761/martinez-rueda-php import

This commit is contained in:
DataHoarder 2022-01-07 12:38:32 +01:00
parent 75507b2b9e
commit 0c2226d310
21 changed files with 2273 additions and 19 deletions

View file

@ -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": "*"
}

30
composer.lock generated
View file

@ -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"

2
deps/martinez-rueda-php/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.idea/
vendor/

42
deps/martinez-rueda-php/README.md vendored Normal file
View file

@ -0,0 +1,42 @@
## Martinez-Rueda polygon boolean operations algorithm
PHP implementation of original algorithm <http://www4.ujaen.es/~fmartin/bool_op.html>
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.
<img src="https://raw.githubusercontent.com/kudm761/kudm761.github.io/master/docs/aa_polygon_original.png" width="200">
###### Union
<img src="https://raw.githubusercontent.com/kudm761/kudm761.github.io/master/docs/aa_polygon_union.png" width="200">
###### Difference (green NOT yellow)
<img src="https://raw.githubusercontent.com/kudm761/kudm761.github.io/master/docs/aa_polygon_difference.png" width="200">
###### Intersection
<img src="https://raw.githubusercontent.com/kudm761/kudm761.github.io/master/docs/aa_polygon_intersection.png" width="200">
###### Xor
<img src="https://raw.githubusercontent.com/kudm761/kudm761.github.io/master/docs/aa_polygon_xor.png" width="200">

25
deps/martinez-rueda-php/composer.json vendored Normal file
View file

@ -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/"
}
}
}

28
deps/martinez-rueda-php/phpunit.xml vendored Normal file
View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
cacheTokens="true"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Martinez-Rueda algorithm polygon Boolean operations tests">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="false">
<directory suffix=".php">src</directory>
<exclude>
<directory suffix=".php">vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View file

@ -0,0 +1,649 @@
<?php
namespace MartinezRueda;
/**
* Time complexity O((n+k) log n)
* n is number of all edges of all polygons in operation
* k is number of intersections of all polygon edges
*
* Also Bentley - Ottmann algorithm is used for search of intersections
* @link https://en.wikipedia.org/wiki/Bentley%E2%80%93Ottmann_algorithm
*
* Class Algorithm
* @package MartinezRueda
*/
class Algorithm
{
const OPERATION_INTERSECTION = 'INTERSECTION';
const OPERATION_UNION = 'UNION';
const OPERATION_DIFFERENCE = 'DIFFERENCE';
const OPERATION_XOR = 'XOR';
const POLYGON_TYPE_SUBJECT = 1;
const POLYGON_TYPE_CLIPPING = 2;
const EDGE_TYPE_NORMAL = 1;
const EDGE_TYPE_NON_CONTRIBUTING = 2;
const EDGE_TYPE_SAME_TRANSITION = 3;
const EDGE_TYPE_DIFFERENT_TRANSITION = 4;
/**
* Deque
*
* @var array
* @deprecated
*/
protected $event_holder = [];
/**
* @var null|PriorityQueue
*/
protected $eq = null;
public function __construct()
{
$this->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);
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace MartinezRueda;
/**
* Class Connector
* @package MartinezRueda
*/
class Connector
{
public $open_polygons = [];
public $closed_polygons = [];
/**
* @var bool
*/
protected $closed = false;
public function isClosed() : bool
{
return $this->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);
}
}

224
deps/martinez-rueda-php/src/Contour.php vendored Normal file
View file

@ -0,0 +1,224 @@
<?php
namespace MartinezRueda;
/**
* Contour represents a sequence of vertices connected by line segments, forming a closed shape.
*
* Class Contour
* @package MartinezRueda
*/
class Contour
{
public $points = [];
protected $holes = [];
protected $is_external = false;
protected $cc = false;
protected $precomputed_cc = null;
public function __construct(array $points)
{
foreach ($points as $point) {
$this->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;
}
}
}

83
deps/martinez-rueda-php/src/Debug.php vendored Normal file
View file

@ -0,0 +1,83 @@
<?php
namespace MartinezRueda;
/**
* Class Debug
* @package MartinezRueda
*/
class Debug
{
public static $debug_on = false;
public static function debug(callable $callee)
{
if (self::$debug_on) {
$callee();
}
}
/**
* @param SweepEvent $event
* @return string
*/
public static function gatherSweepEventData(SweepEvent $event) : string
{
$data = [
'index' => $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;
}
}

188
deps/martinez-rueda-php/src/Helper.php vendored Normal file
View file

@ -0,0 +1,188 @@
<?php
namespace MartinezRueda;
/**
* Class Helper
* @package MartinezRueda
*/
class Helper
{
/**
* Signed area of the triangle (p0, p1, p2)
*
* @param Point $p0
* @param Point $p1
* @param Point $p2
* @return float
*/
public static function signedArea(Point $p0, Point $p1, Point $p2) : float
{
return ($p0->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' => ''];
}
}

45
deps/martinez-rueda-php/src/Point.php vendored Normal file
View file

@ -0,0 +1,45 @@
<?php
namespace MartinezRueda;
/**
* Class Point
* @package MartinezRueda
*/
class Point
{
public $x = null;
public $y = null;
/**
* Point constructor.
*
* @param float $x
* @param float $y
*/
public function __construct(float $x = 0, float $y = 0)
{
$this->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);
}
}

View file

@ -0,0 +1,160 @@
<?php
namespace MartinezRueda;
/**
* Class PointChain
* @package MartinezRueda
*/
class PointChain
{
public $segments = [];
public $closed = false;
/**
* PointChain constructor.
* @param array $segments
*/
public function __construct(array $segments = [])
{
$this->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;
}
}

171
deps/martinez-rueda-php/src/Polygon.php vendored Normal file
View file

@ -0,0 +1,171 @@
<?php
namespace MartinezRueda;
/**
* Class Polygon
* @package MartinezRueda
*/
class Polygon
{
public $contours = [];
/**
* Polygon constructor.
* Get array of contours (each is array of points and each point is 2-size array)
*
* contours_xy = [[]contour1, []contour2]
* []contour = [[]point1, []point2]
* []point = [x, y]
*
* @example $contours_xy = [[[1, 4], [4, 5], [6, 4], [1, 1]], [...]]
* @param array $contours_xy
*/
public function __construct(array $contours_xy)
{
foreach ($contours_xy as $contour_xy) {
$contour_points = [];
foreach ($contour_xy as $xy) {
$contour_points[] = new Point($xy[0], $xy[1]);
}
$this->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;
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace MartinezRueda;
/**
* Priority queue that holds sweep-events sorted from left to right.
*
* Class PriorityQueue
* @package MartinezRueda
*/
class PriorityQueue
{
/**
* Array of SweepEvents sorted from left to right
*
* @var array
*/
public $events = [];
protected $sorted = false;
/**
* @return int
*/
public function size() : int
{
return sizeof($this->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);
}
}

51
deps/martinez-rueda-php/src/Segment.php vendored Normal file
View file

@ -0,0 +1,51 @@
<?php
namespace MartinezRueda;
/**
* Class Segment
* @package MartinezRueda
*/
class Segment
{
public $p1 = null;
public $p2 = null;
public function __construct(Point $p1, Point $p2)
{
$this->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;
}
}

View file

@ -0,0 +1,154 @@
<?php
namespace MartinezRueda;
/**
* Event - X coordinate at which something interesting happens:
* left, right endpoint or edge intersection
*
* Class SweepEvent
* @package MartinezRueda
*/
class SweepEvent
{
/**
* Point associated with the event
*
* @var Point|null
*/
public $p = null;
/**
* Event associated to the other endpoint of the edge
*
* @var SweepEvent|null
*/
public $other = null;
/**
* Is the point the left endpoint of the edge (p, other->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();
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace MartinezRueda;
/**
* We use a vertical line to sweep the plane from left to right.
* At every moment the edges that intersect the sweep-line are stored here,
* ordered from bottom to top as they intersect the sweep-line.
*
* It consists of the ordered sequence of the edges of both polygons
* intersecting the sweep-line.
*
* For more explanation see sweep-line algorithm.
* @link https://en.wikipedia.org/wiki/Sweep_line_algorithm
*
* Class SweepLine
* @package MartinezRueda
*/
class SweepLine
{
/**
* @var array of SweepEvents
*/
public $events = [];
public function size() : int
{
return sizeof($this->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);
}
}

View file

View file

@ -0,0 +1,45 @@
<?php
use \MartinezRueda\Algorithm;
use MartinezRueda\Helper;
class AlgorithmIntersectionTest extends \PHPUnit\Framework\TestCase
{
protected $implementation = null;
public function setUp()
{
$this->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']);
}
}

View file

@ -0,0 +1,91 @@
<?php
use \MartinezRueda\Algorithm;
use MartinezRueda\Helper;
class AlgorithmUnionTest extends \PHPUnit\Framework\TestCase
{
protected $implementation = null;
public function setUp()
{
$this->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']);
}
}