diff --git a/build.sbt b/build.sbt index 2257ef7..60b072d 100644 --- a/build.sbt +++ b/build.sbt @@ -29,7 +29,7 @@ libraryDependencies ++= Seq( "com.lihaoyi" % "fastparse_2.11" % "0.4.3" % "provided", "org.scalatest" %% "scalatest" % "2.2.1" % "test", "com.vividsolutions" % "jts" % "1.13" % "test", - "com.esri.geometry" % "esri-geometry-api" % "1.2.1" % "test" + "com.esri.geometry" % "esri-geometry-api" % "1.2.1" ) libraryDependencies ++= Seq( diff --git a/src/main/scala/magellan/Shape.scala b/src/main/scala/magellan/Shape.scala index f3fdf26..d0c689d 100644 --- a/src/main/scala/magellan/Shape.scala +++ b/src/main/scala/magellan/Shape.scala @@ -16,7 +16,9 @@ package magellan +import com.esri.core.geometry.OperatorBuffer import com.fasterxml.jackson.annotation.{JsonIgnore, JsonProperty} +import magellan.esri.ESRIUtil import org.apache.spark.sql.catalyst.InternalRow import org.apache.spark.sql.types._ @@ -173,6 +175,14 @@ trait Shape extends DataType with Serializable { */ @JsonIgnore def isEmpty(): Boolean + + def buffer(distance: Double): Polygon = { + val esriGeometry = ESRIUtil.toESRIGeometry(this) + val bufferedEsriGeometry = OperatorBuffer.local() + .execute(esriGeometry, null, distance, null) + + ESRIUtil.fromESRIGeometry(bufferedEsriGeometry).asInstanceOf[Polygon] + } } /** diff --git a/src/main/scala/magellan/dsl/package.scala b/src/main/scala/magellan/dsl/package.scala index dffa0b3..a783525 100644 --- a/src/main/scala/magellan/dsl/package.scala +++ b/src/main/scala/magellan/dsl/package.scala @@ -49,6 +49,8 @@ package object dsl { def withinRange(origin: Point, radius: Double): Column = Column(WithinCircleRange(c.expr, origin, radius)) def asGeoJSON(): Column = Column(AsGeoJSON(c.expr)) + + def buffer(distance: Double): Column = Column(Buffer(c.expr, distance)) } implicit def point(x: Column, y: Column) = Column(PointConverter(x.expr, y.expr)) @@ -57,6 +59,8 @@ package object dsl { implicit def asGeoJSON(x: Column) = Column(AsGeoJSON(x.expr)) + implicit def buffer(x: Column, distance: Double) = Column(Buffer(x.expr, distance)) + implicit class DslDataset[T](c: Dataset[T]) { def df: Dataset[T] = c diff --git a/src/main/scala/magellan/esri/ESRIUtil.scala b/src/main/scala/magellan/esri/ESRIUtil.scala new file mode 100644 index 0000000..ca66b25 --- /dev/null +++ b/src/main/scala/magellan/esri/ESRIUtil.scala @@ -0,0 +1,206 @@ +package magellan.esri + +import com.esri.core.geometry.{Geometry => ESRIGeometry, Point => ESRIPoint, Polygon => ESRIPolygon, Polyline => ESRIPolyLine} +import magellan._ +import scala.collection.mutable.ArrayBuffer + +object ESRIUtil { + + def fromESRIGeometry(geometry: ESRIGeometry): Shape = { + geometry match { + case esriPoint: ESRIPoint => fromESRI(esriPoint) + case esriPolyline: ESRIPolyLine => fromESRI(esriPolyline) + case esriPolygon: ESRIPolygon => fromESRI(esriPolygon) + } + } + + def toESRIGeometry(shape: Shape): ESRIGeometry = { + shape match { + case point: Point => toESRI(point) + case polyline: PolyLine => toESRI(polyline) + case polygon: Polygon => toESRI(polygon) + } + } + + /** + * Convert ESRI 2D Point to Magellan Point. + * + * @param esriPoint + * @return + */ + def fromESRI(esriPoint: ESRIPoint): Point = { + Point(esriPoint.getX, esriPoint.getY) + } + + /** + * Convert Magellan Point to ESRI 2D Point + * + * @param point + * @return + */ + def toESRI(point: Point): ESRIPoint = { + val esriPoint = new ESRIPoint() + esriPoint.setXY(point.getX(), point.getY()) + esriPoint + } + + /** + * Convert ESRI PolyLine to Magellan PolyLine. + * + * @param esriPolyLine + * @return + */ + def fromESRI(esriPolyLine: ESRIPolyLine): PolyLine = { + val length = esriPolyLine.getPointCount + if (length == 0) { + PolyLine(Array[Int](), Array[Point]()) + } else { + val indices = ArrayBuffer[Int]() + indices.+=(0) + val points = ArrayBuffer[Point]() + var start = esriPolyLine.getPoint(0) + var currentRingIndex = 0 + points.+=(Point(start.getX(), start.getY())) + + for (i <- (1 until length)) { + val p = esriPolyLine.getPoint(i) + val j = esriPolyLine.getPathEnd(currentRingIndex) + if (j < length) { + val end = esriPolyLine.getPoint(j) + if (p.getX == end.getX && p.getY == end.getY) { + indices.+=(i) + currentRingIndex += 1 + // add start point + points.+= (Point(start.getX(), start.getY())) + start = end + } + } + points.+=(Point(p.getX(), p.getY())) + } + PolyLine(indices.toArray, points.toArray) + } + } + + /** + * Convert Magellan PolyLine to ESRI PolyLine. + * + * @param polyline + * @return + */ + def toESRI(polyline: PolyLine): ESRIPolyLine = { + val l = new ESRIPolyLine() + val indices = polyline.getRings() + val length = polyline.length + if (length > 0) { + var startIndex = 0 + var endIndex = 1 + var currentRingIndex = 0 + val startVertex = polyline.getVertex(startIndex) + l.startPath( + startVertex.getX(), + startVertex.getY()) + + while (endIndex < length) { + val endVertex = polyline.getVertex(endIndex) + l.lineTo(endVertex.getX(), endVertex.getY()) + startIndex += 1 + endIndex += 1 + // if we reach a ring boundary skip it + val nextRingIndex = currentRingIndex + 1 + if (nextRingIndex < indices.length) { + val nextRing = indices(nextRingIndex) + if (endIndex == nextRing) { + startIndex += 1 + endIndex += 1 + currentRingIndex = nextRingIndex + val startVertex = polyline.getVertex(startIndex) + l.startPath( + startVertex.getX(), + startVertex.getY()) + } + } + } + } + l + } + + /** + * Convert ESRI Polygon to Magellan Polygon. + * + * @param esriPolygon + * @return + */ + def fromESRI(esriPolygon: ESRIPolygon): Polygon = { + val length = esriPolygon.getPointCount + if (length == 0) { + Polygon(Array[Int](), Array[Point]()) + } else { + val indices = ArrayBuffer[Int]() + indices.+=(0) + val points = ArrayBuffer[Point]() + var start = esriPolygon.getPoint(0) + var currentRingIndex = 0 + points.+=(Point(start.getX(), start.getY())) + + for (i <- (1 until length)) { + val p = esriPolygon.getPoint(i) + val j = esriPolygon.getPathEnd(currentRingIndex) + if (j < length) { + val end = esriPolygon.getPoint(j) + if (p.getX == end.getX && p.getY == end.getY) { + indices.+=(i) + currentRingIndex += 1 + // add start point + points.+= (Point(start.getX(), start.getY())) + start = end + } + } + points.+=(Point(p.getX(), p.getY())) + } + Polygon(indices.toArray, points.toArray) + } + } + + /** + * Convert Magellan Polygon to ESRI Polygon. + * + * @param polygon + * @return + */ + def toESRI(polygon: Polygon): ESRIPolygon = { + val p = new ESRIPolygon() + val indices = polygon.getRings() + val length = polygon.length + if (length > 0) { + var startIndex = 0 + var endIndex = 1 + var currentRingIndex = 0 + val startVertex = polygon.getVertex(startIndex) + p.startPath( + startVertex.getX(), + startVertex.getY()) + + while (endIndex < length) { + val endVertex = polygon.getVertex(endIndex) + p.lineTo(endVertex.getX(), endVertex.getY()) + startIndex += 1 + endIndex += 1 + // if we reach a ring boundary skip it + val nextRingIndex = currentRingIndex + 1 + if (nextRingIndex < indices.length) { + val nextRing = indices(nextRingIndex) + if (endIndex == nextRing) { + startIndex += 1 + endIndex += 1 + currentRingIndex = nextRingIndex + val startVertex = polygon.getVertex(startIndex) + p.startPath( + startVertex.getX(), + startVertex.getY()) + } + } + } + } + p + } +} diff --git a/src/main/scala/org/apache/spark/sql/catalyst/expressions/functions.scala b/src/main/scala/org/apache/spark/sql/catalyst/expressions/functions.scala index 89d6869..26905ab 100644 --- a/src/main/scala/org/apache/spark/sql/catalyst/expressions/functions.scala +++ b/src/main/scala/org/apache/spark/sql/catalyst/expressions/functions.scala @@ -285,6 +285,59 @@ case class AsGeoJSON(override val child: Expression) } } +case class Buffer(override val child: Expression, distance: Double) + extends UnaryExpression with MagellanExpression { + + override def nullable: Boolean = false + + override def dataType: DataType = new PolygonUDT() + + override protected def nullSafeEval(input: Any): Any = { + val shape = newInstance(input.asInstanceOf[InternalRow]) + val bufferedShape = shape.buffer(distance) + serialize(bufferedShape) + } + + override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { + val childTypeVar = ctx.freshName("childType") + val childShapeVar = ctx.freshName("childShape") + val shapeSerializerVar = ctx.freshName("shapeSerializer") + val resultSerializerVar = ctx.freshName("resultSerializer") + val distanceVar = ctx.freshName("distance") + val resultVar = ctx.freshName("result") + val resultTypeVar = ctx.freshName("resultType") + val serializersVar = ctx.freshName("serializers") + val idx = ctx.references.length + ctx.addReferenceObj("distance", distance); + + ctx.addMutableState(classOf[java.util.HashMap[Integer, UserDefinedType[Shape]]].getName, s"$serializersVar", + s"$serializersVar = new java.util.HashMap>() ;" + + s"$serializersVar.put(1, new org.apache.spark.sql.types.PointUDT());" + + s"$serializersVar.put(2, new org.apache.spark.sql.types.LineUDT());" + + s"$serializersVar.put(3, new org.apache.spark.sql.types.PolyLineUDT());" + + s"$serializersVar.put(5, new org.apache.spark.sql.types.PolygonUDT());" + + "") + + nullSafeCodeGen(ctx, ev, (c1) => { + s"" + + s"Integer $childTypeVar = $c1.getInt(0); \n" + + s"Double $distanceVar = (Double) references[$idx]; \n" + + s"org.apache.spark.sql.types.UserDefinedType $shapeSerializerVar = " + + s"((org.apache.spark.sql.types.UserDefinedType)" + + s"$serializersVar.get($childTypeVar)); \n" + + s"magellan.Shape $childShapeVar = (magellan.Shape)" + + s"$shapeSerializerVar.deserialize($c1); \n" + + s"magellan.Shape $resultVar = $childShapeVar.buffer($distanceVar); \n" + + s"Integer $resultTypeVar = $resultVar.getType(); \n" + + s"org.apache.spark.sql.types.UserDefinedType $resultSerializerVar = " + + s"((org.apache.spark.sql.types.UserDefinedType)" + + s"$serializersVar.get($resultTypeVar)); \n" + + s"${ev.value} = (org.apache.spark.sql.catalyst.InternalRow)$resultSerializerVar.serialize($resultVar); \n" + }) + + } +} + object Indexer { val indexUDT = new ZOrderCurveUDT() diff --git a/src/test/scala/magellan/PointSuite.scala b/src/test/scala/magellan/PointSuite.scala index 02fda82..4fc1f1d 100644 --- a/src/test/scala/magellan/PointSuite.scala +++ b/src/test/scala/magellan/PointSuite.scala @@ -69,6 +69,16 @@ class PointSuite extends FunSuite with TestSparkContext { test("within circle") { assert(Point(0.0, 0.0) withinCircle (Point(0.5, 0.5), 0.75)) assert(!(Point(0.0, 0.0) withinCircle (Point(0.5, 0.5), 0.5))) + } + test("buffer point") { + val polygon = Point(0.0, 1.0).buffer(0.5) + assert(polygon.getNumRings() === 1) + // check that [0.0, 0.75] is within this polygon + assert(polygon.contains(Point(0.0, 0.75))) + // check that [0.4, 1.0] is within this polygon + assert(polygon.contains(Point(0.4, 1.0))) + // check that [0.6, 1.0] is outside this polygon + assert(!polygon.contains(Point(0.6, 1.0))) } } diff --git a/src/test/scala/magellan/PolyLineSuite.scala b/src/test/scala/magellan/PolyLineSuite.scala index 1929702..eb5f138 100644 --- a/src/test/scala/magellan/PolyLineSuite.scala +++ b/src/test/scala/magellan/PolyLineSuite.scala @@ -115,4 +115,24 @@ class PolyLineSuite extends FunSuite with TestSparkContext { val y = PolyLine(Array(0), Array(Point(0.5, 0.0), Point(0.0, 0.5))) assert(y.contains(Point(0.5, 0.0))) } + + test("buffer polyline") { + /** + * +-------+ 1.5,1.5 + * +----+ + + * + + + * +----+ + + * +-------+ + * + */ + + val ring = Array(Point(-1.0, 1.0), Point(1.0, 1.0), + Point(1.0, -1.0), Point(-1.0, -1.0)) + val polyline = PolyLine(Array(0), ring) + + val bufferedPolygon = polyline.buffer(0.5) + assert(bufferedPolygon.getNumRings() === 1) + assert(bufferedPolygon.contains(Point(1.3, 1.3))) + assert(!bufferedPolygon.contains(Point(0.5, 0.5))) + } } diff --git a/src/test/scala/magellan/PolygonSuite.scala b/src/test/scala/magellan/PolygonSuite.scala index 6525add..e7d3701 100644 --- a/src/test/scala/magellan/PolygonSuite.scala +++ b/src/test/scala/magellan/PolygonSuite.scala @@ -19,6 +19,7 @@ import com.esri.core.geometry.{GeometryEngine, Point => ESRIPoint, Polygon => ES import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import magellan.TestingUtils._ +import magellan.esri.ESRIUtil import org.scalatest.FunSuite @@ -60,7 +61,7 @@ class PolygonSuite extends FunSuite { // contains is a strict check. agrees with definition in ESRI - val esriPolygon = toESRI(polygon) + val esriPolygon = ESRIUtil.toESRIGeometry(polygon) assert(!polygon.contains(Point(1.0, 1.0))) assert(!GeometryEngine.contains(esriPolygon, new ESRIPoint(1.0, 1.0), null)) @@ -82,7 +83,7 @@ class PolygonSuite extends FunSuite { // contains is a strict check. agrees with definition in ESRI - val esriPolygon = toESRI(polygon) + val esriPolygon = ESRIUtil.toESRIGeometry(polygon) assert(!polygon.contains(Point(1.0, 1.0))) assert(!GeometryEngine.contains(esriPolygon, new ESRIPoint(1.0, 1.0), null)) @@ -92,53 +93,13 @@ class PolygonSuite extends FunSuite { } - test("fromESRI") { - val esriPolygon = new ESRIPolygon() - // outer ring1 - esriPolygon.startPath(-200, -100) - esriPolygon.lineTo(200, -100) - esriPolygon.lineTo(200, 100) - esriPolygon.lineTo(-190, 100) - esriPolygon.lineTo(-190, 90) - esriPolygon.lineTo(-200, 90) - - // hole - esriPolygon.startPath(-100, 50) - esriPolygon.lineTo(100, 50) - esriPolygon.lineTo(100, -40) - esriPolygon.lineTo(90, -40) - esriPolygon.lineTo(90, -50) - esriPolygon.lineTo(-100, -50) - - // island - esriPolygon.startPath(-10, -10) - esriPolygon.lineTo(10, -10) - esriPolygon.lineTo(10, 10) - esriPolygon.lineTo(-10, 10) - - esriPolygon.reverseAllPaths() - - val polygon = fromESRI(esriPolygon) - assert(polygon.getRings() === Array(0, 6, 12)) - assert(polygon.getVertex(6) === Point(-200.0, -100.0)) - assert(polygon.getVertex(13) === Point(-100.0, 50.0)) - } - - test("toESRI") { + test("check within matches esri within") { // no hole var ring = 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)) var polygon = Polygon(Array(0), ring) - var esriPolygon = toESRI(polygon) - assert(esriPolygon.calculateRingArea2D(0) ~== 4.0 absTol 0.001) - assert(esriPolygon.getPathCount === 1) - assert(esriPolygon.getPoint(0).getX === 1.0) - assert(esriPolygon.getPoint(0).getY === 1.0) - assert(esriPolygon.getPoint(1).getX === 1.0) - assert(esriPolygon.getPoint(1).getY === -1.0) - assert(esriPolygon.getPoint(3).getX === -1.0) - assert(esriPolygon.getPoint(3).getY === 1.0) + var esriPolygon = ESRIUtil.toESRI(polygon) val esriPoint = new ESRIPoint() @@ -158,7 +119,7 @@ class PolygonSuite extends FunSuite { ) polygon = Polygon(Array(0, 5), ring) - esriPolygon = toESRI(polygon) + esriPolygon = ESRIUtil.toESRI(polygon) assert(esriPolygon.calculateRingArea2D(0) ~== 4.0 absTol 0.001) assert(esriPolygon.calculateRingArea2D(1) ~== -0.5 absTol 0.001) @@ -173,21 +134,21 @@ class PolygonSuite extends FunSuite { var esriPolyLine = new ESRIPolyline() var polyline = PolyLine(Array(0), Array(Point(0.0, 0.0), Point(3.0, 3.0))) - esriPolyLine = toESRI(polyline) + esriPolyLine = ESRIUtil.toESRI(polyline) assert(polygon.intersects(polyline)) assert(!GeometryEngine.disjoint(esriPolygon, esriPolyLine, null)) polyline = PolyLine(Array(0), Array(Point(0.75, 0.75), Point(0.90, 0.90))) - esriPolyLine = toESRI(polyline) + esriPolyLine = ESRIUtil.toESRI(polyline) assert(polygon.intersects(polyline)) assert(!GeometryEngine.disjoint(esriPolygon, esriPolyLine, null)) polyline = PolyLine(Array(0), Array(Point(0.0, 0.0), Point(0.3, 0.0))) - esriPolyLine = toESRI(polyline) + esriPolyLine = ESRIUtil.toESRI(polyline) assert(!polygon.intersects(polyline)) assert(GeometryEngine.disjoint(esriPolygon, esriPolyLine, null)) @@ -421,4 +382,23 @@ class PolygonSuite extends FunSuite { assert(firstRing.equals(polygon1)) assert(secondRing.equals(polygon2)) } + + test("buffer polygon") { + /** + * +---------+ 1.5,1.5 + * + +----+ + + * + + + + + * + +----+ + + * +---------+ + * + */ + + val ring = 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 polygon = Polygon(Array(0), ring) + + val bufferedPolygon = polygon.buffer(0.5) + assert(bufferedPolygon.getNumRings() === 1) + assert(bufferedPolygon.contains(Point(1.3, 1.3))) + } } diff --git a/src/test/scala/magellan/TestingUtils.scala b/src/test/scala/magellan/TestingUtils.scala index 734b9d1..82a716d 100644 --- a/src/test/scala/magellan/TestingUtils.scala +++ b/src/test/scala/magellan/TestingUtils.scala @@ -16,7 +16,6 @@ package magellan -import com.esri.core.geometry.{Point => ESRIPoint, Polygon => ESRIPolygon, Polyline => ESRIPolyLine} import com.google.common.base.Splitter import magellan.geometry.R2Loop import org.apache.spark.sql.catalyst.InternalRow @@ -26,7 +25,6 @@ import org.apache.spark.sql.types.{PointUDT, PolygonUDT} import org.scalatest.exceptions.TestFailedException import scala.collection.JavaConversions._ -import scala.collection.mutable.ArrayBuffer object TestingUtils { @@ -115,124 +113,6 @@ object TestingUtils { override def toString: String = x.toString } - def fromESRI(esriPolygon: ESRIPolygon): Polygon = { - val length = esriPolygon.getPointCount - if (length == 0) { - Polygon(Array[Int](), Array[Point]()) - } else { - val indices = ArrayBuffer[Int]() - indices.+=(0) - val points = ArrayBuffer[Point]() - var start = esriPolygon.getPoint(0) - var currentRingIndex = 0 - points.+=(Point(start.getX(), start.getY())) - - for (i <- (1 until length)) { - val p = esriPolygon.getPoint(i) - val j = esriPolygon.getPathEnd(currentRingIndex) - if (j < length) { - val end = esriPolygon.getPoint(j) - if (p.getX == end.getX && p.getY == end.getY) { - indices.+=(i) - currentRingIndex += 1 - // add start point - points.+= (Point(start.getX(), start.getY())) - start = end - } - } - points.+=(Point(p.getX(), p.getY())) - } - Polygon(indices.toArray, points.toArray) - } - } - - def toESRI(polygon: Polygon): ESRIPolygon = { - val p = new ESRIPolygon() - val indices = polygon.getRings() - val length = polygon.length - if (length > 0) { - var startIndex = 0 - var endIndex = 1 - var currentRingIndex = 0 - val startVertex = polygon.getVertex(startIndex) - p.startPath( - startVertex.getX(), - startVertex.getY()) - - while (endIndex < length) { - val endVertex = polygon.getVertex(endIndex) - p.lineTo(endVertex.getX(), endVertex.getY()) - startIndex += 1 - endIndex += 1 - // if we reach a ring boundary skip it - val nextRingIndex = currentRingIndex + 1 - if (nextRingIndex < indices.length) { - val nextRing = indices(nextRingIndex) - if (endIndex == nextRing) { - startIndex += 1 - endIndex += 1 - currentRingIndex = nextRingIndex - val startVertex = polygon.getVertex(startIndex) - p.startPath( - startVertex.getX(), - startVertex.getY()) - } - } - } - } - p - } - - def toESRI(line: Line): ESRIPolyLine = { - val l = new ESRIPolyLine() - l.startPath(line.getStart().getX(), line.getStart().getY()) - l.lineTo(line.getEnd().getX(), line.getEnd().getY()) - l - } - - def toESRI(polyline: PolyLine): ESRIPolyLine = { - val l = new ESRIPolyLine() - val indices = polyline.getRings() - val length = polyline.length - if (length > 0) { - var startIndex = 0 - var endIndex = 1 - var currentRingIndex = 0 - val startVertex = polyline.getVertex(startIndex) - l.startPath( - startVertex.getX(), - startVertex.getY()) - - while (endIndex < length) { - val endVertex = polyline.getVertex(endIndex) - l.lineTo(endVertex.getX(), endVertex.getY()) - startIndex += 1 - endIndex += 1 - // if we reach a ring boundary skip it - val nextRingIndex = currentRingIndex + 1 - if (nextRingIndex < indices.length) { - val nextRing = indices(nextRingIndex) - if (endIndex == nextRing) { - startIndex += 1 - endIndex += 1 - currentRingIndex = nextRingIndex - val startVertex = polyline.getVertex(startIndex) - l.startPath( - startVertex.getX(), - startVertex.getY()) - } - } - } - } - l - } - - def toESRI(point: Point): ESRIPoint = { - val esriPoint = new ESRIPoint() - esriPoint.setXY(point.getX(), point.getY()) - esriPoint - } - def makeLoop(str: String): R2Loop = { val tokens = Splitter.on(',').split(str) val size = tokens.size diff --git a/src/test/scala/magellan/catalyst/BufferSuite.scala b/src/test/scala/magellan/catalyst/BufferSuite.scala new file mode 100644 index 0000000..39446b4 --- /dev/null +++ b/src/test/scala/magellan/catalyst/BufferSuite.scala @@ -0,0 +1,25 @@ +package magellan.catalyst + +import magellan.{Point, Polygon, TestSparkContext} +import org.apache.spark.sql.magellan.dsl.expressions._ +import org.scalatest.FunSuite + +class BufferSuite extends FunSuite with TestSparkContext { + + test("buffer point") { + val sqlCtx = this.sqlContext + import sqlCtx.implicits._ + val point = Point(0.0, 1.0) + val points = Seq((1, point)) + val df = sc.parallelize(points).toDF("id", "point") + val buffered = df.withColumn("buffered", $"point" buffer 0.5) + val polygon = buffered.select($"buffered").take(1)(0).get(0).asInstanceOf[Polygon] + assert(polygon.getNumRings() === 1) + // check that [0.0, 0.75] is within this polygon + assert(polygon.contains(Point(0.0, 0.75))) + // check that [0.4, 1.0] is within this polygon + assert(polygon.contains(Point(0.4, 1.0))) + // check that [0.6, 1.0] is outside this polygon + assert(!polygon.contains(Point(0.6, 1.0))) + } +} diff --git a/src/test/scala/magellan/catalyst/WKTSuite.scala b/src/test/scala/magellan/catalyst/WKTSuite.scala index c3b56e8..e9bb186 100644 --- a/src/test/scala/magellan/catalyst/WKTSuite.scala +++ b/src/test/scala/magellan/catalyst/WKTSuite.scala @@ -17,7 +17,7 @@ package magellan.catalyst import com.esri.core.geometry.GeometryEngine -import magellan.TestingUtils._ +import magellan.esri.ESRIUtil import magellan.{Point, Polygon, TestSparkContext} import org.apache.spark.sql.Row import org.apache.spark.sql.magellan.dsl.expressions._ @@ -82,13 +82,13 @@ class WKTSuite extends FunSuite with TestSparkContext { // compare with ESRI val esriPoints = points.collect().map { case Row(id: String, point: Point) => - val esriPoint = toESRI(point) + val esriPoint = ESRIUtil.toESRIGeometry(point) (id, esriPoint) } val esriResults = polygons.flatMap { case Row(polygonId: String, value: String, text: String, polygon: Polygon) => - val esriPolygon = toESRI(polygon) + val esriPolygon = ESRIUtil.toESRIGeometry(polygon) esriPoints.map {case (pointId, esriPoint) => val within = GeometryEngine.contains(esriPolygon, esriPoint, null) (within, pointId, polygonId) @@ -96,7 +96,7 @@ class WKTSuite extends FunSuite with TestSparkContext { } val expected = esriResults.collect().sortBy(_._2) - assert(expected.size === actual.size) + assert(expected.length === actual.length) assert(expected.map(x => (x._2, x._3)).deep === actual.deep) } } diff --git a/src/test/scala/magellan/esri/ESRIUtilSuite.scala b/src/test/scala/magellan/esri/ESRIUtilSuite.scala new file mode 100644 index 0000000..cddd416 --- /dev/null +++ b/src/test/scala/magellan/esri/ESRIUtilSuite.scala @@ -0,0 +1,109 @@ +package magellan.esri + +import com.esri.core.geometry.{Point => ESRIPoint, Polygon => ESRIPolygon, Polyline => ESRIPolyline} +import org.scalatest.FunSuite +import magellan._ +import magellan.TestingUtils._ + +class ESRIUtilSuite extends FunSuite { + + test("to esri-point") { + val esriPoint = ESRIUtil.toESRI(Point(0.0, 1.0)) + assert(esriPoint.getX === 0.0) + assert(esriPoint.getY === 1.0) + } + + test("from esri-point") { + val esriPoint = new ESRIPoint(0.0, 1.0) + val point = ESRIUtil.fromESRI(esriPoint) + assert(point.getX() == 0.0) + assert(point.getY() == 1.0) + } + + test("to esri-polygon") { + // no hole + var ring = 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)) + var polygon = Polygon(Array(0), ring) + var esriPolygon = ESRIUtil.toESRI(polygon) + assert(esriPolygon.calculateRingArea2D(0) ~== 4.0 absTol 0.001) + assert(esriPolygon.getPathCount === 1) + assert(esriPolygon.getPoint(0).getX === 1.0) + assert(esriPolygon.getPoint(0).getY === 1.0) + assert(esriPolygon.getPoint(1).getX === 1.0) + assert(esriPolygon.getPoint(1).getY === -1.0) + assert(esriPolygon.getPoint(3).getX === -1.0) + assert(esriPolygon.getPoint(3).getY === 1.0) + } + + test("from esri-polygon") { + val esriPolygon = new ESRIPolygon() + // outer ring1 + esriPolygon.startPath(-200, -100) + esriPolygon.lineTo(200, -100) + esriPolygon.lineTo(200, 100) + esriPolygon.lineTo(-190, 100) + esriPolygon.lineTo(-190, 90) + esriPolygon.lineTo(-200, 90) + + // hole + esriPolygon.startPath(-100, 50) + esriPolygon.lineTo(100, 50) + esriPolygon.lineTo(100, -40) + esriPolygon.lineTo(90, -40) + esriPolygon.lineTo(90, -50) + esriPolygon.lineTo(-100, -50) + + // island + esriPolygon.startPath(-10, -10) + esriPolygon.lineTo(10, -10) + esriPolygon.lineTo(10, 10) + esriPolygon.lineTo(-10, 10) + + esriPolygon.reverseAllPaths() + + val polygon = ESRIUtil.fromESRI(esriPolygon) + assert(polygon.getRings() === Array(0, 6, 12)) + assert(polygon.getVertex(6) === Point(-200.0, -100.0)) + assert(polygon.getVertex(13) === Point(-100.0, 50.0)) + } + + test("to esri-polyline") { + var ring = Array(Point(1.0, 1.0), Point(1.0, -1.0), + Point(-1.0, -1.0), Point(-1.0, 1.0)) + var polyline = PolyLine(Array(0), ring) + var esriPolyline = ESRIUtil.toESRI(polyline) + assert(esriPolyline.getPoint(0).getX === 1.0) + assert(esriPolyline.getPoint(0).getY === 1.0) + assert(esriPolyline.getPoint(1).getX === 1.0) + assert(esriPolyline.getPoint(1).getY === -1.0) + assert(esriPolyline.getPoint(3).getX === -1.0) + assert(esriPolyline.getPoint(3).getY === 1.0) + } + + test("from esri-polyline") { + val esriPolyline = new ESRIPolyline() + // outer ring1 + esriPolyline.startPath(-200, -100) + esriPolyline.lineTo(200, -100) + esriPolyline.lineTo(200, 100) + esriPolyline.lineTo(-190, 100) + esriPolyline.lineTo(-190, 90) + esriPolyline.lineTo(-200, 90) + + esriPolyline.startPath(-100, 50) + esriPolyline.lineTo(100, 50) + esriPolyline.lineTo(100, -40) + esriPolyline.lineTo(90, -40) + esriPolyline.lineTo(90, -50) + esriPolyline.lineTo(-100, -50) + + + esriPolyline.reverseAllPaths() + + val polyline = ESRIUtil.fromESRI(esriPolyline) + assert(polyline.getRings() === Array(0, 6)) + assert(polyline.getVertex(2) === Point(-190.0, 100.0)) + assert(polyline.getVertex(7) === Point(-100.0, -50.0)) + } +}