Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,39 @@ public static String asText(Geography g) {
return toJTS(g).toText();
}

// ─── Level 2: JTS + S2 geodesic metrics ──────────────────────────────────
// ─── Level 2: Geodesic metrics ───────────────────────────────────────────

/**
* Spherical length in meters of a geography, calculated on the sphere. Edges are interpreted as
* great-circle arcs; the summed arc-angle is scaled by {@link Haversine#AVG_EARTH_RADIUS}.
* Multi-polylines sum the children's lengths; geography collections recurse. Returns {@code 0.0}
* for point/polygon geographies and for {@code null}.
*/
public static double length(Geography g) {
if (g == null) return 0.0;
Geography typed = (g instanceof WKBGeography) ? ((WKBGeography) g).getS2Geography() : g;
double radians = sphericalLength(typed);
return radians * Haversine.AVG_EARTH_RADIUS;
}

/** Arc-angle (radians) of {@code g} on the unit sphere; 0 for non-linear kinds. */
private static double sphericalLength(Geography g) {
if (g instanceof PolylineGeography) {
double sum = 0.0;
for (S2Polyline pl : ((PolylineGeography) g).getPolylines()) {
sum += pl.getArclengthAngle().radians();
}
return sum;
}
if (g instanceof GeographyCollection) {
double sum = 0.0;
for (Geography feature : ((GeographyCollection) g).getFeatures()) {
sum += sphericalLength(feature);
}
return sum;
}
return 0.0;
}

/**
* Spherical area in square meters of a geography, calculated on the sphere. The Earth is modeled
Expand Down Expand Up @@ -233,8 +265,7 @@ private static double sphericalArea(Geography g) {

/**
* Geometry-to-geometry geodesic distance in meters. Uses S2ClosestEdgeQuery for true minimum
* distance between any two points on the geometries (not centroid-to-centroid). Consistent with
* sedona-db's s2_distance implementation.
* distance between any two points on the geometries (not centroid-to-centroid).
*/
public static Double distance(Geography g1, Geography g2) {
if (g1 == null || g2 == null) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,48 @@ public void asText_nullHandling() {
assertNull(Functions.asText(null));
}

// ─── Level 2: ST_Area, ST_Distance ───────────────────────────────────────
// ─── Level 2: ST_Length, ST_Area, ST_Distance ────────────────────────────

@Test
public void length_equatorDegree() throws ParseException {
Geography g = Constructors.geogFromWKT("LINESTRING (0 0, 1 0)", 4326);
double len = Functions.length(g);
// Sphere of radius 6371008 m: 1° along a great circle is ~111,195 m.
assertEquals(111195.10, len, 1.0);
}

@Test
public void length_meridianDegree() throws ParseException {
Geography g = Constructors.geogFromWKT("LINESTRING (0 0, 0 1)", 4326);
double len = Functions.length(g);
// Meridians are great circles on a sphere — same length as the equator degree.
assertEquals(111195.10, len, 1.0);
}

@Test
public void length_point_returnsZero() throws ParseException {
Geography g = Constructors.geogFromWKT("POINT (1 2)", 4326);
assertEquals(0.0, Functions.length(g), 0.0);
}

@Test
public void length_polygon_returnsZero() throws ParseException {
Geography g = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326);
assertEquals(0.0, Functions.length(g), 0.0);
}

@Test
public void length_multilinestring_sumsChildren() throws ParseException {
Geography g = Constructors.geogFromWKT("MULTILINESTRING ((0 0, 1 0), (5 0, 6 0))", 4326);
double len = Functions.length(g);
// Two disjoint 1° equatorial arcs → 2 * (R * 1° in radians) ≈ 222,390 m.
assertEquals(2 * 111195.10, len, 2.0);
}

@Test
public void length_nullHandling() {
assertEquals(0.0, Functions.length(null), 0.0);
}

