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 @@ -245,6 +245,34 @@ public boolean isPoint() {
return wkbBaseType() == 1;
}

/** Returns the X coordinate directly from Point WKB, or null for non-points and empty points. */
public Double getPointX() {
return getPointOrdinate(0);
}

/** Returns the Y coordinate directly from Point WKB, or null for non-points and empty points. */
public Double getPointY() {
return getPointOrdinate(1);
}

/**
* Reads X or Y directly from the WKB payload. Z/M ordinates are intentionally tolerated and
* ignored so these accessors match Geometry ST_X/ST_Y behavior for higher-dimensional points.
*/
private Double getPointOrdinate(int ordinate) {
if (!isPoint()) return null;

boolean le = (wkbBytes[0] == 0x01);
int coordOffset = wkbPayloadOffset();
ByteBuffer bb =
ByteBuffer.wrap(wkbBytes).order(le ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
double x = bb.getDouble(coordOffset);
double y = bb.getDouble(coordOffset + Double.BYTES);
// JTS treats a point as empty when either X or Y is NaN.
if (Double.isNaN(x) || Double.isNaN(y)) return null;
return ordinate == 0 ? x : y;
}

/** Extract the S2Point from a Point WKB without full S2 parse. */
public S2Point extractPoint() {
requireXYOnly();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,20 @@ public static String asText(Geography g) {
return toJTS(g).toText();
}

/** Return the longitude (X coordinate) of a point geography, or null for non-point inputs. */
public static Double x(Geography g) {
if (g == null) return null;
if (g instanceof WKBGeography) return ((WKBGeography) g).getPointX();
return org.apache.sedona.common.Functions.x(toJTS(g));
}

/** Return the latitude (Y coordinate) of a point geography, or null for non-point inputs. */
public static Double y(Geography g) {
if (g == null) return null;
if (g instanceof WKBGeography) return ((WKBGeography) g).getPointY();
return org.apache.sedona.common.Functions.y(toJTS(g));
}

// ─── Level 2: Geodesic metrics ───────────────────────────────────────────

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,25 @@ public void nPoints_polygon() throws ParseException {
assertEquals(5, Functions.nPoints(g));
}

@Test
public void xY_point() throws ParseException {
Geography g = Constructors.geogFromWKT("POINT (-73.9857 40.7484)", 4326);
assertEquals(-73.9857, Functions.x(g), 1e-12);
assertEquals(40.7484, Functions.y(g), 1e-12);
}

@Test
public void xY_nonPointAndNull() throws ParseException {
Geography line = Constructors.geogFromWKT("LINESTRING (0 0, 1 1)", 4326);
Geography emptyPoint = Constructors.geogFromWKT("POINT EMPTY", 4326);
assertNull(Functions.x(line));
assertNull(Functions.y(line));
assertNull(Functions.x(emptyPoint));
assertNull(Functions.y(emptyPoint));
assertNull(Functions.x(null));
assertNull(Functions.y(null));
}

// S2 area-weighted centroids on small polygons differ from planar by an O(d^2/R^2)
// spherical correction. 5e-3 deg (~500 m) is wide enough to absorb that drift on
// 1°-scale shapes near origin, while still catching any real bug (a planar/JTS centroid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public void fromWKB_point_lazyParse() throws ParseException {
WKBGeography geog = WKBGeography.fromWKB(wkb, 4326);
assertEquals(4326, geog.getSRID());
assertSame(wkb, geog.getWKBBytes());
assertEquals(30.0, geog.getPointX(), EPS);
assertEquals(10.0, geog.getPointY(), EPS);

// Accessing JTS should parse lazily
Geometry jts = geog.getJTSGeometry();
Expand Down Expand Up @@ -245,6 +247,8 @@ public void serialize_deserialize_emptyPoint() throws IOException, ParseExceptio
byte[] bytes = GeographyWKBSerializer.serialize(original);
Geography deserialized = GeographyWKBSerializer.deserialize(bytes);
assertTrue(deserialized instanceof WKBGeography);
assertNull(((WKBGeography) deserialized).getPointX());
assertNull(((WKBGeography) deserialized).getPointY());
}

// ─── Serialize S2 Geography via new serializer ────────────────────────────
Expand Down Expand Up @@ -418,6 +422,8 @@ public void ewkbPoint_withSRIDFlag_decodesCorrectly() throws ParseException {
// isPoint() must recognize the base type after stripping the EWKB SRID flag.
assertTrue(geog.isPoint());
assertEquals(0, geog.dimension());
assertEquals(30.0, geog.getPointX(), EPS);
assertEquals(10.0, geog.getPointY(), EPS);

// extractPoint() must skip the 4 SRID bytes; lon/lat should be the original values.
S2Point p = geog.extractPoint();
Expand All @@ -432,6 +438,8 @@ public void isoPointZ_throwsUnsupported() {
WKBGeography geog = WKBGeography.fromWKB(wkbZ, 0);
// isPoint() is safe — just tests base type — but extractPoint/shape must refuse Z/M.
assertTrue(geog.isPoint());
assertEquals(30.0, geog.getPointX(), EPS);
assertEquals(10.0, geog.getPointY(), EPS);
assertThrows(UnsupportedOperationException.class, geog::extractPoint);
assertThrows(UnsupportedOperationException.class, () -> new WkbS2Shape(wkbZ));
}
Expand Down
2 changes: 2 additions & 0 deletions docs/api/sql/geography/Geography-Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ These functions operate on geography type objects.
| [ST_DWithin](Geography-Functions/ST_DWithin.md) | Boolean | Test whether two geographies are within a given geodesic distance (in meters) of each other. | v1.9.1 |
| [ST_Within](Geography-Functions/ST_Within.md) | Boolean | Test whether geography A is fully within geography B. | v1.9.1 |
| [ST_Equals](Geography-Functions/ST_Equals.md) | Boolean | Test whether two geographies are spatially equal. | v1.9.1 |
| [ST_X](Geography-Functions/ST_X.md) | Double | Return the longitude (X coordinate) of a point geography. | v1.9.1 |
| [ST_Y](Geography-Functions/ST_Y.md) | Double | Return the latitude (Y coordinate) of a point geography. | v1.9.1 |
42 changes: 42 additions & 0 deletions docs/api/sql/geography/Geography-Functions/ST_X.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
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_X

Introduction: Returns the longitude (X coordinate) of a point geography. Returns `NULL` for non-point geographies and empty points.

Format:

`ST_X (A: Geography)`

Return type: `Double`

Since: `v1.9.1`

SQL Example

```sql
SELECT ST_X(ST_GeogFromWKT('POINT (-73.9857 40.7484)'));
```

Output:

```
-73.9857
```
42 changes: 42 additions & 0 deletions docs/api/sql/geography/Geography-Functions/ST_Y.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
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_Y

Introduction: Returns the latitude (Y coordinate) of a point geography. Returns `NULL` for non-point geographies and empty points.

Format:

`ST_Y (A: Geography)`

Return type: `Double`

Since: `v1.9.1`

SQL Example

```sql
SELECT ST_Y(ST_GeogFromWKT('POINT (-73.9857 40.7484)'));
```

Output:

```
40.7484
```
Original file line number Diff line number Diff line change
Expand Up @@ -736,15 +736,19 @@ private[apache] case class ST_Azimuth(inputExpressions: Seq[Expression])
}

private[apache] case class ST_X(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.x _) {
extends InferredExpression(
inferrableFunction1(Functions.x),
inferrableFunction1(org.apache.sedona.common.geography.Functions.x)) {

protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
}
}

private[apache] case class ST_Y(inputExpressions: Seq[Expression])
extends InferredExpression(Functions.y _) {
extends InferredExpression(
inferrableFunction1(Functions.y),
inferrableFunction1(org.apache.sedona.common.geography.Functions.y)) {

protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = {
copy(inputExpressions = newChildren)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@ class GeographyFunctionTest extends TestBaseScala {
assertEquals(1.0, point.getX, 1e-9)
assertEquals(2.0, point.getY, 1e-9)
}

it("ST_X and ST_Y") {
val row = sparkSession
.sql("""
SELECT
ST_X(ST_GeogFromWKT('POINT (-73.9857 40.7484)', 4326)) AS x,
ST_Y(ST_GeogFromWKT('POINT (-73.9857 40.7484)', 4326)) AS y
""")
.first()
assertEquals(-73.9857, row.getDouble(0), 1e-12)
assertEquals(40.7484, row.getDouble(1), 1e-12)
}

it("ST_X and ST_Y return null for non-point geography") {
val row = sparkSession
.sql("""
SELECT
ST_X(ST_GeogFromWKT('LINESTRING (0 0, 1 1)', 4326)) AS x,
ST_Y(ST_GeogFromWKT('LINESTRING (0 0, 1 1)', 4326)) AS y
""")
.first()
assertTrue(row.isNullAt(0))
assertTrue(row.isNullAt(1))
}
}

// ─── Level 2: ST_Length, ST_Area, ST_Distance ──────────────────────────
Expand Down
Loading