Skip to content

Commit 4858c50

Browse files
committed
st_orientedenvelope
1 parent c7e42cb commit 4858c50

8 files changed

Lines changed: 129 additions & 0 deletions

File tree

common/src/main/java/org/apache/sedona/common/Functions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.apache.sedona.common.subDivide.GeometrySubDivider;
3737
import org.apache.sedona.common.utils.*;
3838
import org.locationtech.jts.algorithm.Angle;
39+
import org.locationtech.jts.algorithm.MinimumAreaRectangle;
3940
import org.locationtech.jts.algorithm.MinimumBoundingCircle;
4041
import org.locationtech.jts.algorithm.Orientation;
4142
import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
@@ -1227,6 +1228,10 @@ public static Geometry minimumBoundingCircle(Geometry geometry, int quadrantSegm
12271228
return circle;
12281229
}
12291230

1231+
public static Geometry orientedEnvelope(Geometry geometry) {
1232+
return MinimumAreaRectangle.getMinimumRectangle(geometry);
1233+
}
1234+
12301235
public static InscribedCircle maximumInscribedCircle(Geometry geometry) {
12311236
// Calculating the tolerance
12321237
Envelope envelope = geometry.getEnvelopeInternal();

common/src/test/java/org/apache/sedona/common/FunctionsTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,76 @@ public void envelopeAndCentroidSRID() throws ParseException {
756756
assertEquals(3857, centroid.getSRID());
757757
}
758758

759+
@Test
760+
public void orientedEnvelope() throws ParseException {
761+
Geometry axisAlignedRect = Constructors.geomFromWKT("POLYGON ((0 0, 4 0, 4 2, 0 2, 0 0))", 0);
762+
String actual = Functions.asWKT(Functions.orientedEnvelope(axisAlignedRect));
763+
String expected = "POLYGON ((0 0, 0 2, 4 2, 4 0, 0 0))";
764+
assertEquals(expected, actual);
765+
766+
Geometry rotatedSquare = Constructors.geomFromWKT("POLYGON ((1 0, 2 1, 1 2, 0 1, 1 0))", 0);
767+
actual = Functions.asWKT(Functions.orientedEnvelope(rotatedSquare));
768+
expected = "POLYGON ((1 0, 0 1, 1 2, 2 1, 1 0))";
769+
assertEquals(expected, actual);
770+
771+
Geometry diagonalPolygon = Constructors.geomFromWKT("POLYGON ((0 0, 1 0, 5 4, 4 4, 0 0))", 0);
772+
actual = Functions.asWKT(Functions.orientedEnvelope(diagonalPolygon));
773+
expected = "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))";
774+
assertEquals(expected, actual);
775+
776+
Geometry narrowRect = Constructors.geomFromWKT("POLYGON ((0 0, 10 0, 10 1, 0 1, 0 0))", 0);
777+
actual = Functions.asWKT(Functions.orientedEnvelope(narrowRect));
778+
expected = "POLYGON ((0 0, 0 1, 10 1, 10 0, 0 0))";
779+
assertEquals(expected, actual);
780+
781+
Geometry triangle = Constructors.geomFromWKT("POLYGON ((0 0, 4 0, 2 3, 0 0))", 0);
782+
actual = Functions.asWKT(Functions.orientedEnvelope(triangle));
783+
expected = "POLYGON ((4 0, 0 0, 0 3, 4 3, 4 0))";
784+
assertEquals(expected, actual);
785+
786+
Geometry irregularPolygon =
787+
Constructors.geomFromWKT("POLYGON ((0 0, 3 1, 5 0, 4 4, 1 3, 0 0))", 0);
788+
actual =
789+
Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(irregularPolygon), 2));
790+
expected = "POLYGON ((5 0, 0.29 -1.18, -0.71 2.82, 4 4, 5 0))";
791+
assertEquals(expected, actual);
792+
793+
Geometry point = Constructors.geomFromWKT("POINT (1 2)", 0);
794+
actual = Functions.asWKT(Functions.orientedEnvelope(point));
795+
expected = "POINT (1 2)";
796+
assertEquals(expected, actual);
797+
798+
Geometry line = Constructors.geomFromWKT("LINESTRING (0 0, 10 0)", 0);
799+
actual = Functions.asWKT(Functions.orientedEnvelope(line));
800+
expected = "LINESTRING (0 0, 10 0)";
801+
assertEquals(expected, actual);
802+
803+
Geometry diagonalLine = Constructors.geomFromWKT("LINESTRING (0 0, 5 5)", 0);
804+
actual = Functions.asWKT(Functions.orientedEnvelope(diagonalLine));
805+
expected = "LINESTRING (0 0, 5 5)";
806+
assertEquals(expected, actual);
807+
808+
Geometry empty = Constructors.geomFromWKT("POLYGON EMPTY", 0);
809+
Geometry orientedEmpty = Functions.orientedEnvelope(empty);
810+
assertTrue(orientedEmpty.isEmpty());
811+
812+
Geometry geomWithSRID = Constructors.geomFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326);
813+
Geometry orientedWithSRID = Functions.orientedEnvelope(geomWithSRID);
814+
assertEquals(4326, orientedWithSRID.getSRID());
815+
816+
Geometry multiPoint = Constructors.geomFromWKT("MULTIPOINT ((0 0), (-1 -1), (3 2))", 0);
817+
actual = Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(multiPoint), 2));
818+
expected = "POLYGON ((-1 -1, -1.12 -0.84, 2.88 2.16, 3 2, -1 -1))";
819+
assertEquals(expected, actual);
820+
821+
Geometry linestring = Constructors.geomFromWKT("LINESTRING (55 75, 125 150)", 0);
822+
Geometry pointGeom = Constructors.geomFromWKT("POINT (20 80)", 0);
823+
Geometry collection = linestring.union(pointGeom);
824+
actual = Functions.asWKT(Functions.reducePrecision(Functions.orientedEnvelope(collection), 2));
825+
expected = "POLYGON ((125 150, 138.08 130.38, 33.08 60.38, 20 80, 125 150))";
826+
assertEquals(expected, actual);
827+
}
828+
759829
@Test
760830
public void getGoogleS2CellIDsPoint() {
761831
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 2));

