Copilot commented on code in PR #2677:
URL: https://github.com/apache/sedona/pull/2677#discussion_r2871243163
##########
common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java:
##########
@@ -359,4 +363,101 @@ public static RasterMetadata
rasterMetadata(GridCoverage2D raster) throws Factor
(int) meta[10],
(int) meta[11]);
}
+
+ /**
+ * Returns the CRS of a raster as PROJJSON string.
+ *
+ * @param raster The input raster.
+ * @return The CRS definition as PROJJSON string, or null if no CRS is set.
+ */
+ public static String crs(GridCoverage2D raster) {
+ return crs(raster, "projjson");
+ }
+
+ /**
+ * Returns the CRS of a raster in the specified format.
+ *
+ * @param raster The input raster.
+ * @param format The desired output format: "projjson", "wkt2", "wkt1", or
"proj".
+ * @return The CRS definition string in the requested format, or null if no
CRS is set.
+ */
+ public static String crs(GridCoverage2D raster, String format) {
+ String fmt;
+ if (format == null || format.trim().isEmpty()) {
+ fmt = "projjson";
+ } else {
+ fmt = format.toLowerCase(Locale.ROOT).trim();
+ }
+ CoordinateReferenceSystem crsDef = raster.getCoordinateReferenceSystem();
+ if (crsDef instanceof DefaultEngineeringCRS) {
+ if (((DefaultEngineeringCRS) crsDef).isWildcard()) {
+ return null;
+ }
+ }
+
+ // Get WKT1 representation from GeoTools (native, no conversion needed)
+ String wkt1;
+ if (crsDef instanceof Formattable) {
+ wkt1 = ((Formattable) crsDef).toWKT(2, false);
+ } else {
+ wkt1 = crsDef.toWKT();
+ }
+
+ if ("wkt1".equals(fmt) || "wkt".equals(fmt)) {
+ return wkt1;
+ }
+
+ // For all other formats, convert through proj4sedona.
+ // Prefer EPSG SRID when available: GeoTools WKT1 projection names (e.g.
Mercator_2SP)
+ // may not be recognized by proj4sedona, but EPSG codes always work.
+ try {
+ Proj proj;
+ int srid = srid(raster);
+ if (srid > 0) {
+ try {
+ proj = new Proj("EPSG:" + srid);
+ } catch (Exception e) {
+ // EPSG code not recognized by proj4sedona, fall back to WKT1
+ proj = createProjFromWkt1(wkt1);
+ }
+ } else {
+ proj = createProjFromWkt1(wkt1);
+ }
+ switch (fmt) {
+ case "projjson":
+ return proj.toProjJson();
+ case "wkt2":
+ return proj.toWkt2();
+ case "proj":
+ case "proj4":
+ return proj.toProjString();
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported CRS format: '"
+ + format
+ + "'. Supported formats: projjson, wkt2, wkt1, proj");
Review Comment:
The error message for unsupported `format` says the supported formats are
`projjson, wkt2, wkt1, proj`, but the implementation also accepts aliases
(`wkt` and `proj4`). Either document these aliases (and include them in the
error message) or remove the aliases to keep the API surface consistent with
the docs.
```suggestion
+ "'. Supported formats: projjson, wkt2, wkt1 (alias:
wkt), proj (alias: proj4)");
```
##########
common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java:
##########
@@ -102,7 +109,159 @@ public static GridCoverage2D setSrid(GridCoverage2D
raster, int srid) {
} else {
crs = FunctionsGeoTools.sridToCRS(srid);
}
+ return replaceCrs(raster, crs);
+ }
+
+ /**
+ * Sets the CRS of a raster using a CRS string. Accepts EPSG codes (e.g.
"EPSG:4326"), WKT1, WKT2,
+ * PROJ strings, and PROJJSON.
+ *
+ * @param raster The input raster.
+ * @param crsString The CRS definition string.
+ * @return The raster with the new CRS.
+ */
+ public static GridCoverage2D setCrs(GridCoverage2D raster, String crsString)
{
+ CoordinateReferenceSystem crs = parseCrsString(crsString);
+ return replaceCrs(raster, crs);
+ }
+
+ /**
+ * Parse a CRS string in any supported format into a GeoTools
CoordinateReferenceSystem.
+ *
+ * <p>Parsing priority:
+ *
+ * <ol>
+ * <li>GeoTools CRS.decode — handles authority codes like EPSG:4326
+ * <li>GeoTools CRS.parseWKT — handles WKT1 strings
+ * <li>proj4sedona — handles WKT2, PROJ strings, PROJJSON. If an EPSG
authority can be resolved,
+ * uses CRS.decode for a lossless result. Otherwise falls back to WKT1
conversion.
+ * </ol>
+ *
+ * @param crsString The CRS definition string.
+ * @return The parsed CoordinateReferenceSystem.
+ * @throws IllegalArgumentException if the CRS string cannot be parsed.
+ */
+ static CoordinateReferenceSystem parseCrsString(String crsString) {
+ if (crsString == null || crsString.trim().isEmpty()) {
+ throw new IllegalArgumentException(
+ "CRS string must not be null or empty. "
+ + "Supported formats: EPSG code (e.g. 'EPSG:4326'), WKT1, WKT2,
PROJ string, PROJJSON.");
+ }
+
+ // Step 1: Try GeoTools CRS.decode (handles EPSG:xxxx, AUTO:xxxx, etc.)
+ try {
+ return CRS.decode(crsString, true);
+ } catch (FactoryException e) {
+ // Not an authority code, continue
+ }
+
+ // Step 2: Try GeoTools WKT parsing with longitude-first axis order
(handles WKT1)
+ Hints hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,
Boolean.TRUE);
+ CRSFactory crsFactory = ReferencingFactoryFinder.getCRSFactory(hints);
+ try {
+ 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);
+
+ // Try to resolve to an EPSG authority code for a lossless result
+ String authority = proj.toEpsgCode();
+ if (authority != null && !authority.isEmpty()) {
+ try {
+ return CRS.decode(authority, true);
+ } catch (FactoryException ex) {
+ // Authority code not recognized by GeoTools, fall through to WKT1
+ }
+ }
+
+ // Fallback: convert to WKT1 via proj4sedona and parse with GeoTools.
+ // proj4sedona may include parameters GeoTools doesn't expect (e.g.
standard_parallel_1
+ // for projections that don't use it). We handle this by trying several
parse strategies:
+ // 1. Raw WKT1 (proj4sedona's projection names may already be recognized
by GeoTools)
+ // 2. Normalized WKT1 (resolve projection names to canonical OGC names)
+ // 3. Strip unexpected parameters iteratively
+ String wkt1 = proj.toWkt1();
+ if (wkt1 != null && !wkt1.isEmpty()) {
+
+ // Strategy 1: Try raw WKT1 directly
+ try {
+ return crsFactory.createFromWKT(wkt1);
+ } catch (FactoryException ex) {
+ // Raw WKT1 failed, continue with normalization
+ }
+
+ // Strategy 2: Try with normalized projection name
+ String normalizedWkt = CrsNormalization.normalizeWkt1ForGeoTools(wkt1);
+ // Strategy 3: If parsing fails due to unexpected parameters, strip
them iteratively.
+ // proj4sedona sometimes includes parameters like standard_parallel_1
for projections
+ // that don't use it. We parse the error message to identify and
remove the offending
+ // parameter, then retry.
+ String currentWkt = normalizedWkt;
+ for (int attempt = 0; attempt < 5; attempt++) {
+ try {
+ return crsFactory.createFromWKT(currentWkt);
+ } catch (FactoryException ex) {
+ lastError = ex;
+ String msg = ex.getMessage();
+ if (msg != null) {
+ Matcher paramMatcher = UNEXPECTED_PARAM_PATTERN.matcher(msg);
+ if (paramMatcher.find()) {
+ String stripped = stripWktParameter(currentWkt,
paramMatcher.group(1));
+ if (stripped.equals(currentWkt)) {
+ break; // Strip was a no-op, give up
+ }
+ currentWkt = stripped;
+ continue;
+ }
+ }
+ break; // Different kind of error, give up
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ lastError = e;
+ }
+
+ 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;
+ }
+
Review Comment:
The thrown `IllegalArgumentException` includes the full `crsString` content
in the message. CRS definitions (WKT/PROJJSON) can be very large, which can
lead to huge exception messages/log output (and Spark driver/UI instability).
Consider truncating the input in the message (e.g., include length + a short
prefix) while keeping the full string available via suppressed/cause if needed.
```suggestion
String crsPreview = summarizeCrsString(crsString);
IllegalArgumentException error =
new IllegalArgumentException(
"Cannot parse CRS string. Supported formats: EPSG code (e.g.
'EPSG:4326'), "
+ "WKT1, WKT2, PROJ string, PROJJSON. "
+ "Input length=" + crsString.length()
+ ", preview: " + crsPreview);
if (lastError != null) {
error.addSuppressed(lastError);
}
throw error;
}
private static String summarizeCrsString(String crsString) {
if (crsString == null) {
return "null";
}
int maxPreviewLength = 256;
if (crsString.length() <= maxPreviewLength) {
return crsString;
}
return crsString.substring(0, maxPreviewLength) + "...";
}
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]