From aa9021eec14ccbdab4c90316ff9a7bf129873f8e Mon Sep 17 00:00:00 2001 From: Ram Sriharsha Date: Thu, 15 Mar 2018 19:22:42 -0700 Subject: [PATCH] Fix relate bug (#207) * Corrected BoundingBox.relate Within case use [new] Polygon:line contains test instead of intersects optimized the code a little to avoid extra intersection test * corrections * Use strict intersections, add tests * Upgrade coverage plugin * Reverting the coverage plugin changes --- src/main/scala/magellan/BoundingBox.scala | 7 +- src/main/scala/magellan/PolyLine.scala | 17 ++-- src/main/scala/magellan/Polygon.scala | 53 +++++------ src/main/scala/magellan/Shape.scala | 41 ++++++--- .../scala/magellan/BoundingBoxSuite.scala | 89 +++++++++++++++++++ src/test/scala/magellan/PolygonSuite.scala | 27 ++++++ 6 files changed, 187 insertions(+), 47 deletions(-) diff --git a/src/main/scala/magellan/BoundingBox.scala b/src/main/scala/magellan/BoundingBox.scala index 44d9831..c1a083e 100644 --- a/src/main/scala/magellan/BoundingBox.scala +++ b/src/main/scala/magellan/BoundingBox.scala @@ -103,6 +103,8 @@ case class BoundingBox(xmin: Double, ymin: Double, xmax: Double, ymax: Double) { Point(xmin, ymax) ) + // include both edges and diagonals + // diagonals catch corner cases of bounding box in annulus val lines = Array( Line(Point(xmin, ymin), Point(xmax, ymin)), Line(Point(xmin, ymin), Point(xmin, ymax)), @@ -112,8 +114,9 @@ case class BoundingBox(xmin: Double, ymin: Double, xmax: Double, ymax: Double) { Line(Point(xmin, ymax), Point(xmax, ymin)) ) - val lineIntersections = (lines filter (shape intersects _)).size - val vertexContained = (vertices filter (shape contains _)).size + // look for strict intersections between the edges of the bounding box and the shape + val lineIntersections = lines count (shape intersects (_, true)) + val vertexContained = vertices count (shape contains _) if (contains(shape.boundingBox)) { Contains diff --git a/src/main/scala/magellan/PolyLine.scala b/src/main/scala/magellan/PolyLine.scala index c17f56f..3577db3 100644 --- a/src/main/scala/magellan/PolyLine.scala +++ b/src/main/scala/magellan/PolyLine.scala @@ -106,13 +106,12 @@ class PolyLine extends Shape { /** - * A polygon intersects a line iff it is a proper intersection, - * or if either vertex of the line touches the polygon. + * A polyline intersects a line iff it is a proper intersection * * @param line * @return */ - private [magellan] def intersects(line: Line): Boolean = { + private [magellan] def intersects(line: Line, strict: Boolean): Boolean = { curves exists (_.intersects(line)) } @@ -131,9 +130,15 @@ class PolyLine extends Shape { def getRing(index: Int): Int = indices(index) - def intersects(polygon:Polygon):Boolean = { - // a polyline intersects a polygon iff any line intersects a polygon - curves exists (_.iterator().exists(polygon intersects)) + /** + * A polygon intersects a polyline iff any line intersects a polygon + * + * @param polygon + * @param strict is this a strict intersection? + * @return + */ + def intersects(polygon:Polygon, strict: Boolean):Boolean = { + curves exists (_.iterator().exists(line => polygon intersects (line, strict))) } def canEqual(other: Any): Boolean = other.isInstanceOf[PolyLine] diff --git a/src/main/scala/magellan/Polygon.scala b/src/main/scala/magellan/Polygon.scala index cf91572..4483146 100644 --- a/src/main/scala/magellan/Polygon.scala +++ b/src/main/scala/magellan/Polygon.scala @@ -121,35 +121,31 @@ class Polygon extends Shape { } /** - * A polygon intersects a line iff it is a proper intersection, - * or if either vertex of the line touches the polygon. + * A polygon intersects a line iff it is a proper intersection(strict), + * or if the interior of the polygon contains any part of the line. * * @param line + * @param strict * @return */ - private [magellan] def intersects(line: Line): Boolean = { - var intersects = false - if(this.contains(line.getStart()) || this.contains(line.getEnd())){ - intersects = true - } - else{ - intersects = loops.exists(_.intersects(line)) - } - intersects + private [magellan] def intersects(line: Line, strict: Boolean): Boolean = { + val interior = this.contains(line.getStart()) || this.contains(line.getEnd()) + val strictIntersects = loops.exists(_.intersects(line)) + strictIntersects || (!strict && interior) } /** - * A polygon intersects a polyline iff it is a proper intersection, + * A polygon intersects a polyline iff it is a proper intersection (strict), * or if either vertex of the polyline touches the polygon. * * @param polyline + * @param strict * @return */ - private [magellan] def intersects(polyline: PolyLine): Boolean = { - polyline.intersects(this) + private [magellan] def intersects(polyline: PolyLine, strict: Boolean): Boolean = { + polyline.intersects(this, strict) } - /** * A polygon intersects another polygon iff at least one edge of the * other polygon intersects this polygon. @@ -157,16 +153,15 @@ class Polygon extends Shape { * @param polygon * @return */ - private [magellan] def intersects(polygon: Polygon): Boolean = { - var intersects = false - if(polygon.getVertexes().exists(other => this.contains(other)) - || this.getVertexes().exists(vertex => polygon.contains(vertex))){ - intersects = true - } - else{ - intersects = polygon.loops.exists(otherLoop => this.loops.exists(_.intersects(otherLoop))) - } - intersects + private [magellan] def intersects(polygon: Polygon, strict: Boolean): Boolean = { + val touches = + polygon.getVertexes().exists(other => this.contains(other)) || + this.getVertexes().exists(vertex => polygon.contains(vertex)) + + val strictIntersects = polygon.loops + .exists(otherLoop => this.loops.exists(_.intersects(otherLoop))) + + strictIntersects || (!strict && touches) } private [magellan] def contains(box: BoundingBox): Boolean = { @@ -180,6 +175,12 @@ class Polygon extends Shape { !(lines exists (!contains(_))) } + /** + * Checks if the polygon intersects the bounding box in a strict sense. + * + * @param box + * @return + */ private [magellan] def intersects(box: BoundingBox): Boolean = { val BoundingBox(xmin, ymin, xmax, ymax) = box val lines = Array( @@ -353,4 +354,4 @@ class PolygonDeserializer extends JsonDeserializer[Polygon] { polygon } -} \ No newline at end of file +} diff --git a/src/main/scala/magellan/Shape.scala b/src/main/scala/magellan/Shape.scala index d0c689d..e13c97b 100644 --- a/src/main/scala/magellan/Shape.scala +++ b/src/main/scala/magellan/Shape.scala @@ -56,31 +56,31 @@ trait Shape extends DataType with Serializable { /** * Tests whether this shape intersects the argument shape. *

- * The intersects predicate has the following equivalent definitions: + * A strict intersection is one where *

* * @param other the Shape with which to compare this Shape + * @param strict is this intersection strict? * @return true if the two Shapes intersect * * @see Shape#disjoint */ - def intersects(other: Shape): Boolean = { + def intersects(other: Shape, strict: Boolean): Boolean = { if (!boundingBox.disjoint(other.boundingBox)) { (this, other) match { case (p: Point, q: Point) => p.equals(q) case (p: Point, q: Polygon) => q.touches(p) case (p: Polygon, q: Point) => p.touches(q) - case (p: Polygon, q: Line) => p.intersects(q) - case (p: Polygon, q: PolyLine) => p.intersects(q) - case (p: Polygon, q: Polygon) => p.intersects(q) - case (p: PolyLine, q: Line) => p.intersects(q) - case (p: PolyLine, q: Polygon) => p.intersects(q) - case (p: Line, q: Polygon) => q.intersects(p) - case (p: Line, q: PolyLine) => q.intersects(p) + case (p: Polygon, q: Line) => p.intersects(q, strict) + case (p: Polygon, q: PolyLine) => p.intersects(q, strict) + case (p: Polygon, q: Polygon) => p.intersects(q, strict) + case (p: PolyLine, q: Line) => p.intersects(q, strict) + case (p: PolyLine, q: Polygon) => p.intersects(q, strict) + case (p: Line, q: Polygon) => q.intersects(p, strict) + case (p: Line, q: PolyLine) => q.intersects(p, strict) case _ => ??? } } else { @@ -88,6 +88,21 @@ trait Shape extends DataType with Serializable { } } + /** + * Computes the non strict intersection between two shapes. + *

+ * The intersects predicate has the following equivalent definitions: + *

* + * + * @param other + * @return + */ + def intersects(other: Shape): Boolean = this.intersects(other, false) + /** * Tests whether this shape contains the * argument shape. @@ -122,7 +137,7 @@ trait Shape extends DataType with Serializable { case (p: Point, q: PolyLine) => false case (p: Polygon, q: Point) => p.contains(q) - case (p: Polygon, q: Line) => p.contains(q) + case (p: Polygon, q: Line) => ??? case (p: Line, q: Point) => p.contains(q) case (p: Line, q: Line) => p.contains(q) @@ -194,7 +209,7 @@ object NullShape extends Shape { override def isEmpty() = true - override def intersects(shape: Shape): Boolean = false + override def intersects(shape: Shape, strict: Boolean = false): Boolean = false override def contains(shape: Shape): Boolean = false diff --git a/src/test/scala/magellan/BoundingBoxSuite.scala b/src/test/scala/magellan/BoundingBoxSuite.scala index 375e4bd..42382de 100644 --- a/src/test/scala/magellan/BoundingBoxSuite.scala +++ b/src/test/scala/magellan/BoundingBoxSuite.scala @@ -74,4 +74,93 @@ class BoundingBoxSuite extends FunSuite { assert(!x.withinCircle(Point(0.5, 0.75), 0.5)) assert(!x.withinCircle(Point(0.5, 0.5), 0.2)) } + + test("Relate") { + /** + * +---------+ 1,1 + * + +----+ + + * + + + + + * + +----+ + + * +---------+ + * + */ + val box = BoundingBox(0.0, 0.0, 0.5, 0.5) + + val outerPolygon = Polygon( + Array(0), + Array(Point(1.0, 1.0), Point(1.0, -1.0), + Point(-1.0, -1.0), Point(-1.0, 1.0), Point(1.0, 1.0))) + + assert(box.relate(outerPolygon) === Relate.Within) + + /** + * +---------+ 1,1 + * + + + * + + + * + + + * +-----+---+ + * +----+ + * + + + * +----+ + */ + + val disjointPolygon = Polygon( + Array(0), + Array(Point(1.1, -1.0), Point(2.0, -1.0), + Point(2.0, -2.0), Point(1.1, -2.0), Point(1.1, -1.0))) + + assert(box.relate(disjointPolygon) === Relate.Disjoint) + + /** + * +---------+ 1,1 + * + + + * + + + * + + + * +-----+---+----+ + * + + + * +----+ + */ + + + val touchesPolygon = Polygon( + Array(0), + Array(Point(1.0, -1.0), Point(2.0, -1.0), + Point(2.0, -2.0), Point(1.0, -2.0), Point(1.0, -1.0))) + + /** + * +---------+ 1,1 + * + + + * + +----+ + * + + + + * +-----+---+ + + * +----+ + */ + + val touchesPolygon2 = Polygon( + Array(0), + Array(Point(1.0, 0.0), Point(2.0, 0.0), + Point(2.0, -2.0), Point(1.0, -2.0), Point(1.0, 0.0))) + + assert(box.relate(touchesPolygon2) == Relate.Disjoint) + + // the interiors of the boxes do not intersect + assert(box.relate(touchesPolygon) === Relate.Disjoint) + + /** + * +---------+ 1,1 + * + 0,0 + 2,0 + * + +---+----+ + * + + + + + * +-----+---+ + + * +--------+ + */ + + val intersectsPolygon = Polygon( + Array(0), + Array(Point(0.0, 0.0), Point(2.0, 0.0), + Point(2.0, -2.0), Point(0.0, -2.0), Point(0.0, 0.0))) + + assert(box.relate(intersectsPolygon) == Relate.Intersects) + + } } diff --git a/src/test/scala/magellan/PolygonSuite.scala b/src/test/scala/magellan/PolygonSuite.scala index 8e2a3cd..4e34f06 100644 --- a/src/test/scala/magellan/PolygonSuite.scala +++ b/src/test/scala/magellan/PolygonSuite.scala @@ -401,4 +401,31 @@ class PolygonSuite extends FunSuite { assert(bufferedPolygon.getNumRings() === 1) assert(bufferedPolygon.contains(Point(1.3, 1.3))) } + + test("strict intersection") { + // polygon - line + val outerRing = Array(Point(1.0, 1.0), Point(1.0, -1.0), + Point(-1.0, -1.0), Point(-1.0, 1.0), Point(1.0, 1.0)) + val outerPolygon = Polygon(Array(0), outerRing) + + val innerRing = Array(Point(0.5, 0.5), Point(0.5, -0.5), + Point(-0.5, -0.5), Point(-0.5, 0.5), Point(0.5, 0.5)) + val innerPolygon = Polygon(Array(0), innerRing) + + assert(outerPolygon.intersects(Line(Point(0.0, 0.0), Point(2.0, 2.0)), strict = true)) + assert(outerPolygon.intersects(Line(Point(0.0, 0.0), Point(1.0, 1.0)), strict = true)) + assert(outerPolygon.intersects(Line(Point(1.0, 1.0), Point(1.0, -1.0)), strict = true)) + + // interior of polygon contains line (disallowed in strict intersection) + assert(!outerPolygon.intersects(Line(Point(0.0, 0.0), Point(0.5, 0.5)), strict = true)) + + // polygon - polygon + assert(!outerPolygon.intersects(innerPolygon, strict = true)) + assert(outerPolygon.intersects(innerPolygon)) + + // polygon - polyline + val innerPolyline = PolyLine(Array(0), innerRing.dropRight(1)) + assert(!outerPolygon.intersects(innerPolyline,strict = true)) + assert(outerPolygon.intersects(innerPolyline)) + } }