Skip to content

Commit

Permalink
[SEDONA-615] Add ST_MaximumInscribedCircle (#1488)
Browse files Browse the repository at this point in the history
* feat: add ST_MaximumInscribedCircle

* fix: spotless errors

* fix: spotless errors
  • Loading branch information
furqaankhan committed Jun 24, 2024
1 parent 0a1db3d commit dc43945
Show file tree
Hide file tree
Showing 16 changed files with 367 additions and 5 deletions.
29 changes: 29 additions & 0 deletions common/src/main/java/org/apache/sedona/common/Functions.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.apache.sedona.common.utils.*;
import org.locationtech.jts.algorithm.MinimumBoundingCircle;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.algorithm.construct.LargestEmptyCircle;
import org.locationtech.jts.algorithm.construct.MaximumInscribedCircle;
import org.locationtech.jts.algorithm.hull.ConcaveHull;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.impl.CoordinateArraySequence;
Expand Down Expand Up @@ -941,6 +943,33 @@ public static Geometry minimumBoundingCircle(Geometry geometry, int quadrantSegm
return circle;
}

public static InscribedCircle maximumInscribedCircle(Geometry geometry) {
// Calculating the tolerance
Envelope envelope = geometry.getEnvelopeInternal();
double width = envelope.getWidth(), height = envelope.getHeight(), size, tolerance;
size = Math.max(width, height);
tolerance = size / 1000.0;

Geometry center, nearest;
double radius;

// All non-polygonal geometries use LargestEmptyCircle
if (!geometry.getClass().getSimpleName().equals("Polygon")
&& !geometry.getClass().getSimpleName().equals("MultiPolygon")) {
LargestEmptyCircle largestEmptyCircle = new LargestEmptyCircle(geometry, tolerance);
center = largestEmptyCircle.getCenter();
nearest = largestEmptyCircle.getRadiusPoint();
radius = largestEmptyCircle.getRadiusLine().getLength();
return new InscribedCircle(center, nearest, radius);
}

MaximumInscribedCircle maximumInscribedCircle = new MaximumInscribedCircle(geometry, tolerance);
center = maximumInscribedCircle.getCenter();
nearest = maximumInscribedCircle.getRadiusPoint();
radius = maximumInscribedCircle.getRadiusLine().getLength();
return new InscribedCircle(center, nearest, radius);
}

public static Pair<Geometry, Double> minimumBoundingRadius(Geometry geometry) {
MinimumBoundingCircle minimumBoundingCircle = new MinimumBoundingCircle(geometry);
Coordinate coods = minimumBoundingCircle.getCentre();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sedona.common.utils;

import org.apache.sedona.common.Functions;
import org.locationtech.jts.geom.Geometry;

public class InscribedCircle {
public final Geometry center;
public final Geometry nearest;
public final double radius;

public InscribedCircle(Geometry center, Geometry nearest, double radius) {
this.center = center;
this.nearest = nearest;
this.radius = radius;
}

@Override
public String toString() {
return String.format(
"---------------\ncenter: %s\nnearest: %s\nradius: %.20f\n---------------\n",
Functions.asWKT(center), Functions.asWKT(nearest), radius);
}

public boolean equals(InscribedCircle other) {
double epsilon = 1e-6;
return this.center.equals(other.center)
&& this.nearest.equals(other.nearest)
&& Math.abs(this.radius - other.radius) < epsilon;
}
}
61 changes: 57 additions & 4 deletions common/src/test/java/org/apache/sedona/common/FunctionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@
import java.util.stream.Collectors;
import org.apache.sedona.common.sphere.Haversine;
import org.apache.sedona.common.sphere.Spheroid;
import org.apache.sedona.common.utils.GeomUtils;
import org.apache.sedona.common.utils.H3Utils;
import org.apache.sedona.common.utils.S2Utils;
import org.apache.sedona.common.utils.ValidDetail;
import org.apache.sedona.common.utils.*;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.projection.ProjectionException;
import org.junit.Test;
Expand Down Expand Up @@ -3459,6 +3456,62 @@ public void locateAlong() throws ParseException {
e.getMessage());
}

@Test
public void maximumInscribedCircle() throws ParseException {
Geometry geom =
Constructors.geomFromEWKT(
"POLYGON ((40 180, 110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 50 90, 90 140, 60 140))");
InscribedCircle actual = Functions.maximumInscribedCircle(geom);
InscribedCircle expected =
new InscribedCircle(
Constructors.geomFromEWKT("POINT (96.953125 76.328125)"),
Constructors.geomFromEWKT("POINT (140 90)"),
45.165846);
assertTrue(expected.equals(actual));

geom =
Constructors.geomFromEWKT(
"MULTILINESTRING ((59.5 17, 65 17), (60 16.5, 66 12), (65 12, 69 17))");
actual = Functions.maximumInscribedCircle(geom);
expected =
new InscribedCircle(
Constructors.geomFromEWKT("POINT (65.0419921875 15.1005859375)"),
Constructors.geomFromEWKT("POINT (65 17)"),
1.8998781);
assertTrue(expected.equals(actual));

geom =
Constructors.geomFromEWKT(
"MULTIPOINT ((60.8 15.5), (63.2 16.3), (63 14), (67.4 14.8), (66.3 18.4), (65. 13.), (67.5 16.9), (64.2 18))");
actual = Functions.maximumInscribedCircle(geom);
expected =
new InscribedCircle(
Constructors.geomFromEWKT("POINT (65.44062499999998 15.953124999999998)"),
Constructors.geomFromEWKT("POINT (67.5 16.9)"),
2.2666269);
assertTrue(expected.equals(actual));

geom = Constructors.geomFromEWKT("MULTIPOINT ((60.8 15.5), (63.2 16.3))");
actual = Functions.maximumInscribedCircle(geom);
expected =
new InscribedCircle(
Constructors.geomFromEWKT("POINT (60.8 15.5)"),
Constructors.geomFromEWKT("POINT (60.8 15.5)"),
0.0);
assertTrue(expected.equals(actual));

geom =
Constructors.geomFromEWKT(
"GEOMETRYCOLLECTION (POINT (60.8 15.5), POINT (63.2 16.3), POINT (63 14), POINT (67.4 14.8), POINT (66.3 18.4), POINT (65 13), POINT (67.5 16.9), POINT (64.2 18))");
actual = Functions.maximumInscribedCircle(geom);
expected =
new InscribedCircle(
Constructors.geomFromEWKT("POINT (65.44062499999998 15.953124999999998)"),
Constructors.geomFromEWKT("POINT (67.5 16.9)"),
2.2666269);
assertTrue(expected.equals(actual));
}

@Test
public void test() throws ParseException {
Geometry geom = Constructors.geomFromEWKT("MULTILINESTRING M ((0 0 1,0 1 2), (0 0 1,0 1 2))");
Expand Down
32 changes: 32 additions & 0 deletions docs/api/snowflake/vector-data/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -1831,6 +1831,38 @@ Result:
The previous implementation only worked for (multi)polygons and had a different interpretation of the second, boolean, argument.
It would also sometimes return multiple geometries for a single geometry input.

## ST_MaximumInscribedCircle

Introduction: Finds the largest circle that is contained within a (multi)polygon, or which does not overlap any lines and points. Returns a row with fields:

- `center` - center point of the circle
- `nearest` - nearest point from the center of the circle
- `radius` - radius of the circle

For polygonal geometries, the function inscribes the circle within the boundary rings, treating internal rings as additional constraints. When processing linear and point inputs, the algorithm inscribes the circle within the convex hull of the input, utilizing the input lines and points as additional boundary constraints.

Format: `ST_MaximumInscribedCircle(geometry: Geometry)`

Since: `v1.6.1`

SQL Example:

```sql
SELECT Sedona.ST_AsText(center) AS center, Sedona.ST_AsText(nearest) AS nearest, radius FROM table(
SELECT ST_MaximumIncribedCircle(ST_GeomFromWKT('POLYGON ((62.11 19.68, 60.79 17.20, 61.30 15.96, 62.11 16.08, 65.93 16.95, 66.20 20.61, 63.08 21.43, 64.48 18.70, 62.11 19.68))'))
)
```

Output:

```
+---------------------------------------------+-------------------------------------------+------------------+
|center |nearest |radius |
+---------------------------------------------+-------------------------------------------+------------------+
|POINT (62.794975585937514 17.774780273437496)|POINT (63.36773534817729 19.15992378007859)|1.4988916836219184|
+---------------------------------------------+-------------------------------------------+------------------+
```

## ST_MaxDistance

Introduction: Calculates and returns the length value representing the maximum distance between any two points across the input geometries. This function is an alias for `ST_LongestDistance`.
Expand Down
32 changes: 32 additions & 0 deletions docs/api/sql/Function.md
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,38 @@ Result:
Be sure to check you code when upgrading. The previous implementation only worked for (multi)polygons and had a different interpretation of the second, boolean, argument.
It would also sometimes return multiple geometries for a single geometry input.

## ST_MaximumInscribedCircle

Introduction: Finds the largest circle that is contained within a (multi)polygon, or which does not overlap any lines and points. Returns a row with fields:

- `center` - center point of the circle
- `nearest` - nearest point from the center of the circle
- `radius` - radius of the circle

For polygonal geometries, the function inscribes the circle within the boundary rings, treating internal rings as additional constraints. When processing linear and point inputs, the algorithm inscribes the circle within the convex hull of the input, utilizing the input lines and points as additional boundary constraints.

Format: `ST_MaximumInscribedCircle(geometry: Geometry)`

Since: `v1.6.1`

SQL Example:

```sql
SELECT inscribedCircle.* FROM (
SELECT ST_MaximumIncribedCircle(ST_GeomFromWKT('POLYGON ((62.11 19.68, 60.79 17.20, 61.30 15.96, 62.11 16.08, 65.93 16.95, 66.20 20.61, 63.08 21.43, 64.48 18.70, 62.11 19.68))')) AS inscribedCircle
)
```

Output:

```
+---------------------------------------------+-------------------------------------------+------------------+
|center |nearest |radius |
+---------------------------------------------+-------------------------------------------+------------------+
|POINT (62.794975585937514 17.774780273437496)|POINT (63.36773534817729 19.15992378007859)|1.4988916836219184|
+---------------------------------------------+-------------------------------------------+------------------+
```

## ST_MaxDistance

Introduction: Calculates and returns the length value representing the maximum distance between any two points across the input geometries. This function is an alias for `ST_LongestDistance`.
Expand Down
12 changes: 12 additions & 0 deletions python/sedona/sql/st_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,18 @@ def ST_MakeValid(geometry: ColumnOrName, keep_collapsed: Optional[Union[ColumnOr
args = (geometry,) if keep_collapsed is None else (geometry, keep_collapsed)
return _call_st_function("ST_MakeValid", args)


@validate_argument_types
def ST_MaximumInscribedCircle(geometry: ColumnOrName) -> Column:
"""Finds the largest circle that is contained within a geometry, or which does not overlap any lines and points
:param geometry:
:type geometry: ColumnOrName
:return: Row of center point, nearest point and radius
:rtype: Column
"""
return _call_st_function("ST_MaximumInscribedCircle", geometry)

@validate_argument_types
def ST_MaxDistance(geom1: ColumnOrName, geom2: ColumnOrName) -> Column:
"""Calculate the maximum distance between two furthest points in the geometries
Expand Down
4 changes: 4 additions & 0 deletions python/tests/sql/test_dataframe_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@
(stf.ST_MMax, ("line",), "4D_line", "", 3.0),
(stf.ST_MakeValid, ("geom",), "invalid_geom", "", "MULTIPOLYGON (((1 5, 3 3, 1 1, 1 5)), ((5 3, 7 5, 7 1, 5 3)))"),
(stf.ST_MakeLine, ("line1", "line2"), "two_lines", "", "LINESTRING (0 0, 1 1, 0 0, 3 2)"),
(stf.ST_MaximumInscribedCircle, ("geom",), "triangle_geom", "ST_AsText(geom.center)", "POINT (0.70703125 0.29296875)"),
(stf.ST_MaximumInscribedCircle, ("geom",), "triangle_geom", "ST_AsText(geom.nearest)", "POINT (0.5 0.5)"),
(stf.ST_MaximumInscribedCircle, ("geom",), "triangle_geom", "geom.radius", 0.2927864015850548),
(stf.ST_MaxDistance, ("a", "b"), "overlapping_polys", "", 3.1622776601683795),
(stf.ST_Points, ("line",), "linestring_geom", "ST_Normalize(geom)", "MULTIPOINT (0 0, 1 0, 2 0, 3 0, 4 0, 5 0)"),
(stf.ST_Polygon, ("geom", 4236), "closed_linestring_geom", "", "POLYGON ((0 0, 1 0, 1 1, 0 0))"),
Expand Down Expand Up @@ -385,6 +388,7 @@
(stf.ST_MMax, (None,)),
(stf.ST_MakeValid, (None,)),
(stf.ST_MakePolygon, (None,)),
(stf.ST_MaximumInscribedCircle, (None,)),
(stf.ST_MaxDistance, (None, None)),
(stf.ST_MaxDistance, (None, "")),
(stf.ST_MaxDistance, ("", None)),
Expand Down
10 changes: 10 additions & 0 deletions python/tests/sql/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,16 @@ def test_st_intersection_not_intersects(self):
intersects = self.spark.sql("select ST_Intersection(a,b) from testtable")
assert intersects.take(1)[0][0].wkt == "POLYGON EMPTY"

def test_st_maximum_inscribed_circle(self):
baseDf = self.spark.sql("SELECT ST_GeomFromWKT('POLYGON ((40 180, 110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 50 90, 90 140, 60 140))') AS geom")
actual = baseDf.selectExpr("ST_MaximumInscribedCircle(geom)").take(1)[0][0]
center = actual.center.wkt
assert center == "POINT (96.953125 76.328125)"
nearest = actual.nearest.wkt
assert nearest == "POINT (140 90)"
radius = actual.radius
assert radius == 45.165845650018

def test_st_is_valid_detail(self):
baseDf = self.spark.sql("SELECT ST_GeomFromText('POLYGON ((0 0, 2 0, 2 2, 0 2, 1 1, 0 0))') AS geom")
actual = baseDf.selectExpr("ST_IsValidDetail(geom)").first()[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ public void test_ST_Intersection_Aggr() throws ParseException {
Constructors.geomFromWKT("POLYGON ((0.5 1, 1 1, 1 0.5, 0.5 0.5, 0.5 1))", 0));
}

@Test
public void test_ST_MaximumInscribedCircle() {
registerUDTF(ST_MaximumInscribedCircle.class);
verifySqlSingleRes(
"select sedona.ST_AsText(center) from table(sedona.ST_MaximumInscribedCircle(sedona.ST_GeomFromText('POLYGON ((40 180, 110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 50 90, 90 140, 60 140))')))",
"POINT (96.953125 76.328125)");
verifySqlSingleRes(
"select sedona.ST_AsText(nearest) from table(sedona.ST_MaximumInscribedCircle(sedona.ST_GeomFromText('POLYGON ((40 180, 110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 50 90, 90 140, 60 140))')))",
"POINT (140 90)");
verifySqlSingleRes(
"select radius from table(sedona.ST_MaximumInscribedCircle(sedona.ST_GeomFromText('POLYGON ((40 180, 110 160, 180 180, 180 120, 140 90, 160 40, 80 10, 70 40, 20 50, 40 180),(60 140, 50 90, 90 140, 60 140))')))",
45.165845650018);
}

@Test
public void test_ST_IsValidDetail() {
registerUDTF(ST_IsValidDetail.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

public class UDTFDDLGenerator {
public static final Class[] udtfClz = {
ST_MaximumInscribedCircle.class,
ST_MinimumBoundingRadius.class,
ST_Intersection_Aggr.class,
ST_SubDivideExplode.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.sedona.snowflake.snowsql.udtfs;

import java.util.stream.Stream;
import org.apache.sedona.common.Functions;
import org.apache.sedona.common.utils.InscribedCircle;
import org.apache.sedona.snowflake.snowsql.GeometrySerde;
import org.apache.sedona.snowflake.snowsql.annotations.UDTFAnnotations;
import org.locationtech.jts.io.ParseException;

@UDTFAnnotations.TabularFunc(
name = "ST_MaximumInscribedCircle",
argNames = {"geometry"})
public class ST_MaximumInscribedCircle {

public static class OutputRow {
public final byte[] center;
public final byte[] nearest;
public final double radius;

public OutputRow(InscribedCircle inscribedCircle) {
this.center = GeometrySerde.serialize(inscribedCircle.center);
this.nearest = GeometrySerde.serialize(inscribedCircle.nearest);
this.radius = inscribedCircle.radius;
}
}

public static Class getOutputClass() {
return OutputRow.class;
}

public ST_MaximumInscribedCircle() {}

public Stream<OutputRow> process(byte[] geometry) throws ParseException {
InscribedCircle inscribedCircle =
Functions.maximumInscribedCircle(GeometrySerde.deserialize(geometry));

return Stream.of(new OutputRow(inscribedCircle));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ object Catalog {
function[ST_Polygon](),
function[ST_Polygonize](),
function[ST_MakePolygon](null),
function[ST_MaximumInscribedCircle](),
function[ST_MaxDistance](),
function[ST_GeoHash](),
function[ST_GeomFromGeoHash](null),
Expand Down
Loading

0 comments on commit dc43945

Please sign in to comment.