From e1da65fc8327e75a7fcfcc7d0d9ffcae9c814aa2 Mon Sep 17 00:00:00 2001 From: Kristin Cowalcijk Date: Fri, 13 Sep 2024 18:14:36 +0800 Subject: [PATCH] Add a lenient mode for RS_Clip and make it lenient by default. --- .../common/raster/RasterBandEditors.java | 37 ++++++++++++++++++- .../common/raster/RasterBandEditorsTest.java | 24 ++++++++++++ docs/api/sql/Raster-operators.md | 7 +++- .../raster/RasterBandEditors.scala | 1 + .../apache/sedona/sql/rasteralgebraTest.scala | 7 ++++ 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/apache/sedona/common/raster/RasterBandEditors.java b/common/src/main/java/org/apache/sedona/common/raster/RasterBandEditors.java index 852a81db4b..781ef7bed3 100644 --- a/common/src/main/java/org/apache/sedona/common/raster/RasterBandEditors.java +++ b/common/src/main/java/org/apache/sedona/common/raster/RasterBandEditors.java @@ -29,6 +29,7 @@ import org.apache.sedona.common.utils.RasterUtils; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.grid.GridCoverage2D; +import org.geotools.coverage.processing.CannotCropException; import org.geotools.coverage.processing.operation.Crop; import org.locationtech.jts.geom.Geometry; import org.opengis.parameter.ParameterValueGroup; @@ -273,10 +274,17 @@ private static void ensureBandAppend(GridCoverage2D raster, int band) { * @param geometry Specify ROI * @param noDataValue no-Data value for empty cells * @param crop Specifies to keep the original extent or not + * @param lenient Return null if the raster and geometry do not intersect when set to true, + * otherwise will throw an exception * @return A clip Raster with defined ROI by the geometry */ public static GridCoverage2D clip( - GridCoverage2D raster, int band, Geometry geometry, double noDataValue, boolean crop) + GridCoverage2D raster, + int band, + Geometry geometry, + double noDataValue, + boolean crop, + boolean lenient) throws FactoryException, TransformException { // Selecting the band from original raster @@ -296,7 +304,16 @@ public static GridCoverage2D clip( parameters.parameter(Crop.PARAMNAME_DEST_NODATA).setValue(new double[] {noDataValue}); parameters.parameter(Crop.PARAMNAME_ROI).setValue(geometry); - GridCoverage2D newRaster = (GridCoverage2D) cropObject.doOperation(parameters, null); + GridCoverage2D newRaster; + try { + newRaster = (GridCoverage2D) cropObject.doOperation(parameters, null); + } catch (CannotCropException e) { + if (lenient) { + return null; + } else { + throw e; + } + } if (!crop) { double[] metadataOriginal = RasterAccessors.metadata(raster); @@ -383,6 +400,22 @@ public static GridCoverage2D clip( return newRaster; } + /** + * Return a clipped raster with the specified ROI by the geometry + * + * @param raster Raster to clip + * @param band Band number to perform clipping + * @param geometry Specify ROI + * @param noDataValue no-Data value for empty cells + * @param crop Specifies to keep the original extent or not + * @return A clip Raster with defined ROI by the geometry + */ + public static GridCoverage2D clip( + GridCoverage2D raster, int band, Geometry geometry, double noDataValue, boolean crop) + throws FactoryException, TransformException { + return clip(raster, band, geometry, noDataValue, crop, true); + } + /** * Return a clipped raster with the specified ROI by the geometry. * diff --git a/common/src/test/java/org/apache/sedona/common/raster/RasterBandEditorsTest.java b/common/src/test/java/org/apache/sedona/common/raster/RasterBandEditorsTest.java index f23aa279d8..6a560b7280 100644 --- a/common/src/test/java/org/apache/sedona/common/raster/RasterBandEditorsTest.java +++ b/common/src/test/java/org/apache/sedona/common/raster/RasterBandEditorsTest.java @@ -32,6 +32,7 @@ import org.apache.sedona.common.Constructors; import org.apache.sedona.common.raster.serde.Serde; import org.geotools.coverage.grid.GridCoverage2D; +import org.geotools.coverage.processing.CannotCropException; import org.junit.Test; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.io.ParseException; @@ -247,6 +248,29 @@ public void testClip() assertTrue(Arrays.equals(expectedValues, actualValues)); } + @Test + public void testClipLenient() + throws FactoryException, IOException, ParseException, TransformException { + GridCoverage2D raster = + rasterFromGeoTiff(resourceFolder + "raster_geotiff_color/FAA_UTM18N_NAD83.tif"); + + // Construct a polygon that does not intersect with the raster + Geometry nonIntersectingGeom = + Constructors.geomFromWKT( + "POLYGON ((-78.22106647832458748 37.76411511479908967, -78.20183062098976734 37.72863564460374874, -78.18088490966962922 37.76753482276972562, -78.22106647832458748 37.76411511479908967))", + 0); + + // Throws an exception in non-lenient mode + assertThrows( + CannotCropException.class, + () -> RasterBandEditors.clip(raster, 1, nonIntersectingGeom, 200, false, false)); + + // Returns null in lenient mode + GridCoverage2D result = RasterBandEditors.clip(raster, 1, nonIntersectingGeom, 200, false); + assertNull(result); + raster.dispose(true); + } + @Test public void testRasterUnion() throws FactoryException { double[][] rasterData1 = diff --git a/docs/api/sql/Raster-operators.md b/docs/api/sql/Raster-operators.md index 91c72ee253..f1c0f757f9 100644 --- a/docs/api/sql/Raster-operators.md +++ b/docs/api/sql/Raster-operators.md @@ -1428,10 +1428,15 @@ Introduction: Returns a raster that is clipped by the given geometry. If `crop` is not specified then it will default to `true`, meaning it will make the resulting raster shrink to the geometry's extent and if `noDataValue` is not specified then the resulting raster will have the minimum possible value for the band pixel data type. !!!Note - Since `v1.5.1`, if the coordinate reference system (CRS) of the input `geom` geometry differs from that of the `raster`, then `geom` will be transformed to match the CRS of the `raster`. If the `raster` or `geom` doesn't have a CRS then it will default to `4326/WGS84`. + - Since `v1.5.1`, if the coordinate reference system (CRS) of the input `geom` geometry differs from that of the `raster`, then `geom` will be transformed to match the CRS of the `raster`. If the `raster` or `geom` doesn't have a CRS then it will default to `4326/WGS84`. + - Since `v1.7.0`, `RS_Clip` function will return `null` if the `raster` and `geometry` geometry do not intersect. If you want to throw an exception in this case, you can set the `lenient` parameter to `false`. Format: +``` +RS_Clip(raster: Raster, band: Integer, geom: Geometry, noDataValue: Double, crop: Boolean, lenient: Boolean) +``` + ``` RS_Clip(raster: Raster, band: Integer, geom: Geometry, noDataValue: Double, crop: Boolean) ``` diff --git a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandEditors.scala b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandEditors.scala index 2026144b10..b9690115f0 100644 --- a/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandEditors.scala +++ b/spark/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandEditors.scala @@ -59,6 +59,7 @@ case class RS_Union(inputExpressions: Seq[Expression]) case class RS_Clip(inputExpressions: Seq[Expression]) extends InferredExpression( + inferrableFunction6(RasterBandEditors.clip), inferrableFunction5(RasterBandEditors.clip), inferrableFunction4(RasterBandEditors.clip), inferrableFunction3(RasterBandEditors.clip)) { 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 ea81708aed..b639532946 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 @@ -1027,6 +1027,13 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen expectedValues = Seq(0.0, 0.0, 0.0, 0.0, null) assertTrue(expectedValues.equals(actualValues)) + // Test with a polygon that does not intersect the raster in lenient mode + val actual = df + .selectExpr( + "RS_Clip(raster, 1, ST_GeomFromWKT('POLYGON((274157 4174899,263510 4174947,269859 4183348,274157 4174899))'))") + .first() + .get(0) + assertNull(actual) } it("Passed RS_AsGeoTiff") {