diff --git a/src/Line.php b/src/Line.php index 085d729..00f510e 100644 --- a/src/Line.php +++ b/src/Line.php @@ -6,7 +6,6 @@ use Location\Bearing\BearingInterface; use Location\Distance\DistanceInterface; -use Location\Intersection\Intersection; use Location\Utility\Cartesian; use RuntimeException; @@ -62,7 +61,7 @@ public function getSegments(): array * Calculates the length of the line (distance between the two * coordinates in meters). * - * @param DistanceInterface $calculator instance of distance calculation class + * @param DistanceInterface $calculator instance of distance calculation class */ public function getLength(DistanceInterface $calculator): float { @@ -117,7 +116,7 @@ public function getMidpoint(): Coordinate * @see http://www.movable-type.co.uk/scripts/latlong.html#intermediate-point * @see http://www.edwilliams.org/avform.htm#Intermediate * - * @param float $fraction 0.0 ... 1.0 (smaller or larger values work too) + * @param float $fraction 0.0 ... 1.0 (smaller or larger values work too) * * @throws RuntimeException */ @@ -133,7 +132,7 @@ public function getIntermediatePoint(float $fraction): Coordinate if ($lat1 + $lat2 == 0.0 && abs($lng1 - $lng2) == M_PI) { throw new RuntimeException( 'Start and end points are antipodes, route is therefore undefined.', - 5382449689 + 5382449689, ); } @@ -153,6 +152,71 @@ public function getIntermediatePoint(float $fraction): Coordinate return new Coordinate(rad2deg($lat), rad2deg($lng)); } + /** + * Two lines intersect if: + * + * 1. The points of the given line are oriented into opposite directions + * 2. The points of this line are oriented into opposite directions + * 3. The points are collinear and the line segments are overlapping + */ + public function intersectsLine(Line $line): bool + { + $orientation = []; + $orientation[11] = $this->getOrientation($line->point1); + $orientation[12] = $this->getOrientation($line->point2); + $orientation[21] = $line->getOrientation($this->point1); + $orientation[22] = $line->getOrientation($this->point2); + + // the lines cross + if ( + $orientation[11] !== $orientation[12] + && $orientation[21] !== $orientation[22] + ) { + return true; + } + + // the lines are collinear or touch + if (!in_array(self::ORIENTATION_COLLINEAR, $orientation, true)) { + return false; + } + + if ( + $orientation[11] === self::ORIENTATION_COLLINEAR && $this->isPointInBounds( + $line->point1, + $this->getBounds(), + ) + ) { + return true; + } + if ( + $orientation[12] === self::ORIENTATION_COLLINEAR && $this->isPointInBounds( + $line->point2, + $this->getBounds(), + ) + ) { + return true; + } + if ( + $orientation[21] === self::ORIENTATION_COLLINEAR && $this->isPointInBounds( + $this->point1, + $line->getBounds(), + ) + ) { + return true; + } + if ( + $orientation[22] === self::ORIENTATION_COLLINEAR && $this->isPointInBounds( + $this->point2, + $line->getBounds(), + ) + ) { + return true; + } + + // the lines do not overlap + return false; + } + /** * Compares the location of a given coordinate to this line returning * its orientation as: @@ -164,9 +228,9 @@ public function getIntermediatePoint(float $fraction): Coordinate public function getOrientation(Coordinate $coordinate): int { $crossproduct1 = ($this->point2->getLat() - $this->point1->getLat()) - * ($coordinate->getLng() - $this->point2->getLng()); - $crossproduct2 = ($this->point2->getLng() - $this->point1->getLng()) - * ($coordinate->getLat() - $this->point2->getLat()); + * $this->getDeltaLng($this->point2, $coordinate); + $crossproduct2 = $this->getDeltaLng($this->point1, $this->point2) + * ($coordinate->getLat() - $this->point2->getLat()); $delta = $crossproduct1 - $crossproduct2; if ($delta > 0) { @@ -180,38 +244,26 @@ public function getOrientation(Coordinate $coordinate): int return self::ORIENTATION_COLLINEAR; } - /** - * Two lines intersect if: - * - * 1. the points of the given line are oriented into opposite directions - * 2. the points of this line are oriented into opposite directions - * 3. the points are collinear and the two line segments are overlapping - */ - public function intersectsLine(Line $line): bool + private function getDeltaLng(Coordinate $point1, Coordinate $point2): float { - $orientation = []; - $orientation[11] = $this->getOrientation($line->point1); - $orientation[12] = $this->getOrientation($line->point2); - $orientation[21] = $line->getOrientation($this->point1); - $orientation[22] = $line->getOrientation($this->point2); + $deltaLng = $point2->getLng() - $point1->getLng(); - // the lines cross - if ( - $orientation[11] !== $orientation[12] - && $orientation[21] !== $orientation[22] - ) { - return true; + if ($deltaLng > 180) { + return $deltaLng - 360; } - // the lines are collinear or touch - if ( - in_array(self::ORIENTATION_COLLINEAR, $orientation, true) - && (new Intersection())->intersects($this, $line, false) - ) { - return true; + if ($deltaLng < -180) { + return $deltaLng + 360; } - // the lines do not overlap - return false; + return $deltaLng; + } + + private function isPointInBounds(Coordinate $point, Bounds $bounds): bool + { + return $point->getLat() >= $bounds->getSouth() + && $point->getLat() <= $bounds->getNorth() + && $point->getLng() >= $bounds->getWest() + && $point->getLng() <= $bounds->getEast(); } } diff --git a/tests/Location/LineIntersectionTest.php b/tests/Location/LineIntersectionTest.php new file mode 100644 index 0000000..9036c71 --- /dev/null +++ b/tests/Location/LineIntersectionTest.php @@ -0,0 +1,66 @@ +assertTrue($line1->intersectsLine($line2)); + } + + public function testIntersectsLineParallelDisjoint(): void + { + $line1 = new Line(new Coordinate(0, 0), new Coordinate(10, 0)); + $line2 = new Line(new Coordinate(0, 1), new Coordinate(10, 1)); + + $this->assertFalse($line1->intersectsLine($line2)); + } + + public function testIntersectsLineCollinearOverlapping(): void + { + $line1 = new Line(new Coordinate(0, 0), new Coordinate(10, 0)); + $line2 = new Line(new Coordinate(5, 0), new Coordinate(15, 0)); + + $this->assertTrue($line1->intersectsLine($line2)); + } + + public function testIntersectsLineCollinearDisjoint(): void + { + $line1 = new Line(new Coordinate(0, 0), new Coordinate(10, 0)); + $line2 = new Line(new Coordinate(12, 0), new Coordinate(15, 0)); + + $this->assertFalse($line1->intersectsLine($line2)); + } + + public function testIntersectsLineSharingEndpoint(): void + { + $line1 = new Line(new Coordinate(0, 0), new Coordinate(10, 0)); + $line2 = new Line(new Coordinate(10, 0), new Coordinate(10, 10)); + + $this->assertTrue($line1->intersectsLine($line2)); + } + + public function testIntersectsLineTJunction(): void + { + $line1 = new Line(new Coordinate(0, 0), new Coordinate(10, 0)); + $line2 = new Line(new Coordinate(5, 0), new Coordinate(5, 5)); + + $this->assertTrue($line1->intersectsLine($line2)); + } + + public function testIntersectsLineDatelineCrossing(): void + { + $line1 = new Line(new Coordinate(0, 179), new Coordinate(0, -179)); + $line2 = new Line(new Coordinate(1, 180), new Coordinate(-1, 180)); + + $this->assertTrue($line1->intersectsLine($line2)); + } +} diff --git a/tests/Regression/Github/96/Issue96Test.php b/tests/Regression/Github/96/Issue96Test.php new file mode 100644 index 0000000..19378b9 --- /dev/null +++ b/tests/Regression/Github/96/Issue96Test.php @@ -0,0 +1,38 @@ +assertFalse($line1->intersectsLine($line2)); + } + + public function testPassingForIssue96(): void + { + $line1 = new Line( + new Coordinate(0.0, 0.0), + new Coordinate(0.0, 1.999999), + ); + $line2 = new Line( + new Coordinate(2.0, 2.0), + new Coordinate(0.0, 10.0), + ); + + $this->assertFalse($line1->intersectsLine($line2)); + } +}