diff --git a/common/src/main/java/org/apache/sedona/common/Functions.java b/common/src/main/java/org/apache/sedona/common/Functions.java index 13cfc619b7..542a9893af 100644 --- a/common/src/main/java/org/apache/sedona/common/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/Functions.java @@ -594,6 +594,14 @@ public static String geometryType(Geometry geometry) { return "ST_" + geometry.getGeometryType(); } + public static String geometryTypeWithMeasured(Geometry geometry) { + String geometryType = geometry.getGeometryType().toUpperCase(); + if (GeomUtils.isMeasuredGeometry(geometry)) { + geometryType += "M"; + } + return geometryType; + } + public static Geometry startPoint(Geometry geometry) { if (geometry instanceof LineString) { LineString line = (LineString) geometry; diff --git a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java index 2a210f5628..d564811d9c 100644 --- a/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java +++ b/common/src/main/java/org/apache/sedona/common/utils/GeomUtils.java @@ -14,13 +14,7 @@ package org.apache.sedona.common.utils; import org.locationtech.jts.geom.*; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.Polygon; import org.locationtech.jts.geom.impl.CoordinateArraySequence; - -import org.locationtech.jts.geom.CoordinateSequence; -import org.locationtech.jts.geom.CoordinateSequenceFilter; -import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.io.ByteOrderValues; import org.locationtech.jts.io.WKBWriter; import org.locationtech.jts.io.WKTWriter; @@ -30,7 +24,6 @@ import java.nio.ByteOrder; import java.util.*; -import java.util.List; import static org.locationtech.jts.geom.Coordinate.NULL_ORDINATE; @@ -489,4 +482,9 @@ public static Double getHausdorffDistance(Geometry g1, Geometry g2, double densi } return hausdorffDistanceObj.distance(); } + + public static Boolean isMeasuredGeometry(Geometry geom) { + Coordinate coordinate = geom.getCoordinate(); + return !Double.isNaN(coordinate.getM()); + } } diff --git a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java index 3dde5180f7..358f5eebda 100644 --- a/common/src/test/java/org/apache/sedona/common/FunctionsTest.java +++ b/common/src/test/java/org/apache/sedona/common/FunctionsTest.java @@ -1078,6 +1078,90 @@ public void affine2DHybridGeomCollection() { assertEquals(expectedPolygon2.toText(), actualGeomCollection.getGeometryN(0).getGeometryN(1).getGeometryN(1).toText()); } + @Test + public void geometryTypeWithMeasured2D() { + String expected1 = "POINT"; + String actual1 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createPoint(new Coordinate(10, 5))); + assertEquals(expected1, actual1); + + // Create a point with measure value + CoordinateXYM coords = new CoordinateXYM(2, 3, 4); + Point measuredPoint = GEOMETRY_FACTORY.createPoint(coords); + String expected2 = "POINTM"; + String actual2 = Functions.geometryTypeWithMeasured(measuredPoint); + assertEquals(expected2, actual2); + + // Create a linestring with measure value + CoordinateXYM[] coordsLineString = new CoordinateXYM[] {new CoordinateXYM(1, 2, 3), new CoordinateXYM(4, 5, 6)}; + LineString measuredLineString = GEOMETRY_FACTORY.createLineString(coordsLineString); + String expected3 = "LINESTRINGM"; + String actual3 = Functions.geometryTypeWithMeasured(measuredLineString); + assertEquals(expected3, actual3); + + // Create a polygon with measure value + CoordinateXYM[] coordsPolygon = new CoordinateXYM[] {new CoordinateXYM(0, 0, 0), new CoordinateXYM(1, 1, 0), new CoordinateXYM(0, 1, 0), new CoordinateXYM(0, 0, 0)}; + Polygon measuredPolygon = GEOMETRY_FACTORY.createPolygon(coordsPolygon); + String expected4 = "POLYGONM"; + String actual4 = Functions.geometryTypeWithMeasured(measuredPolygon); + assertEquals(expected4, actual4); + } + + @Test + public void geometryTypeWithMeasured3D() { + String expected1 = "POINT"; + String actual1 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createPoint(new Coordinate(10, 5, 1))); + assertEquals(expected1, actual1); + + // Create a point with measure value + CoordinateXYZM coordsPoint = new CoordinateXYZM(2, 3, 4, 0); + Point measuredPoint = GEOMETRY_FACTORY.createPoint(coordsPoint); + String expected2 = "POINTM"; + String actual2 = Functions.geometryTypeWithMeasured(measuredPoint); + assertEquals(expected2, actual2); + + // Create a linestring with measure value + CoordinateXYZM[] coordsLineString = new CoordinateXYZM[] {new CoordinateXYZM(1, 2, 3, 0), new CoordinateXYZM(4, 5, 6, 0)}; + LineString measuredLineString = GEOMETRY_FACTORY.createLineString(coordsLineString); + String expected3 = "LINESTRINGM"; + String actual3 = Functions.geometryTypeWithMeasured(measuredLineString); + assertEquals(expected3, actual3); + + // Create a polygon with measure value + CoordinateXYZM[] coordsPolygon = new CoordinateXYZM[] {new CoordinateXYZM(0, 0, 0, 0), new CoordinateXYZM(1, 1, 0, 0), new CoordinateXYZM(0, 1, 0, 0), new CoordinateXYZM(0, 0, 0, 0)}; + Polygon measuredPolygon = GEOMETRY_FACTORY.createPolygon(coordsPolygon); + String expected4 = "POLYGONM"; + String actual4 = Functions.geometryTypeWithMeasured(measuredPolygon); + assertEquals(expected4, actual4); + } + + @Test + public void geometryTypeWithMeasuredCollection() { + String expected1 = "GEOMETRYCOLLECTION"; + String actual1 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {GEOMETRY_FACTORY.createPoint(new Coordinate(10, 5))})); + assertEquals(expected1, actual1); + + // Create a geometrycollection with measure value + CoordinateXYM coords = new CoordinateXYM(2, 3, 4); + Point measuredPoint = GEOMETRY_FACTORY.createPoint(coords); + String expected2 = "GEOMETRYCOLLECTIONM"; + String actual2 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {measuredPoint})); + assertEquals(expected2, actual2); + + // Create a geometrycollection with measure value + CoordinateXYM[] coordsLineString = new CoordinateXYM[] {new CoordinateXYM(1, 2, 3), new CoordinateXYM(4, 5, 6)}; + LineString measuredLineString = GEOMETRY_FACTORY.createLineString(coordsLineString); + String expected3 = "GEOMETRYCOLLECTIONM"; + String actual3 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {measuredLineString})); + assertEquals(expected3, actual3); + + // Create a geometrycollection with measure value + CoordinateXYM[] coordsPolygon = new CoordinateXYM[] {new CoordinateXYM(0, 0, 0), new CoordinateXYM(1, 1, 0), new CoordinateXYM(0, 1, 0), new CoordinateXYM(0, 0, 0)}; + Polygon measuredPolygon = GEOMETRY_FACTORY.createPolygon(coordsPolygon); + String expected4 = "GEOMETRYCOLLECTIONM"; + String actual4 = Functions.geometryTypeWithMeasured(GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] {measuredPolygon})); + assertEquals(expected4, actual4); + } + @Test public void hausdorffDistanceDefaultGeom2D() throws Exception { Polygon polygon1 = GEOMETRY_FACTORY.createPolygon(coordArray3d(1, 0, 1, 1, 1, 2, 2, 1, 5, 2, 0, 1, 1, 0, 1)); diff --git a/docs/api/flink/Function.md b/docs/api/flink/Function.md index 9b172dd550..3d5cce6341 100644 --- a/docs/api/flink/Function.md +++ b/docs/api/flink/Function.md @@ -1,3 +1,37 @@ +## GeometryType + +Introduction: Returns the type of the geometry as a string. Eg: 'LINESTRING', 'POLYGON', 'MULTIPOINT', etc. This function also indicates if the geometry is measured, by returning a string of the form 'POINTM'. + +Format: `GeometryType (A:geometry)` + +Since: `v1.5.0` + +Example: + +```sql +SELECT GeometryType(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)')); +``` + +Result: + +``` + geometrytype +-------------- + LINESTRING +``` + +```sql +SELECT GeometryType(ST_GeomFromText('POINTM(0 0 1)')); +``` + +Result: + +``` + geometrytype +-------------- + POINTM +``` + ## ST_3DDistance Introduction: Return the 3-dimensional minimum cartesian distance between A and B diff --git a/docs/api/sql/Function.md b/docs/api/sql/Function.md index 2ea607b2f2..fc0bab7721 100644 --- a/docs/api/sql/Function.md +++ b/docs/api/sql/Function.md @@ -1,3 +1,37 @@ +## GeometryType + +Introduction: Returns the type of the geometry as a string. Eg: 'LINESTRING', 'POLYGON', 'MULTIPOINT', etc. This function also indicates if the geometry is measured, by returning a string of the form 'POINTM'. + +Format: `GeometryType (A:geometry)` + +Since: `v1.5.0` + +Example: + +```sql +SELECT GeometryType(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)')); +``` + +Result: + +``` + geometrytype +-------------- + LINESTRING +``` + +```sql +SELECT GeometryType(ST_GeomFromText('POINTM(0 0 1)')); +``` + +Result: + +``` + geometrytype +-------------- + POINTM +``` + ## ST_3DDistance Introduction: Return the 3-dimensional minimum cartesian distance between A and B diff --git a/flink/src/main/java/org/apache/sedona/flink/Catalog.java b/flink/src/main/java/org/apache/sedona/flink/Catalog.java index d9ee14e8c6..fe82f6274c 100644 --- a/flink/src/main/java/org/apache/sedona/flink/Catalog.java +++ b/flink/src/main/java/org/apache/sedona/flink/Catalog.java @@ -36,6 +36,7 @@ public static UserDefinedFunction[] getFuncs() { new Constructors.ST_GeomFromKML(), new Constructors.ST_MPolyFromText(), new Constructors.ST_MLineFromText(), + new Functions.GeometryType(), new Functions.ST_Area(), new Functions.ST_AreaSpheroid(), new Functions.ST_Azimuth(), diff --git a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java index e1a22bb787..c842e11d9b 100644 --- a/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java +++ b/flink/src/main/java/org/apache/sedona/flink/expressions/Functions.java @@ -21,6 +21,14 @@ import org.opengis.referencing.operation.TransformException; public class Functions { + public static class GeometryType extends ScalarFunction { + @DataTypeHint("String") + public String eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) { + Geometry geom = (Geometry) o; + return org.apache.sedona.common.Functions.geometryTypeWithMeasured(geom); + } + } + public static class ST_Area extends ScalarFunction { @DataTypeHint("Double") public Double eval(@DataTypeHint(value = "RAW", bridgedTo = org.locationtech.jts.geom.Geometry.class) Object o) { diff --git a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java index 37c9b30583..da323dc758 100644 --- a/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/FunctionTest.java @@ -153,7 +153,6 @@ public void testTransformWKT() throws FactoryException { } - @Test public void testDimension(){ Table pointTable = tableEnv.sqlQuery( @@ -164,6 +163,7 @@ public void testDimension(){ "SELECT ST_Dimension(ST_GeomFromWKT('GEOMETRYCOLLECTION(MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)), ((2 2, 2 3, 3 3, 3 2, 2 2))), MULTIPOINT(6 6, 7 7, 8 8))'))"); assertEquals(2, first(pointTable).getField(0)); } + @Test public void testDistance() { Table pointTable = createPointTable(testDataSize); @@ -250,6 +250,17 @@ public void testGeomToGeoHash() { assertEquals(first(pointTable).getField(0), "s0000"); } + @Test + public void testGeometryType() { + Table pointTable = tableEnv.sqlQuery( + "SELECT GeometryType(ST_GeomFromText('LINESTRING(77.29 29.07,77.42 29.26,77.27 29.31,77.29 29.07)'))"); + assertEquals("LINESTRING", first(pointTable).getField(0)); + + pointTable = tableEnv.sqlQuery( + "SELECT GeometryType(ST_GeomFromText('POINTM(2.0 3.5 10.2)'))"); + assertEquals("POINTM", first(pointTable).getField(0)); + } + @Test public void testPointOnSurface() { Table pointTable = createPointTable_real(testDataSize); diff --git a/python/sedona/sql/st_functions.py b/python/sedona/sql/st_functions.py index 6ee6c64fee..5cb2eb1cdd 100644 --- a/python/sedona/sql/st_functions.py +++ b/python/sedona/sql/st_functions.py @@ -24,6 +24,7 @@ __all__ = [ + "GeometryType", "ST_3DDistance", "ST_AddPoint", "ST_Area", @@ -120,6 +121,17 @@ _call_st_function = partial(call_sedona_function, "st_functions") +@validate_argument_types +def GeometryType(geometry: ColumnOrName): + """Return the type of the geometry as a string. + This function also indicates if the geometry is measured, by returning a string of the form 'POINTM'. + + :param geometry: Geometry column to calculate the dimension for. + :type geometry: ColumnOrName + :return: Type of geometry as a string column. + :rtype: Column + """ + return _call_st_function("GeometryType", geometry) @validate_argument_types def ST_3DDistance(a: ColumnOrName, b: ColumnOrName) -> Column: diff --git a/python/tests/sql/test_dataframe_api.py b/python/tests/sql/test_dataframe_api.py index d61bc4c12e..ae290b35b7 100644 --- a/python/tests/sql/test_dataframe_api.py +++ b/python/tests/sql/test_dataframe_api.py @@ -49,6 +49,7 @@ (stc.ST_PolygonFromText, ("multiple_point", lambda: f.lit(',')), "constructor", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"), # functions + (stf.GeometryType, ("line",), "linestring_geom", "", "LINESTRING"), (stf.ST_3DDistance, ("a", "b"), "two_points", "", 5.0), (stf.ST_Affine, ("geom", 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), "square_geom", "", "POLYGON ((2 3, 4 5, 5 6, 3 4, 2 3))"), (stf.ST_AddPoint, ("line", lambda: f.expr("ST_Point(1.0, 1.0)")), "linestring_geom", "", "LINESTRING (0 0, 1 0, 2 0, 3 0, 4 0, 5 0, 1 1)"), diff --git a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala index c74513bde7..d8f486c72a 100644 --- a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala +++ b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala @@ -36,6 +36,7 @@ object Catalog { val expressions: Seq[FunctionDescription] = Seq( // Expression for vectors + function[GeometryType](), function[ST_PointFromText](), function[ST_PolygonFromText](), function[ST_LineStringFromText](), diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala index 6ea96af180..166a4c0473 100644 --- a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala @@ -1018,6 +1018,7 @@ case class ST_Dimension(inputExpressions: Seq[Expression]) copy(inputExpressions = newChildren) } } + case class ST_BoundingDiagonal(inputExpressions: Seq[Expression]) extends InferredExpression(Functions.boundingDiagonal _) { protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { @@ -1031,3 +1032,10 @@ case class ST_HausdorffDistance(inputExpressions: Seq[Expression]) copy(inputExpressions = newChildren) } } + +case class GeometryType(inputExpressions: Seq[Expression]) + extends InferredExpression(Functions.geometryTypeWithMeasured _) with FoldableExpression { + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala index 562f362992..128dc079e8 100644 --- a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala @@ -23,6 +23,9 @@ import org.apache.spark.sql.sedona_sql.expressions.collect.{ST_Collect} import org.locationtech.jts.operation.buffer.BufferParameters object st_functions extends DataFrameAPI { + def GeometryType(geometry: Column): Column = wrapExpression[GeometryType](geometry) + def GeometryType(geometry: String): Column = wrapExpression[GeometryType](geometry) + def ST_3DDistance(a: Column, b: Column): Column = wrapExpression[ST_3DDistance](a, b) def ST_3DDistance(a: String, b: String): Column = wrapExpression[ST_3DDistance](a, b) diff --git a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala index 62006f7b3d..d2010a68c5 100644 --- a/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala +++ b/sql/common/src/test/scala/org/apache/sedona/sql/dataFrameAPITestScala.scala @@ -1038,5 +1038,13 @@ class dataFrameAPITestScala extends TestBaseScala { assert(expected == actual) assert(expected == actualDefaultValue) } + + it("Passed GeometryType") { + val polyDf = sparkSession.sql("SELECT ST_GeomFromWKT('POLYGON ((1 2, 2 1, 2 0, 4 1, 1 2))') AS geom") + val df = polyDf.select(GeometryType("geom")) + val expected = "POLYGON" + val actual = df.take(1)(0).get(0).asInstanceOf[String] + assert(expected == actual) + } } } diff --git a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala index 4200b3e62e..e7b9e308c0 100644 --- a/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala +++ b/sql/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala @@ -2062,4 +2062,23 @@ class functionTestScala extends TestBaseScala with Matchers with GeometrySample } } + it ("should pass GeometryType") { + val geomTestCases = Map ( + ("'POINT (51.3168 -0.56)'") -> "'POINT'", + ("'POINT (0 0 1)'") -> "'POINT'", + ("'LINESTRING (0 0, 0 90)'") -> "'LINESTRING'", + ("'POLYGON ((0 0,0 5,5 0,0 0))'") -> "'POLYGON'", + ("'POINTM (1 2 3)'") -> "'POINTM'", + ("'LINESTRINGM (0 0 1, 0 90 1)'") -> "'LINESTRINGM'", + ("'POLYGONM ((0 0 1, 0 5 1, 5 0 1, 0 0 1))'") -> "'POLYGONM'" + ) + for ((geom, expectedResult) <- geomTestCases) { + val df = sparkSession.sql(s"SELECT GeometryType(ST_GeomFromText($geom)), " + + s"$expectedResult") + val actual = df.take(1)(0).get(0).asInstanceOf[String] + val expected = df.take(1)(0).get(1).asInstanceOf[String] + assertEquals(expected, actual) + } + } + }