@Test
public void area_unitBoxAtEquator() throws ParseException {
Expand Down
1 change: 1 addition & 0 deletions docs/api/sql/geography/Geography-Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ These functions operate on geography type objects.
| [ST_NPoints](Geography-Functions/ST_NPoints.md) | Integer | Return the number of points (vertices) in a geography. | v1.9.0 |
| [ST_NumGeometries](Geography-Functions/ST_NumGeometries.md) | Integer | Return the number of sub-geometries in a geography (1 for single geometries). | v1.9.1 |
| [ST_Distance](Geography-Functions/ST_Distance.md) | Double | Return the minimum geodesic distance between two geographies in meters. | v1.9.0 |
| [ST_Length](Geography-Functions/ST_Length.md) | Double | Return the spherical length of a geography in meters, summed along great-circle edges. | v1.9.1 |
| [ST_Contains](Geography-Functions/ST_Contains.md) | Boolean | Test whether geography A fully contains geography B. | v1.9.0 |
50 changes: 50 additions & 0 deletions docs/api/sql/geography/Geography-Functions/ST_Length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
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.
-->

# ST_Length

Introduction: Returns the spherical length of a geography in meters, calculated on the sphere. The Earth is modeled as a sphere of radius `R = 6 371 008 m` (the authalic Earth radius). Each edge between successive vertices is interpreted as a great-circle arc; the per-edge arc-angles are summed and scaled by `R`.

Multi-linestrings sum the children's lengths; geography collections recurse and add up the lengths of their linear members. Returns `0.0` for non-linear geographies (points, polygons) and for `NULL`.

![ST_Length on a Geography on the sphere](../../../../image/ST_Length_geography/ST_Length_geography.svg "ST_Length on a Geography (sphere-native)")

If you specifically want the WGS84 ellipsoidal length (which differs from the spherical value by up to ~0.5 % depending on latitude), convert via `ST_GeogToGeometry` first and call the Geometry-typed `ST_Length(..., useSpheroid => true)` overload instead.

Format:

`ST_Length (A: Geography)`

Return type: `Double`

Since: `v1.9.1`

SQL Example

```sql
SELECT ST_Length(ST_GeogFromWKT('LINESTRING (0 0, 1 0)'));
```

Output (in meters):

```
111195.10117748393
```

The result is approximately 111.2 km — one degree of arc on a sphere of radius `R = 6 371 008 m`.
80 changes: 80 additions & 0 deletions docs/image/ST_Length_geography/ST_Length_geography.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,17 @@ private[apache] case class ST_Expand(inputExpressions: Seq[Expression])
}

/**
* Return the length measurement of a Geometry
* Return the length measurement of a Geometry or Geography. Supports both Geometry (JTS, planar
* length in the input's coordinate units) and Geography (S2, geodesic length in meters on the
* WGS84 spheroid) via InferredExpression dual dispatch.
*
* @param inputExpressions
* Geometry or Geography
*/
private[apache] case class ST_Length(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.length _) {
extends InferredExpression(
inferrableFunction1(Functions.length),
inferrableFunction1(org.apache.sedona.common.geography.Functions.length)) {
Comment thread
zhangfengcdt marked this conversation as resolved.

protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import org.locationtech.jts.geom.Point
import org.locationtech.jts.io.WKTReader

/**
* Spark SQL integration tests for Geography ST functions. Tests one representative function per
* architecture level: L1 (ST_NPoints), L2 (ST_Distance), L3 (ST_Contains).
* Spark SQL integration tests for Geography ST functions. Representative functions per
* architecture level: L1 (ST_NPoints), L2 (ST_Distance, ST_Length), L3 (ST_Contains).
*/
class GeographyFunctionTest extends TestBaseScala {

Expand Down Expand Up @@ -143,10 +143,26 @@ class GeographyFunctionTest extends TestBaseScala {
}
}

// ─── Level 2: ST_Area, ST_Distance ─────────────────────────────────────
// ─── Level 2: ST_Length, ST_Area, ST_Distance ──────────────────────────

describe("Level 2: Geodesic metrics") {

it("ST_Length along the equator") {
val row = sparkSession
.sql("SELECT ST_Length(ST_GeogFromWKT('LINESTRING (0 0, 1 0)', 4326)) AS l")
.first()
val len = row.getDouble(0)
// Sphere of radius 6371008 m: 1° along a great circle is ~111,195 m.
assertEquals(111195.10, len, 1.0)
}

it("ST_Length of a point returns 0") {
val row = sparkSession
.sql("SELECT ST_Length(ST_GeogFromWKT('POINT (1 2)', 4326)) AS l")
.first()
assertEquals(0.0, row.getDouble(0), 0.0)
}

it("ST_Area unit box at equator") {
val row = sparkSession
.sql("SELECT ST_Area(ST_GeogFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))', 4326)) AS a")
Expand Down
Loading