Skip to content

Commit

Permalink
Refactor polyline (#166)
Browse files Browse the repository at this point in the history
* Add tests for polyline functions

* Refactor Polyline

* cleanup
  • Loading branch information
halfabrane authored and harsha2010 committed Sep 18, 2017
1 parent e80949e commit 236bd74
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 125 deletions.
166 changes: 96 additions & 70 deletions src/main/scala/magellan/PolyLine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
package magellan

import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty}
import magellan.geometry.{Curve, R2Loop}
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.GenericInternalRow
import org.apache.spark.sql.types._

import scala.util.control.Breaks._
import scala.collection.mutable.ArrayBuffer

/**
* A PolyLine is an ordered set of vertices that consists of one or more parts.
Expand All @@ -28,80 +31,105 @@ import scala.util.control.Breaks._
* Parts may or may not intersect one another
*/
@SQLUserDefinedType(udt = classOf[PolyLineUDT])
class PolyLine(
val indices: Array[Int],
val xcoordinates: Array[Double],
val ycoordinates: Array[Double],
override val boundingBox: BoundingBox) extends Shape {
class PolyLine extends Shape {

private var indices: Array[Int] = _
private var xcoordinates: Array[Double] = _
private var ycoordinates: Array[Double] = _

@transient var curves = new ArrayBuffer[Curve]()

@JsonIgnore private var _boundingBox: BoundingBox = _

private[magellan] def init(
indices: Array[Int],
xcoordinates: Array[Double],
ycoordinates: Array[Double],
boundingBox: BoundingBox): Unit = {

this.indices = indices
this.xcoordinates = xcoordinates
this.ycoordinates = ycoordinates
this._boundingBox = boundingBox
// initialize the loops
val offsets = indices.zip(indices.drop(1) ++ Array(xcoordinates.length))
for ((start, end) <- offsets) {
curves += ({
val curves = new R2Loop()
curves.init(xcoordinates, ycoordinates, start, end - 1)
curves
})
}
}
override def getType(): Int = 3

def this() {this(Array(0), Array(), Array(), BoundingBox(0,0,0,0))}
def init(row: InternalRow): Unit = {
init(row.getArray(5).toIntArray(),
row.getArray(6).toDoubleArray(),
row.getArray(7).toDoubleArray(),
BoundingBox(row.getDouble(1), row.getDouble(2), row.getDouble(3), row.getDouble(4)))
}

override def getType(): Int = 3
def serialize(): InternalRow = {
val row = new GenericInternalRow(8)
val BoundingBox(xmin, ymin, xmax, ymax) = boundingBox
row.update(0, getType())
row.update(1, xmin)
row.update(2, ymin)
row.update(3, xmax)
row.update(4, ymax)
row.update(5, new IntegerArrayData(indices))
row.update(6, new DoubleArrayData(xcoordinates))
row.update(7, new DoubleArrayData(ycoordinates))
row
}

@JsonProperty
private [magellan] def getXCoordinates(): Array[Double] = xcoordinates
private def getXCoordinates(): Array[Double] = xcoordinates

@JsonProperty
private [magellan] def getYCoordinates(): Array[Double] = ycoordinates

private [magellan] def contains(point:Point): Boolean = {
var startIndex = 0
var endIndex = 1
var contains = false
val length = xcoordinates.size

if(!exceedsBounds(point))
breakable {
while(endIndex < length) {
val startX = xcoordinates(startIndex)
val startY = ycoordinates(startIndex)
val endX = xcoordinates(endIndex)
val endY = ycoordinates(endIndex)
val slope = (endY - startY)/(endX - startX)
val pointSlope = (endY - point.getY())/(endX - point.getX())
if(slope == pointSlope) {
contains = true
break
}
startIndex += 1
endIndex += 1
}
private def getYCoordinates(): Array[Double] = ycoordinates

@JsonProperty
override def boundingBox = _boundingBox

private[magellan] def contains(point: Point): Boolean = {
val numLoops = curves.size
var touches = false
var i = 0
while (i < numLoops && !touches) {
touches |= curves(i).touches(point)
i += 1
}
contains
touches
}

def exceedsBounds(point:Point):Boolean = {
val BoundingBox(pt_xmin, pt_ymin, pt_xmax, pt_ymax) = point.boundingBox
val BoundingBox(xmin, ymin, xmax, ymax) = boundingBox

pt_xmin < xmin && pt_ymin < ymin ||
pt_xmax > xmax && pt_ymax > ymax
/**
* A polygon intersects a line iff it is a proper intersection,
* or if either vertex of the line touches the polygon.
*
* @param line
* @return
*/
private [magellan] def intersects(line: Line): Boolean = {
curves exists (_.intersects(line))
}

def intersects(line:Line):Boolean = {
var startIndex = 0
var endIndex = 1
var intersects = false
val length = xcoordinates.size

breakable {

while(endIndex < length) {
val startX = xcoordinates(startIndex)
val startY = ycoordinates(startIndex)
val endX = xcoordinates(endIndex)
val endY = ycoordinates(endIndex)
// check if any segment intersects incoming line
if(line.intersects(Line(Point(startX, startY), Point(endX, endY)))) {
intersects = true
break
}
startIndex += 1
endIndex += 1
}
}
intersects
}
@JsonIgnore
override def isEmpty(): Boolean = xcoordinates.length == 0

def length(): Int = xcoordinates.length

def getVertex(index: Int) = Point(xcoordinates(index), ycoordinates(index))

@JsonProperty
def getRings(): Array[Int] = indices

@JsonIgnore
def getNumRings(): Int = indices.length

def getRing(index: Int): Int = indices(index)

def canEqual(other: Any): Boolean = other.isInstanceOf[PolyLine]

Expand Down Expand Up @@ -133,9 +161,6 @@ class PolyLine(
???
}

@JsonIgnore
override def isEmpty(): Boolean = xcoordinates.length == 0

/*override def jsonValue: JValue =
("type" -> "udt") ~
("class" -> this.getClass.getName) ~
Expand Down Expand Up @@ -171,11 +196,12 @@ object PolyLine {
}
i += 1
}
new PolyLine(
val polyline = new PolyLine()
polyline.init(
indices,
points.map(_.getX()),
points.map(_.getY()),
BoundingBox(xmin, ymin, xmax, ymax)
)
BoundingBox(xmin, ymin, xmax, ymax))
polyline
}
}
}
8 changes: 8 additions & 0 deletions src/main/scala/magellan/Shape.scala
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,20 @@ trait Shape extends DataType with Serializable {

if (boundingBox.contains(other.boundingBox)) {
(this, other) match {

case (p: Point, q: Point) => p.equals(q)
case (p: Point, q: Line) => false
case (p: Point, q: Polygon) => false
case (p: Point, q: PolyLine) => false

case (p: Polygon, q: Point) => p.contains(q)
case (p: Polygon, q: Line) => p.contains(q)

case (p: Line, q: Point) => p.contains(q)
case (p: Line, q: Line) => p.contains(q)

case (p: PolyLine, q: Point) => p.contains(q)

case _ => ???
}
} else {
Expand Down
46 changes: 46 additions & 0 deletions src/main/scala/magellan/geometry/Curve.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright 2015 Ram Sriharsha
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package magellan.geometry

import magellan.{Line, Point, Relate}

/**
* A curve consists of a single chain of vertices represents a open curve on the plane.
*
* Curves are not allowed to have any duplicate vertices (whether adjacent or
* not), and non-adjacent edges are not allowed to intersect. Curves must have at
* least 2 vertices. Although these restrictions are not enforced in optimized
* code, you may get unexpected results if they are violated.
*/
trait Curve extends Serializable {

/**
* Returns true if the curve touches the point, false otherwise.
*
* @param point
* @return
*/
def touches(point: Point): Boolean

/**
* Returns true if the line intersects (properly or vertex touching) loop, false otherwise.
*
* @param line
* @return
*/
def intersects(line: Line): Boolean

}
17 changes: 7 additions & 10 deletions src/main/scala/magellan/geometry/Loop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
*/
package magellan.geometry

import magellan.Relate.Touches
import magellan.{Line, Point, Relate}

/**
*
* A Loop represents a simple polygon. It consists of a single
* A Loop represents a closed curve. It consists of a single
* chain of vertices where the first vertex is explicitly connected to the last.
*
* Loops are not allowed to have any duplicate vertices (whether adjacent or
Expand All @@ -32,7 +33,11 @@ import magellan.{Line, Point, Relate}
* that loops do not necessarily contain all (or any) of their vertices.
*
*/
trait Loop extends Serializable {
trait Loop extends Serializable with Curve {

override def touches(point: Point) = {
containsOrCrosses(point) == Touches
}

/**
* A loop contains the given point iff the point is properly contained within the
Expand All @@ -54,14 +59,6 @@ trait Loop extends Serializable {
*/
def containsOrCrosses(point: Point): Relate

/**
* Returns true if the line intersects (properly or vertex touching) loop, false otherwise.
*
* @param line
* @return
*/
def intersects(line: Line): Boolean

/**
* Returns true if the two loops intersect (properly or vertex touching), false otherwise.
* @param loop
Expand Down
5 changes: 4 additions & 1 deletion src/main/scala/magellan/geometry/R2Loop.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ class R2Loop extends Loop {

override def iterator() = new LoopIterator()

override def toString = s"R2Loop($xcoordinates, $ycoordinates, $startIndex, $endIndex)"
override def toString = s"R2Loop(${xcoordinates.mkString(",")}," +
s" ${ycoordinates.mkString(",")}," +
s" $startIndex," +
s" $endIndex)"

@inline private def intersects(point: Point, line: Line): Boolean = {
val (start, end) = (line.getStart(), line.getEnd())
Expand Down
22 changes: 4 additions & 18 deletions src/main/scala/org/apache/spark/sql/types/PolyLineUDT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,7 @@ class PolyLineUDT extends UserDefinedType[PolyLine] with GeometricUDT {
))

override def serialize(polyLine: PolyLine): InternalRow = {
val row = new GenericInternalRow(8)
val BoundingBox(xmin, ymin, xmax, ymax) = polyLine.boundingBox
row.update(0, polyLine.getType())
row.update(1, xmin)
row.update(2, ymin)
row.update(3, xmax)
row.update(4, ymax)
row.update(5, new IntegerArrayData(polyLine.indices))
row.update(6, new DoubleArrayData(polyLine.xcoordinates))
row.update(7, new DoubleArrayData(polyLine.ycoordinates))
row
polyLine.serialize()
}

override def serialize(shape: Shape) = serialize(shape.asInstanceOf[PolyLine])
Expand All @@ -38,13 +28,9 @@ class PolyLineUDT extends UserDefinedType[PolyLine] with GeometricUDT {

override def deserialize(datum: Any): PolyLine = {
val row = datum.asInstanceOf[InternalRow]
val polyLine = new PolyLine(
row.getArray(5).toIntArray(),
row.getArray(6).toDoubleArray(),
row.getArray(7).toDoubleArray(),
BoundingBox(row.getDouble(1), row.getDouble(2), row.getDouble(3), row.getDouble(4))
)
polyLine
val polyline = new PolyLine()
polyline.init(row)
polyline
}

override def pyUDT: String = "magellan.types.PolyLineUDT"
Expand Down
8 changes: 3 additions & 5 deletions src/test/scala/magellan/GeoJSONSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,9 @@ class GeoJSONSuite extends FunSuite with TestSparkContext {
import sqlCtx.implicits._
val p = df.select($"polyline").first()(0).asInstanceOf[PolyLine]
// [ -122.04864044239585, 37.408617050391001 ], [ -122.047741818556602, 37.408915362324983 ]
assert(p.indices.size === 2)
assert(p.xcoordinates.head == -122.04864044239585)
assert(p.ycoordinates.head == 37.408617050391001)
assert(p.xcoordinates.last == -122.047741818556602)
assert(p.ycoordinates.last == 37.408915362324983)
assert(p.getNumRings() === 2)
assert(p.getVertex(0) == Point(-122.04864044239585, 37.408617050391001))
assert(p.getVertex(1) == Point(-122.047741818556602, 37.408915362324983))
}

test("Read Polygon") {
Expand Down
Loading

0 comments on commit 236bd74

Please sign in to comment.