This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch rs-set-crs-2674
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/rs-set-crs-2674 by this push:
new bb42c4df69 fix: address PR review comments
bb42c4df69 is described below
commit bb42c4df69a842f20e3e1c219e49ab9c1d0dcd7a
Author: Jia Yu <[email protected]>
AuthorDate: Sun Mar 1 02:11:08 2026 -0800
fix: address PR review comments
- Use longitude-first axis order for WKT parsing
(FORCE_LONGITUDE_FIRST_AXIS_ORDER)
- Remove tryResolveToEpsg() - RS_SRID returns 0 for custom CRS, use RS_CRS
instead
- Add null/empty guard for format parameter in crs()
- Use ConcurrentHashMap for thread-safe alias cache writes
- Guard DefaultMathTransformFactory downcast with instanceof
- Catch specific exceptions in proj4sedona parsing, attach as suppressed
- Remove 'lossless' claim from PROJJSON docs
- Update RS_SRID docs: 0 can mean custom (non-EPSG) CRS
- Fix Javadoc to include WKT2 in format list
- Update Spark test to expect SRID=0 for WKT1 without AUTHORITY
---
.../sedona/common/raster/RasterAccessors.java | 3 ++
.../apache/sedona/common/raster/RasterEditors.java | 50 ++++++++++++++++------
.../common/raster/CrsRoundTripComplianceTest.java | 2 +-
docs/api/sql/Raster-Operators/RS_CRS.md | 2 +-
docs/api/sql/Raster-Operators/RS_SRID.md | 2 +-
.../org/apache/sedona/sql/rasteralgebraTest.scala | 8 +++-
6 files changed, 48 insertions(+), 19 deletions(-)
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
b/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
index e8306a02bc..187365bbd8 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
@@ -381,6 +381,9 @@ public class RasterAccessors {
* @return The CRS definition string in the requested format, or null if no
CRS is set.
*/
public static String crs(GridCoverage2D raster, String format) {
+ if (format == null || format.trim().isEmpty()) {
+ format = "projjson";
+ }
CoordinateReferenceSystem crsDef = raster.getCoordinateReferenceSystem();
if (crsDef instanceof DefaultEngineeringCRS) {
if (((DefaultEngineeringCRS) crsDef).isWildcard()) {
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
b/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
index 23030e5f58..ae88b28e88 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
@@ -30,6 +30,7 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.media.jai.Interpolation;
@@ -42,10 +43,12 @@ import org.geotools.api.coverage.grid.GridCoverage;
import org.geotools.api.coverage.grid.GridGeometry;
import org.geotools.api.metadata.spatial.PixelOrientation;
import org.geotools.api.referencing.FactoryException;
+import org.geotools.api.referencing.crs.CRSFactory;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.api.referencing.datum.PixelInCell;
import org.geotools.api.referencing.operation.MathTransform;
import org.geotools.api.referencing.operation.MathTransform2D;
+import org.geotools.api.referencing.operation.MathTransformFactory;
import org.geotools.api.referencing.operation.OperationMethod;
import org.geotools.api.referencing.operation.Projection;
import org.geotools.api.referencing.operation.TransformException;
@@ -62,6 +65,7 @@ import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.crs.DefaultEngineeringCRS;
import org.geotools.referencing.operation.DefaultMathTransformFactory;
import org.geotools.referencing.operation.transform.AffineTransform2D;
+import org.geotools.util.factory.Hints;
import org.locationtech.jts.index.strtree.STRtree;
public class RasterEditors {
@@ -154,14 +158,17 @@ public class RasterEditors {
// Not an authority code, continue
}
- // Step 2: Try GeoTools CRS.parseWKT (handles WKT1)
+ // Step 2: Try GeoTools WKT parsing with longitude-first axis order
(handles WKT1)
try {
- return CRS.parseWKT(crsString);
+ Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,
Boolean.TRUE);
+ CRSFactory crsFactory = ReferencingFactoryFinder.getCRSFactory(hints);
+ return crsFactory.createFromWKT(crsString);
} catch (FactoryException e) {
// Not WKT1, continue
}
// Step 3: Use proj4sedona (handles WKT2, PROJ, PROJJSON)
+ Exception lastError = null;
try {
Proj proj = new Proj(crsString);
@@ -183,9 +190,12 @@ public class RasterEditors {
// 3. Strip unexpected parameters iteratively
String wkt1 = proj.toWkt1();
if (wkt1 != null && !wkt1.isEmpty()) {
+ Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,
Boolean.TRUE);
+ CRSFactory crsFactory = ReferencingFactoryFinder.getCRSFactory(hints);
+
// Strategy 1: Try raw WKT1 directly
try {
- return CRS.parseWKT(wkt1);
+ return crsFactory.createFromWKT(wkt1);
} catch (FactoryException ex) {
// Raw WKT1 failed, continue with normalization
}
@@ -199,7 +209,7 @@ public class RasterEditors {
String currentWkt = normalizedWkt;
for (int attempt = 0; attempt < 5; attempt++) {
try {
- return CRS.parseWKT(currentWkt);
+ return crsFactory.createFromWKT(currentWkt);
} catch (FactoryException ex) {
String msg = ex.getMessage();
if (msg != null) {
@@ -209,18 +219,24 @@ public class RasterEditors {
continue;
}
}
+ lastError = ex;
break; // Different kind of error, give up
}
}
}
- } catch (Exception e) {
- // proj4sedona could not parse it either
+ } catch (RuntimeException e) {
+ lastError = e;
}
- throw new IllegalArgumentException(
- "Cannot parse CRS string. Supported formats: EPSG code (e.g.
'EPSG:4326'), "
- + "WKT1, WKT2, PROJ string, PROJJSON. Input: "
- + crsString);
+ IllegalArgumentException error =
+ new IllegalArgumentException(
+ "Cannot parse CRS string. Supported formats: EPSG code (e.g.
'EPSG:4326'), "
+ + "WKT1, WKT2, PROJ string, PROJJSON. Input: "
+ + crsString);
+ if (lastError != null) {
+ error.addSuppressed(lastError);
+ }
+ throw error;
}
// Fallback map for proj4sedona projection names that have no equivalent in
GeoTools'
@@ -243,7 +259,8 @@ public class RasterEditors {
}
// Lazy-initialized caches built once from GeoTools' registered
OperationMethod objects.
- // aliasCache: exact alias string -> canonical OGC name
+ // aliasCache: exact alias string -> canonical OGC name (ConcurrentHashMap
for thread-safe
+ // writes from Tier 2 normalized matching)
// normalizedCache: normalized form (lowercase, no spaces/underscores) ->
set of canonical names
private static volatile Map<String, String> aliasCache;
private static volatile Map<String, Set<String>> normalizedCache;
@@ -341,11 +358,16 @@ public class RasterEditors {
if (aliasCache != null) {
return;
}
- DefaultMathTransformFactory factory =
- (DefaultMathTransformFactory)
ReferencingFactoryFinder.getMathTransformFactory(null);
+ MathTransformFactory mtf =
ReferencingFactoryFinder.getMathTransformFactory(null);
+ DefaultMathTransformFactory factory;
+ if (mtf instanceof DefaultMathTransformFactory) {
+ factory = (DefaultMathTransformFactory) mtf;
+ } else {
+ factory = new DefaultMathTransformFactory();
+ }
Set<OperationMethod> methods =
factory.getAvailableMethods(Projection.class);
- Map<String, String> aliases = new HashMap<>();
+ Map<String, String> aliases = new ConcurrentHashMap<>();
Map<String, Set<String>> normalized = new HashMap<>();
for (OperationMethod method : methods) {
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/CrsRoundTripComplianceTest.java
b/common/src/test/java/org/apache/sedona/common/raster/CrsRoundTripComplianceTest.java
index 6a64d6de35..5833d86d7d 100644
---
a/common/src/test/java/org/apache/sedona/common/raster/CrsRoundTripComplianceTest.java
+++
b/common/src/test/java/org/apache/sedona/common/raster/CrsRoundTripComplianceTest.java
@@ -29,7 +29,7 @@ import org.junit.Test;
/**
* Round-trip compliance tests for RS_SetCRS and RS_CRS across representative
EPSG codes.
*
- * <p>For each EPSG code and each format (PROJ, PROJJSON, WKT1), this test:
+ * <p>For each EPSG code and each format (PROJ, PROJJSON, WKT1, WKT2), this
test:
*
* <ol>
* <li>Creates a raster with that CRS via RS_SetCRS("EPSG:xxxx")
diff --git a/docs/api/sql/Raster-Operators/RS_CRS.md
b/docs/api/sql/Raster-Operators/RS_CRS.md
index 04d98dc0eb..c8586c02a7 100644
--- a/docs/api/sql/Raster-Operators/RS_CRS.md
+++ b/docs/api/sql/Raster-Operators/RS_CRS.md
@@ -37,7 +37,7 @@ Since: `v1.9.0`
| Format | Description |
| :--- | :--- |
-| `'projjson'` | PROJJSON format (default). Modern, lossless, machine-readable
JSON representation. |
+| `'projjson'` | PROJJSON format (default). Modern, machine-readable JSON
representation. |
| `'wkt2'` | Well-Known Text 2 (ISO 19162). Modern standard CRS
representation. |
| `'wkt1'` | Well-Known Text 1 (OGC 01-009). Legacy format, widely supported. |
| `'proj'` | PROJ string format. Compact, human-readable representation. |
diff --git a/docs/api/sql/Raster-Operators/RS_SRID.md
b/docs/api/sql/Raster-Operators/RS_SRID.md
index 7820828e2e..8d79b94b0c 100644
--- a/docs/api/sql/Raster-Operators/RS_SRID.md
+++ b/docs/api/sql/Raster-Operators/RS_SRID.md
@@ -19,7 +19,7 @@
# RS_SRID
-Introduction: Returns the spatial reference system identifier (SRID) of the
raster geometry.
+Introduction: Returns the spatial reference system identifier (SRID) of the
raster geometry. Returns 0 if the raster has no CRS defined or if the CRS is a
custom (non-EPSG) coordinate reference system. To retrieve the full CRS
definition for custom CRS, use [RS_CRS](RS_CRS.md).
Format: `RS_SRID (raster: Raster)`
diff --git
a/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
b/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
index db975596b1..a6736429da 100644
--- a/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
+++ b/spark/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
@@ -500,9 +500,13 @@ class rasteralgebraTest extends TestBaseScala with
BeforeAndAfter with GivenWhen
val wkt1 =
"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS
84\",6378137,298.257223563]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433]]"
val df = sparkSession.read.format("binaryFile").load(resourceFolder +
"raster/test1.tiff")
- val result =
+ // WKT1 without AUTHORITY clause: RS_SRID returns 0, but RS_CRS still
works
+ val srid =
df.selectExpr(s"RS_SRID(RS_SetCRS(RS_FromGeoTiff(content),
'${wkt1}'))").first().getInt(0)
- assert(result == 4326)
+ assert(srid == 0)
+ val crs =
+ df.selectExpr(s"RS_CRS(RS_SetCRS(RS_FromGeoTiff(content), '${wkt1}'),
'wkt1')").first().getString(0)
+ assert(crs != null && crs.contains("WGS"))
}
it("Passed RS_CRS should handle null values") {