python/sedona/spark/sql/st_functions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,18 @@ def ST_NumInteriorRing(geometry: ColumnOrName) -> Column:
15951595
return _call_st_function("ST_NumInteriorRing", geometry)
15961596

15971597

1598+
@validate_argument_types
1599+
def ST_OrientedEnvelope(geometry: ColumnOrName) -> Column:
1600+
"""Return the minimum rotated rectangle enclosing a geometry.
1601+
1602+
:param geometry: Geometry column to compute oriented envelope for.
1603+
:type geometry: ColumnOrName
1604+
:return: Minimum area rotated rectangle as a geometry column.
1605+
:rtype: Column
1606+
"""
1607+
return _call_st_function("ST_OrientedEnvelope", geometry)
1608+
1609+
15981610
@validate_argument_types
15991611
def ST_PointN(geometry: ColumnOrName, n: Union[ColumnOrName, int]) -> Column:
16001612
"""Get the n-th point (starts at 1) for a geometry.

python/tests/sql/test_function.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,3 +2529,14 @@ def test_st_approximate_medial_axis(self):
25292529
actual_with_max = actual_df_with_max.take(1)[0][0]
25302530
assert actual_with_max is not None
25312531
assert actual_with_max.geom_type == "MultiLineString"
2532+
2533+
def test_st_oriented_envelope(self):
2534+
actual = self.spark.sql(
2535+
"SELECT ST_AsText(ST_OrientedEnvelope(ST_GeomFromText('POLYGON ((0 0, 1 0, 5 4, 4 4, 0 0))')))"
2536+
).take(1)[0][0]
2537+
assert actual == "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))"
2538+
2539+
actual = self.spark.sql(
2540+
"SELECT ST_AsText(ST_OrientedEnvelope(ST_GeomFromText('POINT (1 2)')))"
2541+
).take(1)[0][0]
2542+
assert actual == "POINT (1 2)"

spark/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ object Catalog extends AbstractCatalog with Logging {
208208
function[ST_XMin](),
209209
function[ST_BuildArea](),
210210
function[ST_OrderingEquals](),
211+
function[ST_OrientedEnvelope](),
211212
function[ST_CollectionExtract](defaultArgs = null),
212213
function[ST_Normalize](),
213214
function[ST_LineFromMultiPoint](),

spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/Functions.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,14 @@ private[apache] case class ST_MinimumBoundingCircle(inputExpressions: Seq[Expres
701701
}
702702
}
703703

704+
private[apache] case class ST_OrientedEnvelope(inputExpressions: Seq[Expression])
705+
extends InferredExpression(Functions.orientedEnvelope _) {
706+
707+
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
708+
copy(inputExpressions = newChildren)
709+
}
710+
}
711+
704712
private[apache] case class ST_HasZ(inputExpressions: Seq[Expression])
705713
extends InferredExpression(Functions.hasZ _) {
706714

spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/st_functions.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@ object st_functions {
504504
def ST_MinimumBoundingRadius(geometry: String): Column =
505505
wrapExpression[ST_MinimumBoundingRadius](geometry)
506506

507+
def ST_OrientedEnvelope(geometry: Column): Column =
508+
wrapExpression[ST_OrientedEnvelope](geometry)
509+
def ST_OrientedEnvelope(geometry: String): Column =
510+
wrapExpression[ST_OrientedEnvelope](geometry)
511+
507512
def ST_IsPolygonCCW(geometry: Column): Column = wrapExpression[ST_IsPolygonCCW](geometry)
508513
def ST_IsPolygonCCW(geometry: String): Column = wrapExpression[ST_IsPolygonCCW](geometry)
509514

spark/common/src/test/scala/org/apache/sedona/sql/functionTestScala.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2373,6 +2373,21 @@ class functionTestScala
23732373
.toList should contain theSameElementsAs List(0, 1, 1)
23742374
}
23752375

2376+
it("Should pass ST_OrientedEnvelope") {
2377+
val testCases = Seq(
2378+
("POLYGON ((0 0, 4 0, 4 2, 0 2, 0 0))", "POLYGON ((0 0, 0 2, 4 2, 4 0, 0 0))"),
2379+
("POLYGON ((0 0, 1 0, 5 4, 4 4, 0 0))", "POLYGON ((0 0, 4.5 4.5, 5 4, 0.5 -0.5, 0 0))"),
2380+
("POINT (1 2)", "POINT (1 2)"))
2381+
2382+
testCases.foreach { case (input, expected) =>
2383+
val actual = sparkSession
2384+
.sql(s"SELECT ST_AsText(ST_OrientedEnvelope(ST_GeomFromWKT('$input')))")
2385+
.first()
2386+
.getString(0)
2387+
assert(expected.equals(actual), s"Input: $input, Expected: $expected, Actual: $actual")
2388+
}
2389+
}
2390+
23762391
it("Should pass ST_LineSegments") {
23772392
val baseDf = sparkSession.sql(
23782393
"SELECT ST_GeomFromWKT('LINESTRING(120 140, 60 120, 30 20)') AS line, ST_GeomFromWKT('POLYGON ((0 0, 0 1, 1 0, 0 0))') AS poly")
@@ -2733,6 +2748,8 @@ class functionTestScala
27332748
assert(functionDf.first().get(0) == null)
27342749
functionDf = sparkSession.sql("select ST_MinimumBoundingRadius(null)")
27352750
assert(functionDf.first().get(0) == null)
2751+
functionDf = sparkSession.sql("select ST_OrientedEnvelope(null)")
2752+
assert(functionDf.first().get(0) == null)
27362753
functionDf = sparkSession.sql("select ST_LineSubstring(null, 0, 0)")
27372754
assert(functionDf.first().get(0) == null)
27382755
functionDf = sparkSession.sql("select ST_LineInterpolatePoint(null, 0)")

0 commit comments

Comments
 (0)