diff --git a/common/src/main/java/org/apache/sedona/common/geography/Functions.java b/common/src/main/java/org/apache/sedona/common/geography/Functions.java index fca6180fefa..df7d47519e0 100644 --- a/common/src/main/java/org/apache/sedona/common/geography/Functions.java +++ b/common/src/main/java/org/apache/sedona/common/geography/Functions.java @@ -214,7 +214,8 @@ public static Double y(Geography g) { * measurement functions. No CRS transformation or SRID compatibility check is performed; if the * inputs have different SRIDs, the first input's SRID is used. When the second input is a * LineString whose first coordinate equals the current endpoint, that seam coordinate is added - * only once. Point and MultiPoint coordinates are always retained. + * only once. Point and MultiPoint coordinates are always retained. Empty inputs contribute no + * coordinates; when exactly one coordinate remains, it is repeated to form a valid LineString. */ public static Geography makeLine(Geography g1, Geography g2) { if (g1 == null || g2 == null) return null; @@ -251,6 +252,11 @@ private static Geometry makeLineGeometry(Geometry first, Geometry second) { coordinates.add(appended[i]); } + // PostGIS and SedonaDB skip empty components. JTS cannot represent their one-coordinate + // LineString result, so repeat that coordinate while preserving the same point set. + if (coordinates.size() == 1) { + coordinates.add(new Coordinate(coordinates.get(0))); + } return first.getFactory().createLineString(coordinates.toArray(new Coordinate[0])); } diff --git a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java index 771fb3b2d42..1f836a7fb0d 100644 --- a/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java +++ b/common/src/test/java/org/apache/sedona/common/Geography/FunctionTest.java @@ -235,6 +235,33 @@ public void makeLine_coincidentPointsRemainUsableAfterWKBRoundTrip() throws Pars assertEquals(3, Functions.nPoints(extended)); } + @Test + public void makeLine_skipsEmptyInputsAndNormalizesSingleCoordinate() throws ParseException { + Geography emptyPoint = Constructors.geogFromWKT("POINT EMPTY", 3857); + Geography emptyLine = Constructors.geogFromWKT("LINESTRING EMPTY", 4326); + Geography emptyMultiPoint = Constructors.geogFromWKT("MULTIPOINT EMPTY", 4326); + Geography point = Constructors.geogFromWKT("POINT (12 34)", 4326); + + Geography onlySecond = roundTripWKB(Functions.makeLine(emptyPoint, point)); + assertEquals("LINESTRING (12 34, 12 34)", Functions.asText(onlySecond)); + assertEquals(2, Functions.nPoints(onlySecond)); + assertEquals(3857, onlySecond.getSRID()); + + Geography onlyFirst = roundTripWKB(Functions.makeLine(point, emptyLine)); + assertEquals("LINESTRING (12 34, 12 34)", Functions.asText(onlyFirst)); + assertEquals(2, Functions.nPoints(onlyFirst)); + assertEquals(4326, onlyFirst.getSRID()); + + Geography afterEmptyMultiPoint = roundTripWKB(Functions.makeLine(emptyMultiPoint, point)); + assertEquals("LINESTRING (12 34, 12 34)", Functions.asText(afterEmptyMultiPoint)); + assertEquals(2, Functions.nPoints(afterEmptyMultiPoint)); + + Geography noCoordinates = roundTripWKB(Functions.makeLine(emptyPoint, emptyLine)); + assertEquals("LINESTRING EMPTY", Functions.asText(noCoordinates)); + assertEquals(0, Functions.nPoints(noCoordinates)); + assertEquals(3857, noCoordinates.getSRID()); + } + @Test public void makeLine_deduplicatesLineStringSeamAndPreservesOtherRepeats() throws ParseException { Geography first = Constructors.geogFromWKT("LINESTRING (0 0, 1 0)", 4326); diff --git a/docs/api/flink/Geography-Functions/ST_MakeLine.md b/docs/api/flink/Geography-Functions/ST_MakeLine.md index 5b46b8adae2..654a99e7c57 100644 --- a/docs/api/flink/Geography-Functions/ST_MakeLine.md +++ b/docs/api/flink/Geography-Functions/ST_MakeLine.md @@ -29,7 +29,7 @@ Return type: `Geography` Since: `v1.9.1` -Only the two-argument signature accepts Geography inputs. The array form of `ST_MakeLine` remains Geometry-only. Repeated and coincident Point or MultiPoint coordinates are preserved. When the second input is a LineString whose first coordinate matches the current endpoint, that seam coordinate appears only once. +Only the two-argument signature accepts Geography inputs. The array form of `ST_MakeLine` remains Geometry-only. Repeated and coincident Point or MultiPoint coordinates are preserved. When the second input is a LineString whose first coordinate matches the current endpoint, that seam coordinate appears only once. Empty inputs contribute no coordinates. If exactly one coordinate remains, it is repeated to produce a valid LineString. The output copies the first input's SRID without transforming either input or validating that their SRIDs match. If the inputs have different SRIDs, the second input's SRID is ignored. An SRID of `0` on the first input remains `0`. diff --git a/docs/api/sql/geography/Geography-Functions/ST_MakeLine.md b/docs/api/sql/geography/Geography-Functions/ST_MakeLine.md index e6213cef7ee..24d315bd72b 100644 --- a/docs/api/sql/geography/Geography-Functions/ST_MakeLine.md +++ b/docs/api/sql/geography/Geography-Functions/ST_MakeLine.md @@ -29,7 +29,7 @@ Return type: `Geography` Since: `v1.9.1` -Only the two-argument signature accepts Geography inputs. The array/aggregate form of `ST_MakeLine` remains Geometry-only. Repeated and coincident Point or MultiPoint coordinates are preserved. When the second input is a LineString whose first coordinate matches the current endpoint, that seam coordinate appears only once. +Only the two-argument signature accepts Geography inputs. The array/aggregate form of `ST_MakeLine` remains Geometry-only. Repeated and coincident Point or MultiPoint coordinates are preserved. When the second input is a LineString whose first coordinate matches the current endpoint, that seam coordinate appears only once. Empty inputs contribute no coordinates. If exactly one coordinate remains, it is repeated to produce a valid LineString. The output copies the first input's SRID without transforming either input or validating that their SRIDs match. If the inputs have different SRIDs, the second input's SRID is ignored. An SRID of `0` on the first input remains `0`. diff --git a/flink/src/test/java/org/apache/sedona/flink/GeographyFunctionTest.java b/flink/src/test/java/org/apache/sedona/flink/GeographyFunctionTest.java index 4f85ff938ef..e8fe29ee3ce 100644 --- a/flink/src/test/java/org/apache/sedona/flink/GeographyFunctionTest.java +++ b/flink/src/test/java/org/apache/sedona/flink/GeographyFunctionTest.java @@ -137,6 +137,27 @@ public void testMakeLinePreservesCoincidentPointsAndFirstSRID() throws Exception } } + @Test + public void testMakeLineSkipsEmptyGeographyInputs() throws Exception { + Table table = + tableEnv.sqlQuery( + "SELECT " + + "ST_AsText(ST_MakeLine(" + + "ST_GeogFromWKT('POINT EMPTY', 3857), " + + "ST_GeogFromWKT('POINT (12 34)', 4326))) AS empty_first, " + + "ST_AsText(ST_MakeLine(" + + "ST_GeogFromWKT('POINT (12 34)', 4326), " + + "ST_GeogFromWKT('LINESTRING EMPTY', 3857))) AS empty_second, " + + "ST_AsEWKT(ST_MakeLine(" + + "ST_GeogFromWKT('POINT EMPTY', 3857), " + + "ST_GeogFromWKT('LINESTRING EMPTY', 4326))) AS both_empty"); + + Row row = first(table); + assertEquals("LINESTRING (12 34, 12 34)", row.getFieldAs("empty_first")); + assertEquals("LINESTRING (12 34, 12 34)", row.getFieldAs("empty_second")); + assertEquals("SRID=3857; LINESTRING EMPTY", row.getFieldAs("both_empty")); + } + @Test public void testDistance() throws Exception { String wktA = "POINT (0 0)"; diff --git a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala index 5adb7ed63f8..bfbc3601848 100644 --- a/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala +++ b/spark/common/src/test/scala/org/apache/sedona/sql/geography/GeographyFunctionTest.scala @@ -259,6 +259,30 @@ class GeographyFunctionTest extends TestBaseScala { assertEquals("SRID=4326; LINESTRING (0 0, 1 0, 2 0)", repeated.getString(1)) assertEquals(3, repeated.getInt(2)) } + + it("ST_MakeLine skips empty geography inputs") { + val row = sparkSession + .sql(""" + SELECT + ST_AsText(ST_MakeLine( + ST_GeogFromWKT('POINT EMPTY', 3857), + ST_GeogFromWKT('POINT (12 34)', 4326) + )) AS empty_first, + ST_AsText(ST_MakeLine( + ST_GeogFromWKT('POINT (12 34)', 4326), + ST_GeogFromWKT('LINESTRING EMPTY', 3857) + )) AS empty_second, + ST_AsEWKT(ST_MakeLine( + ST_GeogFromWKT('POINT EMPTY', 3857), + ST_GeogFromWKT('LINESTRING EMPTY', 4326) + )) AS both_empty + """) + .first() + + assertEquals("LINESTRING (12 34, 12 34)", row.getString(0)) + assertEquals("LINESTRING (12 34, 12 34)", row.getString(1)) + assertEquals("SRID=3857; LINESTRING EMPTY", row.getString(2)) + } } // ─── Level 2: ST_Length, ST_Area, ST_Distance ──────────────────────────