This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push:
new 5a15d06553 [SEDONA-746] Fix RS_Clip behavior (#2400)
5a15d06553 is described below
commit 5a15d065538cce8f59674cf2d463164727dce2a0
Author: Pranav Toggi <[email protected]>
AuthorDate: Tue Oct 14 23:53:27 2025 -0700
[SEDONA-746] Fix RS_Clip behavior (#2400)
Co-authored-by: Copilot <[email protected]>
---
.../sedona/common/raster/RasterBandEditors.java | 231 +++++++++++----------
.../apache/sedona/common/raster/RasterOutputs.java | 86 ++++++++
.../apache/sedona/common/raster/Rasterization.java | 155 +++++++++-----
.../sedona/common/raster/FunctionEditorsTest.java | 4 +-
.../common/raster/RasterBandAccessorsTest.java | 20 ++
.../common/raster/RasterBandEditorsTest.java | 104 +++++++++-
.../common/raster/RasterConstructorsTest.java | 2 +-
.../sedona/common/raster/RasterizationTests.java | 162 +++++++++++++++
spark/common/src/test/resources/allNoData.tiff | Bin 0 -> 131468 bytes
.../rasterization/test_rasterization.tiff | Bin 0 -> 524672 bytes
10 files changed, 591 insertions(+), 173 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 51c906429f..82f57610d6 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
@@ -27,7 +27,6 @@ import javax.media.jai.RasterFactory;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.sedona.common.utils.RasterUtils;
-import org.geotools.api.geometry.BoundingBox;
import org.geotools.api.parameter.ParameterValueGroup;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.operation.TransformException;
@@ -35,8 +34,6 @@ 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.geotools.geometry.jts.JTS;
-import org.geotools.geometry.jts.ReferencedEnvelope;
import org.locationtech.jts.geom.Geometry;
public class RasterBandEditors {
@@ -269,6 +266,46 @@ public class RasterBandEditors {
}
}
+ public static GridCoverage2D crop(
+ GridCoverage2D raster, double noDataValue, Geometry geomExtent, boolean
lenient) {
+ // Crop the raster
+ // this will shrink the extent of the raster to the geometry
+ Crop cropObject = new Crop();
+ ParameterValueGroup parameters = cropObject.getParameters();
+ parameters.parameter("Source").setValue(raster);
+ parameters.parameter(Crop.PARAMNAME_DEST_NODATA).setValue(new double[]
{noDataValue});
+ parameters.parameter(Crop.PARAMNAME_ROI).setValue(geomExtent);
+
+ // crop the raster to the geometry extent
+ try {
+ raster = (GridCoverage2D) cropObject.doOperation(parameters, null);
+ } catch (CannotCropException e) {
+ if (lenient) {
+ return null;
+ } else {
+ throw e;
+ }
+ }
+
+ RenderedImage image = raster.getRenderedImage();
+ int minX = image.getMinX();
+ int minY = image.getMinY();
+ if (minX != 0 || minY != 0) {
+ raster = RasterUtils.shiftRasterToZeroOrigin(raster, noDataValue);
+ } else {
+ raster =
+ RasterUtils.clone(
+ raster.getRenderedImage(),
+ raster.getGridGeometry(),
+ raster.getSampleDimensions(),
+ raster,
+ noDataValue,
+ true);
+ }
+
+ return raster;
+ }
+
/**
* Return a clipped raster with the specified ROI by the geometry
*
@@ -292,121 +329,97 @@ public class RasterBandEditors {
boolean lenient)
throws FactoryException, TransformException {
- // Selecting the band from original raster
- RasterUtils.ensureBand(raster, band);
- GridCoverage2D singleBandRaster = RasterBandAccessors.getBand(raster, new
int[] {band});
-
- Pair<GridCoverage2D, Geometry> pair =
- RasterUtils.setDefaultCRSAndTransform(singleBandRaster, geometry);
- singleBandRaster = pair.getLeft();
- geometry = pair.getRight();
-
- // Use rasterizeGeomExtent for AOI geometries smaller than a pixel
- double[] metadata = RasterAccessors.metadata(singleBandRaster);
- ReferencedEnvelope geomEnvelope =
- Rasterization.rasterizeGeomExtent(geometry, singleBandRaster,
metadata, allTouched);
- Geometry geomExtent = JTS.toGeometry((BoundingBox) geomEnvelope);
-
- // Crop the raster
- // this will shrink the extent of the raster to the geometry
- Crop cropObject = new Crop();
- ParameterValueGroup parameters = cropObject.getParameters();
- parameters.parameter("Source").setValue(singleBandRaster);
- parameters.parameter(Crop.PARAMNAME_DEST_NODATA).setValue(new double[]
{noDataValue});
- parameters.parameter(Crop.PARAMNAME_ROI).setValue(geomExtent);
-
- GridCoverage2D newRaster;
- try {
- newRaster = (GridCoverage2D) cropObject.doOperation(parameters, null);
- } catch (CannotCropException e) {
+ if (!RasterPredicates.rsIntersects(raster, geometry)) {
if (lenient) {
return null;
- } else {
- throw e;
}
+ throw new IllegalArgumentException("Geometry does not intersect
Raster.");
}
- if (!crop) {
- double[] metadataOriginal = RasterAccessors.metadata(raster);
- int widthOriginalRaster = (int) metadataOriginal[2],
- heightOriginalRaster = (int) metadataOriginal[3];
- Raster rasterData = RasterUtils.getRaster(raster.getRenderedImage());
+ // Selecting the band from original raster
+ RasterUtils.ensureBand(raster, band);
+ Pair<GridCoverage2D, Geometry> pair =
RasterUtils.setDefaultCRSAndTransform(raster, geometry);
+ geometry = pair.getRight();
- // create a new raster and set a default value that's the no-data value
- String bandType = RasterBandAccessors.getBandType(raster, 1);
- int dataTypeCode =
RasterUtils.getDataTypeCode(RasterBandAccessors.getBandType(raster, 1));
- boolean isDataTypeIntegral =
RasterUtils.isDataTypeIntegral(dataTypeCode);
- WritableRaster resultRaster =
- RasterFactory.createBandedRaster(
- dataTypeCode, widthOriginalRaster, heightOriginalRaster, 1,
null);
- int sizeOfArray = widthOriginalRaster * heightOriginalRaster;
- if (isDataTypeIntegral) {
- int[] array =
- ArrayUtils.toPrimitive(
- Collections.nCopies(sizeOfArray, (int) noDataValue)
- .toArray(new Integer[sizeOfArray]));
- resultRaster.setSamples(0, 0, widthOriginalRaster,
heightOriginalRaster, 0, array);
- } else {
- double[] array =
- ArrayUtils.toPrimitive(
- Collections.nCopies(sizeOfArray, noDataValue).toArray(new
Double[sizeOfArray]));
- resultRaster.setSamples(0, 0, widthOriginalRaster,
heightOriginalRaster, 0, array);
- }
+ double[] rasterMetadata = RasterAccessors.metadata(raster);
+ int rasterWidth = (int) rasterMetadata[2], rasterHeight = (int)
rasterMetadata[3];
+ Raster rasterData = RasterUtils.getRaster(raster.getRenderedImage());
+
+ // create a new raster and set a default value that's the no-data value
+ String bandType = RasterBandAccessors.getBandType(raster, band);
+ int dataTypeCode =
RasterUtils.getDataTypeCode(RasterBandAccessors.getBandType(raster, band));
+ boolean isDataTypeIntegral = RasterUtils.isDataTypeIntegral(dataTypeCode);
+ WritableRaster writableRaster =
+ RasterFactory.createBandedRaster(dataTypeCode, rasterWidth,
rasterHeight, 1, null);
+ int sizeOfArray = rasterWidth * rasterHeight;
+ if (isDataTypeIntegral) {
+ int[] array =
+ ArrayUtils.toPrimitive(
+ Collections.nCopies(sizeOfArray, (int) noDataValue)
+ .toArray(new Integer[sizeOfArray]));
+ writableRaster.setSamples(0, 0, rasterWidth, rasterHeight, 0, array);
+ } else {
+ double[] array =
+ ArrayUtils.toPrimitive(
+ Collections.nCopies(sizeOfArray, noDataValue).toArray(new
Double[sizeOfArray]));
+ writableRaster.setSamples(0, 0, rasterWidth, rasterHeight, 0, array);
+ }
- // rasterize the geometry to iterate over the clipped raster
- GridCoverage2D rasterized =
- RasterConstructors.asRaster(geometry, raster, bandType, allTouched,
150);
- Raster rasterizedData =
RasterUtils.getRaster(rasterized.getRenderedImage());
- double[] metadataRasterized = RasterAccessors.metadata(rasterized);
- int widthRasterized = (int) metadataRasterized[2],
- heightRasterized = (int) metadataRasterized[3];
-
- for (int j = 0; j < heightRasterized; j++) {
- for (int i = 0; i < widthRasterized; i++) {
- Point2D point = RasterUtils.getWorldCornerCoordinates(rasterized, i,
j);
- int[] rasterCoord =
- RasterUtils.getGridCoordinatesFromWorld(raster, point.getX(),
point.getY());
- int x = Math.abs(rasterCoord[0]), y = Math.abs(rasterCoord[1]);
-
- if (rasterizedData.getPixel(i, j, (int[]) null)[0] == 0) {
- continue;
- }
-
- if (isDataTypeIntegral) {
- int[] pixelValue = rasterData.getPixel(x, y, (int[]) null);
-
- resultRaster.setPixel(x, y, new int[] {pixelValue[band - 1]});
- } else {
- double[] pixelValue = rasterData.getPixel(x, y, (double[]) null);
-
- resultRaster.setPixel(x, y, new double[] {pixelValue[band - 1]});
- }
+ // rasterize the geometry to iterate over the clipped raster
+ GridCoverage2D maskRaster =
+ RasterConstructors.asRaster(geometry, raster, bandType, allTouched,
150);
+ Raster maskData = RasterUtils.getRaster(maskRaster.getRenderedImage());
+ double[] maskMetadata = RasterAccessors.metadata(maskRaster);
+ int maskWidth = (int) maskMetadata[2], maskHeight = (int) maskMetadata[3];
+
+ // Calculate offset
+ Point2D point = RasterUtils.getWorldCornerCoordinates(maskRaster, 1, 1);
+
+ // Add half the pixel length to account for floating-point precision
issues.
+ // This adjustment increases the margin of error because the upper-left
corner of a pixel
+ // is very close to neighboring pixels. Without this adjustment,
floating-point inaccuracies
+ // can cause the calculated world coordinates to incorrectly fall into an
adjacent pixel.
+ // For example, the upperLeftY of a maskRaster pixel might be
243924.000000000001,
+ // while the upperLeftY of the original raster pixel is
243924.000000000000.
+ int[] rasterCoord =
+ RasterUtils.getGridCoordinatesFromWorld(
+ raster, point.getX() + (rasterMetadata[4] / 2), point.getY() +
(rasterMetadata[5] / 2));
+ double offsetX = rasterCoord[0];
+ double offsetY = rasterCoord[1];
+
+ for (int j = 0; j < maskHeight; j++) {
+ for (int i = 0; i < maskWidth; i++) {
+ // Calculate mapped raster index
+ int x = (int) (i + offsetX), y = (int) (j + offsetY);
+
+ // Check if the pixel in the maskRaster data is valid
+ if (maskData.getPixel(i, j, (int[]) null)[0] == 0) {
+ continue;
+ }
+
+ if (isDataTypeIntegral) {
+ int[] pixelValue = rasterData.getPixel(x, y, (int[]) null);
+ writableRaster.setPixel(x, y, new int[] {pixelValue[band - 1]});
+ } else {
+ double[] pixelValue = rasterData.getPixel(x, y, (double[]) null);
+ writableRaster.setPixel(x, y, new double[] {pixelValue[band - 1]});
}
}
- newRaster =
- RasterUtils.clone(
- resultRaster,
- raster.getGridGeometry(),
- newRaster.getSampleDimensions(),
- newRaster,
- noDataValue,
- true);
- } else {
- RenderedImage image = newRaster.getRenderedImage();
- int minX = image.getMinX();
- int minY = image.getMinY();
- if (minX != 0 || minY != 0) {
- newRaster = RasterUtils.shiftRasterToZeroOrigin(newRaster,
noDataValue);
- } else {
- newRaster =
- RasterUtils.clone(
- newRaster.getRenderedImage(),
- newRaster.getGridGeometry(),
- newRaster.getSampleDimensions(),
- newRaster,
- noDataValue,
- true);
- }
+ }
+
+ // Not cropped but clipped
+ GridCoverage2D newRaster =
+ RasterUtils.clone(
+ writableRaster,
+ raster.getGridGeometry(),
+ new GridSampleDimension[] {raster.getSampleDimension(band - 1)},
+ raster,
+ noDataValue,
+ true);
+
+ if (crop) {
+ Geometry maskRasterExtent = GeometryFunctions.envelope(maskRaster);
+ return crop(newRaster, noDataValue, maskRasterExtent, lenient);
}
return newRaster;
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
index 77d115352f..c2dc22782f 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
@@ -29,6 +29,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
import java.util.Base64;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
@@ -37,6 +38,7 @@ import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import org.apache.sedona.common.utils.RasterUtils;
import org.geotools.api.coverage.grid.GridCoverageWriter;
+import org.geotools.api.metadata.spatial.PixelOrientation;
import org.geotools.api.parameter.GeneralParameterValue;
import org.geotools.api.parameter.ParameterValueGroup;
import org.geotools.coverage.grid.GridCoverage2D;
@@ -45,6 +47,7 @@ import org.geotools.gce.arcgrid.ArcGridWriteParams;
import org.geotools.gce.arcgrid.ArcGridWriter;
import org.geotools.gce.geotiff.GeoTiffWriteParams;
import org.geotools.gce.geotiff.GeoTiffWriter;
+import org.geotools.referencing.operation.transform.AffineTransform2D;
public class RasterOutputs {
public static byte[] asGeoTiff(
@@ -294,6 +297,89 @@ public class RasterOutputs {
return Base64.getEncoder().encodeToString(png);
}
+ public static String asMatrixPretty(GridCoverage2D raster, int band, int
precision) {
+ try {
+ RasterUtils.ensureBand(raster, band);
+ RenderedImage img = raster.getRenderedImage();
+ Raster r = RasterUtils.getRaster(img);
+
+ final int minX = img.getMinX();
+ final int minY = img.getMinY();
+ final int width = r.getWidth();
+ final int height = r.getHeight();
+ final int bandIdx = band - 1;
+
+ // Affine transform (upper-left corners)
+ AffineTransform2D at = RasterUtils.getAffineTransform(raster,
PixelOrientation.UPPER_LEFT);
+ final double a = at.getScaleX(), b = at.getShearX(), d = at.getShearY(),
e = at.getScaleY();
+ final double tx = at.getTranslateX(), ty = at.getTranslateY();
+
+ final boolean integral =
RasterUtils.isDataTypeIntegral(r.getDataBuffer().getDataType());
+ final String numPat = precision <= 0 ? "0" : "0." +
"#".repeat(precision);
+ final DecimalFormat df = new DecimalFormat(numPat);
+
+ String[][] cells = new String[height + 1][width + 1];
+ cells[0][0] = "y\\x";
+
+ // Header: use upper-left corner (no +0.5)
+ for (int x = 0; x < width; x++) {
+ double xc = minX + x;
+ double yc = minY;
+ double worldX = a * xc + b * yc + tx;
+ cells[0][x + 1] = "[" + (minX + x) + ", " + df.format(worldX) + "]";
+ }
+
+ // Rows
+ for (int y = 0; y < height; y++) {
+ double yc = minY + y;
+ double xc = minX;
+ double worldY = d * xc + e * yc + ty;
+ cells[y + 1][0] = "[" + (minY + y) + ", " + df.format(worldY) + "]";
+
+ if (integral) {
+ int[] vals = r.getSamples(0, y, width, 1, bandIdx, (int[]) null);
+ for (int x = 0; x < width; x++) cells[y + 1][x + 1] =
String.valueOf(vals[x]);
+ } else {
+ double[] vals = r.getSamples(0, y, width, 1, bandIdx, (double[])
null);
+ for (int x = 0; x < width; x++) cells[y + 1][x + 1] =
df.format(vals[x]);
+ }
+ }
+
+ // --- Pretty print ---
+ int rows = height + 1, cols = width + 1;
+ int[] colWidths = new int[cols];
+ for (int c = 0; c < cols; c++) {
+ int max = 0;
+ for (int rIdx = 0; rIdx < rows; rIdx++) max = Math.max(max,
cells[rIdx][c].length());
+ colWidths[c] = max;
+ }
+
+ StringBuilder out = new StringBuilder();
+ String sep = "+";
+ for (int c = 0; c < cols; c++) sep += "-".repeat(colWidths[c] + 2) + "+";
+ sep += "\n";
+
+ out.append(sep);
+ for (int rIdx = 0; rIdx < rows; rIdx++) {
+ out.append("| ");
+ for (int c = 0; c < cols; c++) {
+ String val = cells[rIdx][c];
+ boolean leftAlign = (c == 0 || rIdx == 0);
+ if (leftAlign) out.append(String.format("%-" + colWidths[c] + "s",
val));
+ else out.append(String.format("%" + colWidths[c] + "s", val));
+ out.append(" | ");
+ }
+ out.append("\n");
+ if (rIdx == 0) out.append(sep);
+ }
+ out.append(sep);
+ return out.toString();
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to format raster matrix", e);
+ }
+ }
+
public static String asMatrix(GridCoverage2D raster, int band, int
postDecimalPrecision) {
RasterUtils.ensureBand(raster, band);
Raster rasterData = RasterUtils.getRaster(raster.getRenderedImage());
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/Rasterization.java
b/common/src/main/java/org/apache/sedona/common/raster/Rasterization.java
index 730272c6e0..f5f32aecff 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/Rasterization.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/Rasterization.java
@@ -87,6 +87,11 @@ public class Rasterization {
boolean allTouched)
throws FactoryException {
+ // For instances where sub geometry is completely outside raster
+ if (geomExtent == null) {
+ return;
+ }
+
switch (geom.getGeometryType()) {
case "GeometryCollection":
case "MultiPolygon":
@@ -303,9 +308,24 @@ public class Rasterization {
return x >= minX && x <= maxX && y >= minY && y <= maxY;
}
- protected static ReferencedEnvelope rasterizeGeomExtent(
+ static ReferencedEnvelope rasterizeGeomExtent(
Geometry geom, GridCoverage2D raster, double[] metadata, boolean
allTouched) {
+ validateRasterMetadata(metadata);
+
+ // Always enable allTouched for MultiLineString and MultiPoint.
+ //
+ // Rationale:
+ // - Points and lines cannot be rasterized using "pixel-center inside
geometry" logic
+ // because they have no area. For these geometry types, we must use "any
touch"
+ // semantics to mark pixels they intersect/touch.
+ // - Single Point/LineString cases are already safeguarded by the
zero-envelope
+ // expansion logic at lines 367:373 (ensuring a non-degenerate search
window).
+ // - For Polygons, whether we expand the snapped extent depends on the
caller’s
+ // allTouched setting (center-based vs any-intersection semantics).
+ //
+ // Enabling allTouched here guarantees correct handling for multi-part
point/line
+ // geometries whose parts may sit exactly on pixel boundaries.
if (Objects.equals(geom.getGeometryType(), "MultiLineString")) {
allTouched = true;
}
@@ -313,57 +333,45 @@ public class Rasterization {
allTouched = true;
}
- ReferencedEnvelope rasterExtent =
+ ReferencedEnvelope geomExtent =
new ReferencedEnvelope(geom.getEnvelopeInternal(),
raster.getCoordinateReferenceSystem2D());
// Using BigDecimal to avoid floating point errors
- BigDecimal upperLeftX = BigDecimal.valueOf(metadata[0]);
- BigDecimal upperLeftY = BigDecimal.valueOf(metadata[1]);
- BigDecimal scaleX = BigDecimal.valueOf(metadata[4]);
- BigDecimal scaleY = BigDecimal.valueOf(metadata[5]);
+ double upperLeftX = metadata[0];
+ double upperLeftY = metadata[1];
+ double scaleX = metadata[4];
+ double scaleY = metadata[5];
// Compute the aligned min/max values
double alignedMinX =
- (scaleX
- .multiply(
- BigDecimal.valueOf(
- Math.floor((rasterExtent.getMinX() + metadata[0]) /
metadata[4])))
- .subtract(upperLeftX))
- .doubleValue();
-
+ toWorldCoordinate(
+ toPixelIndex(geomExtent.getMinX(), scaleX, upperLeftX, true),
scaleX, upperLeftX);
double alignedMinY =
- (scaleY
- .multiply(
- BigDecimal.valueOf(
- Math.ceil((rasterExtent.getMinY() + metadata[1]) /
metadata[5])))
- .subtract(upperLeftY))
- .doubleValue();
-
+ toWorldCoordinate(
+ toPixelIndex(geomExtent.getMinY(), scaleY, upperLeftY, true),
scaleY, upperLeftY);
double alignedMaxX =
- (scaleX
- .multiply(
- BigDecimal.valueOf(
- Math.ceil((rasterExtent.getMaxX() + metadata[0]) /
metadata[4])))
- .subtract(upperLeftX))
- .doubleValue();
-
+ toWorldCoordinate(
+ toPixelIndex(geomExtent.getMaxX(), scaleX, upperLeftX, false),
scaleX, upperLeftX);
double alignedMaxY =
- (scaleY
- .multiply(
- BigDecimal.valueOf(
- Math.floor((rasterExtent.getMaxY() + metadata[1]) /
metadata[5])))
- .subtract(upperLeftY))
- .doubleValue();
-
- // For points and LineStrings at intersection of 2 or more pixels,
- // extend search grid by 1 pixel in each direction
+ toWorldCoordinate(
+ toPixelIndex(geomExtent.getMaxY(), scaleY, upperLeftY, false),
scaleY, upperLeftY);
+
+ // Ensure the snapped AOI window is at least one pixel wide/tall.
+ // After snapping the continuous bbox to pixel edges, a point or thin line
can
+ // collapse to min == max along an axis (i.e., a degenerate
0-width/0-height window).
+ // That would produce an empty search region and skip rasterization
entirely.
+ //
+ // We grow the aligned window by exactly one pixel on each side, using the
+ // pixel size (scaleX > 0, scaleY < 0 for north-up). This yields at least a
+ // 1-pixel-wide/1-pixel-tall search window and guarantees we visit the
pixel(s)
+ // the geometry lies in.
if (alignedMaxX == alignedMinX) {
- alignedMinX -= metadata[4];
- alignedMaxX += metadata[4];
+ alignedMinX -= scaleX;
+ alignedMaxX += scaleX;
}
if (alignedMaxY == alignedMinY) {
- alignedMinY += metadata[5];
- alignedMaxY -= metadata[5];
+ alignedMinY += scaleY;
+ alignedMaxY -= scaleY;
}
// Get the extent of the original raster
@@ -372,15 +380,37 @@ public class Rasterization {
double originalMaxX = raster.getEnvelope().getMaximum(0);
double originalMaxY = raster.getEnvelope().getMaximum(1);
- // Extend the aligned extent by 1 pixel if allTouched is true and if any
geometry extent meets
- // the aligned extent
+ // Quick bbox intersection test for when rasterizeGeomExtent gets called
independently
+ // return null if they do not intersect
+ if (alignedMinX >= originalMaxX
+ || alignedMaxX <= originalMinX
+ || alignedMinY >= originalMaxY
+ || alignedMaxY <= originalMinY) {
+ return null;
+ }
+
+ // Handle "allTouched" behavior when geometry edges line up exactly with
pixel boundaries.
+ //
+ // Normally, each pixel is considered only if its *center* falls inside
the geometry.
+ // But if an edge of the geometry sits exactly on a pixel boundary, the
geometry
+ // might "touch" neighboring pixels without covering their centers — and
those pixels
+ // would be skipped.
+ //
+ // When allTouched = true, we expand the aligned bounding box outward by
one pixel
+ // on any side where the geometry’s edge exactly matches a pixel boundary.
+ // This guarantees we include those neighboring pixels that the geometry
merely touches.
+ //
+ // We only expand sides that line up perfectly with grid lines (equal
coordinates).
+ // The scaleX / scaleY values (which already encode pixel size and
direction) ensure
+ // this expansion moves exactly one pixel outward in each direction.
if (allTouched) {
- alignedMinX -= (rasterExtent.getMinX() == alignedMinX) ? metadata[4] : 0;
- alignedMinY += (rasterExtent.getMinY() == alignedMinY) ? metadata[5] : 0;
- alignedMaxX += (rasterExtent.getMaxX() == alignedMaxX) ? metadata[4] : 0;
- alignedMaxY -= (rasterExtent.getMaxY() == alignedMaxY) ? metadata[5] : 0;
+ alignedMinX -= (geomExtent.getMinX() == alignedMinX) ? scaleX : 0;
+ alignedMinY += (geomExtent.getMinY() == alignedMinY) ? scaleY : 0;
+ alignedMaxX += (geomExtent.getMaxX() == alignedMaxX) ? scaleX : 0;
+ alignedMaxY -= (geomExtent.getMaxY() == alignedMaxY) ? scaleY : 0;
}
+ // Clamp the aligned extent to the original raster extent
alignedMinX = Math.max(alignedMinX, originalMinX);
alignedMinY = Math.max(alignedMinY, originalMinY);
alignedMaxX = Math.min(alignedMaxX, originalMaxX);
@@ -388,16 +418,38 @@ public class Rasterization {
// Create the aligned raster extent
ReferencedEnvelope alignedRasterExtent =
- ReferencedEnvelope.rect(
+ new ReferencedEnvelope(
alignedMinX,
+ alignedMaxX,
alignedMinY,
- alignedMaxX - alignedMinX,
- alignedMaxY - alignedMinY,
- rasterExtent.getCoordinateReferenceSystem());
+ alignedMaxY,
+ geomExtent.getCoordinateReferenceSystem());
return alignedRasterExtent;
}
+ private static double toPixelIndex(double coord, double scale, double
upperLeft, boolean isMin) {
+ BigDecimal rel =
BigDecimal.valueOf(coord).subtract(BigDecimal.valueOf(upperLeft));
+
+ BigDecimal px = rel.divide(BigDecimal.valueOf(scale), 16,
RoundingMode.FLOOR);
+ if (scale > 0) {
+ return isMin
+ ? px.setScale(0, RoundingMode.FLOOR).doubleValue()
+ : px.setScale(0, RoundingMode.CEILING).doubleValue();
+ } else {
+ return isMin
+ ? px.setScale(0, RoundingMode.CEILING).doubleValue()
+ : px.setScale(0, RoundingMode.FLOOR).doubleValue();
+ }
+ }
+
+ private static double toWorldCoordinate(double pixelIndex, double scale,
double upperLeft) {
+ return BigDecimal.valueOf(pixelIndex)
+ .multiply(BigDecimal.valueOf(scale))
+ .add(BigDecimal.valueOf(upperLeft))
+ .doubleValue();
+ }
+
private static RasterizationParams calculateRasterizationParams(
GridCoverage2D raster,
boolean useGeometryExtent,
@@ -574,7 +626,7 @@ public class Rasterization {
// calculating slope
for (double y = yStart; y >= yEnd; y--) {
double xIntercept = p1X; // Vertical line, xIntercept is constant
- if (xIntercept < 0 || xIntercept >=
params.writableRaster.getWidth()) {
+ if (xIntercept < 0 || xIntercept >
params.writableRaster.getWidth()) {
continue; // Skip xIntercepts outside geomExtent
}
scanlineIntersections.computeIfAbsent(y, k -> new
TreeSet<>()).add(xIntercept);
@@ -583,6 +635,7 @@ public class Rasterization {
double slope = (worldP2.y - worldP1.y) / (worldP2.x - worldP1.x);
double xMin = (geomExtent.getMinX() - params.upperLeftX) /
params.scaleX;
double xMax = (geomExtent.getMaxX() - params.upperLeftX) /
params.scaleX;
+
for (double y = yStart; y >= yEnd; y--) {
double xIntercept = p1X + ((p1Y - y) / slope);
if ((xIntercept < xMin) || (xIntercept >= xMax)) {
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/FunctionEditorsTest.java
b/common/src/test/java/org/apache/sedona/common/raster/FunctionEditorsTest.java
index 37c6d4ed73..c6310e8371 100644
---
a/common/src/test/java/org/apache/sedona/common/raster/FunctionEditorsTest.java
+++
b/common/src/test/java/org/apache/sedona/common/raster/FunctionEditorsTest.java
@@ -68,8 +68,8 @@ public class FunctionEditorsTest extends RasterTestBase {
expected =
new double[] {
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0, 10.0,
10.0, 10.0, 10.0, 10.0,
- 10.0, 0.0, 0.0, 0.0, 10.0, 10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 10.0,
- 10.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
+ 10.0, 0.0, 0.0, 0.0, 10.0, 10.0, 0.0, 0.0, 0.0, 10.0, 10.0, 0.0,
0.0, 0.0, 0.0, 0.0, 10.0,
+ 10.0, 0.0, 10.0, 10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 10.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 10.0,
10.0, 10.0, 10.0, 0.0,
0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
10.0, 10.0, 10.0, 10.0,
10.0, 10.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/RasterBandAccessorsTest.java
b/common/src/test/java/org/apache/sedona/common/raster/RasterBandAccessorsTest.java
index d953d91ece..0eaea40341 100644
---
a/common/src/test/java/org/apache/sedona/common/raster/RasterBandAccessorsTest.java
+++
b/common/src/test/java/org/apache/sedona/common/raster/RasterBandAccessorsTest.java
@@ -182,6 +182,26 @@ public class RasterBandAccessorsTest extends
RasterTestBase {
raster, nonIntersectingGeom, 1, "sum", false, false, false));
}
+ @Test
+ public void testZonalStatsWithMultiPolygonAndAllNoDataValues()
+ throws IOException, ParseException, FactoryException {
+ GridCoverage2D raster = rasterFromGeoTiff(resourceFolder +
"allNoData.tiff");
+ String polygon =
+ "SRID=2263;MULTIPOLYGON (((994691.8668410989 222732.55147949007,
994838.9964562772 222657.33114599282, 994835.8129632992 222651.32007158513,
994866.5497303953 222635.3891649635, 994894.9626119346 222620.6621292782,
994897.9281735104 222627.20212832885, 994912.1027229384 222619.95519748307,
994956.740641462 222597.13404523468, 994906.6541878665 222501.29768674026,
994887.6889911637 222465.0093761085, 994837.7340744453 222492.35408672754,
994853.625535307 222529.50849369404, 994840 [...]
+ Geometry geom = Constructors.geomFromEWKT(polygon);
+
+ Double actual = RasterBandAccessors.getZonalStats(raster, geom, 1,
"count", false, false);
+ double expected = 20470.0;
+ assertEquals(expected, actual, 0d);
+
+ actual = RasterBandAccessors.getZonalStats(raster, geom, 1, "sum", false,
false);
+ expected = 0;
+ assertEquals(expected, actual, 0d);
+
+ actual = RasterBandAccessors.getZonalStats(raster, geom, 1, "mean", false,
true);
+ assertNull(actual);
+ }
+
@Test
public void testZonalStatsWithNoData() throws IOException, FactoryException,
ParseException {
GridCoverage2D raster =
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 c46dd601b0..9b637c54a4 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
@@ -30,11 +30,11 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.sedona.common.Constructors;
+import org.apache.sedona.common.FunctionsGeoTools;
import org.apache.sedona.common.raster.serde.Serde;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.operation.TransformException;
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;
@@ -217,12 +217,70 @@ public class RasterBandEditorsTest extends RasterTestBase
{
double bandNoDataValue =
RasterBandAccessors.getBandNoDataValue(clippedRaster);
double expectedBandNoDataValue = 0.0;
double[] actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
- double[] expectedValues = {2.0, 3.0, 6.0, 7.0};
+ double[] expectedValues = {0.0, 0.0, 0.0, 7.0};
+
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertTrue(Arrays.equals(expectedValues, actualValues));
+
+ Geometry geomTransformed = FunctionsGeoTools.transform(geom, "EPSG:5070",
"EPSG:4326");
+
+ clippedRaster = RasterBandEditors.clip(raster, 1, geomTransformed, false,
0, true);
+ bandNoDataValue = RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ expectedBandNoDataValue = 0.0;
+ actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+ expectedValues = new double[] {0.0, 0.0, 0.0, 7.0};
assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
assertTrue(Arrays.equals(expectedValues, actualValues));
}
+ @Test
+ public void testClipEdgeCase()
+ throws FactoryException, TransformException, ParseException, IOException
{
+ GridCoverage2D raster =
+ rasterFromGeoTiff(resourceFolder +
"rasterization/test_rasterization.tiff");
+ String polygon =
+ "POLYGON ((-112.33284156960647 35.2847218363903, -111.83787460663726
32.58335532290123, -110.70619885787484 32.647569129204015, -110.29612639967809
30.671467842387234, -108.19258739561617 31.285215692431052, -107.96086869190255
34.366572252095835, -108.22326988847186 35.68843747308277, -110.52624509609558
35.31045505826484, -112.33284156960647 35.2847218363903))";
+ Geometry geom = Constructors.geomFromWKT(polygon,
RasterAccessors.srid(raster));
+ GridCoverage2D clippedRaster = RasterBandEditors.clip(raster, 1, geom,
true, 0, true);
+
+ double bandNoDataValue =
RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ double expectedBandNoDataValue = 0.0;
+ double[] actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+ double[] expectedValues = {
+ 12.0, 13.0, 14.0, 15.0, 16.0, 0.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0,
32.0, 33.0, 34.0, 35.0,
+ 36.0, 37.0, 0.0, 43.0, 44.0, 45.0, 46.0, 47.0, 0.0, 0.0, 54.0, 55.0,
56.0, 0.0, 0.0, 0.0,
+ 64.0, 65.0, 0.0, 0.0
+ };
+
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertArrayEquals(expectedValues, actualValues, 0.0);
+ }
+
+ @Test
+ public void testClipWithClippedGeometryWithHole()
+ throws FactoryException, TransformException, ParseException, IOException
{
+ GridCoverage2D raster =
+ rasterFromGeoTiff(resourceFolder +
"rasterization/test_rasterization.tiff");
+ String polygon =
+ "POLYGON ((-116.974798 27.410563, -113.10645 25.244469, -110.249147
27.566499, -110.205189 28.420171, -111.392068 29.036741, -109.28206 29.420242,
-107.831429 28.49744, -108.446848 27.566499, -107.391844 25.720509, -102.995994
24.08636, -99.479314 27.722214, -112.183321 32.063743, -116.974798 27.410563))";
+ Geometry geom = Constructors.geomFromWKT(polygon, 4326);
+ GridCoverage2D clippedRaster = RasterBandEditors.clip(raster, 1, geom,
true, 0, true);
+
+ double bandNoDataValue =
RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ double expectedBandNoDataValue = 0.0;
+ double[] actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+ double[] expectedValues = {
+ 0.0, 42.0, 43.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 51.0, 52.0, 53.0,
54.0, 55.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 0.0, 0.0,
71.0, 72.0, 73.0,
+ 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 0.0,
86.0, 87.0, 88.0, 89.0,
+ 90.0, 91.0, 92.0, 93.0, 94.0, 0.0, 96.0, 97.0, 98.0, 99.0, 100.0
+ };
+
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertArrayEquals(expectedValues, actualValues, 0.0);
+ }
+
@Test
public void testClip()
throws IOException, FactoryException, TransformException, ParseException,
@@ -233,11 +291,17 @@ public class RasterBandEditorsTest extends RasterTestBase
{
"POLYGON ((236722 4204770, 243900 4204770, 243900 4197590, 221170
4197590, 236722 4204770))";
Geometry geom = Constructors.geomFromWKT(polygon,
RasterAccessors.srid(raster));
+ // Testing red band without crop
GridCoverage2D clippedRaster = RasterBandEditors.clip(raster, 1, geom,
false, 200, false);
- double[] clippedMetadata =
- Arrays.stream(RasterAccessors.metadata(clippedRaster), 0, 9).toArray();
- double[] originalMetadata =
Arrays.stream(RasterAccessors.metadata(raster), 0, 9).toArray();
- assertArrayEquals(originalMetadata, clippedMetadata, 0.01d);
+
+ double[] clippedMetadata = RasterAccessors.metadata(clippedRaster);
+ double[] originalMetadata = RasterAccessors.metadata(raster);
+ assertArrayEquals(
+ Arrays.stream(originalMetadata, 0, 9).toArray(),
+ Arrays.stream(clippedMetadata, 0, 9).toArray(),
+ 0.01d);
+
+ assertEquals(1, clippedMetadata[9], FP_TOLERANCE);
String actual = String.valueOf(clippedRaster.getSampleDimensions()[0]);
String expected =
@@ -254,7 +318,9 @@ public class RasterBandEditorsTest extends RasterTestBase {
Double[] expectedValues = new Double[] {null, null, 0.0, 0.0, null};
assertTrue(Arrays.equals(expectedValues, actualValues));
- GridCoverage2D croppedRaster = RasterBandEditors.clip(raster, 1, geom,
false, 200, true);
+ // Testing green band with crop
+ GridCoverage2D croppedRaster = RasterBandEditors.clip(raster, 2, geom,
false, 200, true);
+
assertEquals(0, croppedRaster.getRenderedImage().getMinX());
assertEquals(0, croppedRaster.getRenderedImage().getMinY());
GridCoverage2D croppedRaster2 =
Serde.deserialize(Serde.serialize(croppedRaster));
@@ -265,9 +331,25 @@ public class RasterBandEditorsTest extends RasterTestBase {
points.add(Constructors.geomFromWKT("POINT(237201 4.20429e+06)", 26918));
points.add(Constructors.geomFromWKT("POINT(237919 4.20357e+06)", 26918));
points.add(Constructors.geomFromWKT("POINT(223802 4.20465e+06)", 26918));
+
+ actual = String.valueOf(croppedRaster.getSampleDimensions()[0]);
+ expected =
+ "RenderedSampleDimension(\"GREEN_BAND\":[200.0 ... 200.0])\n"
+ + " ‣ Category(\"No data\":[200...200])\n";
+ assertEquals(expected, actual);
+
+ double[] croppedMetadata = RasterAccessors.metadata(clippedRaster);
+ originalMetadata = RasterAccessors.metadata(raster);
+ assertArrayEquals(
+ Arrays.stream(originalMetadata, 0, 9).toArray(),
+ Arrays.stream(croppedMetadata, 0, 9).toArray(),
+ 0.01d);
+
+ assertEquals(1, croppedMetadata[9], FP_TOLERANCE);
+
actualValues = PixelFunctions.values(croppedRaster, points, 1).toArray(new
Double[0]);
- expectedValues = new Double[] {0.0, 0.0, 0.0, 0.0, 255.0};
- assertTrue(Arrays.equals(expectedValues, actualValues));
+ expectedValues = new Double[] {85.0, 85.0, 127.0, 212.0, null};
+ assertArrayEquals(expectedValues, actualValues);
}
@Test
@@ -283,8 +365,10 @@ public class RasterBandEditorsTest extends RasterTestBase {
0);
// Throws an exception in non-lenient mode
+ // Throws "Geometry does not intersect Raster." instead - emitted by
Rasterization.rasterize()
+ // Should Rasterization.rasterize() have a lenient mode as well?
assertThrows(
- CannotCropException.class,
+ IllegalArgumentException.class,
() -> RasterBandEditors.clip(raster, 1, nonIntersectingGeom, false,
200, false, false));
// Returns null in lenient mode
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
b/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
index f42d4acc5e..a5cc02aada 100644
---
a/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
+++
b/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
@@ -612,7 +612,7 @@ public class RasterConstructorsTest extends RasterTestBase {
if (offsetX + x < width && offsetY + y < height) {
float[] expectedValues = gridCoverage2D.evaluate(gridCoord,
(float[]) null);
if (bandIndices == null) {
- Assert.assertArrayEquals(expectedValues, values, 1e-6f);
+ assertArrayEquals(expectedValues, values, 1e-6f);
} else {
Assert.assertEquals(bandIndices.length, values.length);
for (int j = 0; j < bandIndices.length; j++) {
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/RasterizationTests.java
b/common/src/test/java/org/apache/sedona/common/raster/RasterizationTests.java
new file mode 100644
index 0000000000..9d03220c6e
--- /dev/null
+++
b/common/src/test/java/org/apache/sedona/common/raster/RasterizationTests.java
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+package org.apache.sedona.common.raster;
+
+import static org.apache.sedona.common.raster.RasterAccessors.metadata;
+
+import org.geotools.api.referencing.FactoryException;
+import org.geotools.api.referencing.operation.TransformException;
+import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.geometry.jts.ReferencedEnvelope;
+import org.junit.Assert;
+import org.junit.Test;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.io.ParseException;
+import org.locationtech.jts.io.WKTReader;
+
+public class RasterizationTests extends RasterTestBase {
+ private final WKTReader wktReader = new WKTReader();
+
+ @Test
+ public void testRasterizeGeomExtent()
+ throws ParseException, FactoryException, TransformException {
+ GridCoverage2D testRaster = RasterConstructors.makeEmptyRaster(1, "F", 4,
4, 4, 4, 1);
+ double[] metadata = metadata(testRaster);
+
+ // Grid aligned polygon completely contained within raster extent
+ String wktPolygon = "POLYGON ((5 2, 6 1, 5 3, 7 3, 5 2))";
+ validateRasterizeGeomExtent(
+ wktPolygon, new double[] {5.0, 7.0, 1.0, 3.0}, testRaster, metadata,
false);
+
+ // Grid aligned polygon in line with raster extent
+ wktPolygon = "POLYGON ((4 2, 6 0, 8 2, 6 4, 4 2))";
+ validateRasterizeGeomExtent(
+ wktPolygon, new double[] {4.0, 8.0, 0.0, 4.0}, testRaster, metadata,
false);
+
+ // Polygon partially outside raster extent
+ wktPolygon = "POLYGON ((3 1, 5 -1, 8 2, 6 4, 3 1))";
+ validateRasterizeGeomExtent(
+ wktPolygon, new double[] {4.0, 8.0, 0.0, 4.0}, testRaster, metadata,
false);
+
+ // Polygon completely outside raster extent
+ wktPolygon = "POLYGON ((-1 -1, 0 -2, 1 -1, 0 0, -1 -1))";
+ validateRasterizeGeomExtent(wktPolygon, null, testRaster, metadata, false);
+
+ // Partial pixel alignment
+ String wktPolygon5 = "POLYGON ((5.5 2.5, 4.5 0.5, 6.5 1.5, 5.5 2.5))";
+ validateRasterizeGeomExtent(
+ wktPolygon5, new double[] {4.0, 7.0, 0.0, 3.0}, testRaster, metadata,
false);
+
+ GridCoverage2D testRaster_frac =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, 4.3333, 4.6666, 0.3333, -0.3333, 0, 0, 4326);
+ double[] metadata_frac = metadata(testRaster_frac);
+
+ // Grid aligned polygon completely contained within raster extent
+ wktPolygon = "Polygon((4.6666 4.0, 4.9999 3.7777, 5.3332 4.0, 4.9999
4.3333, 4.6666 4.0))";
+ validateRasterizeGeomExtent(
+ wktPolygon,
+ new double[] {4.6666, 5.3332, 3.6667, 4.3333},
+ testRaster_frac,
+ metadata_frac,
+ false);
+
+ // Partial pixel alignment
+ wktPolygon = "Polygon((4.7 3.9, 4.9999 3.9, 5.1 4.0, 4.9999 4.2, 4.7
3.9))";
+ validateRasterizeGeomExtent(
+ wktPolygon,
+ new double[] {4.6666, 5.3332, 3.6667, 4.3333},
+ testRaster_frac,
+ metadata_frac,
+ false);
+
+ // polygon larger than raster extent
+ wktPolygon = "Polygon((4.0 3.0, 4.0 2.0, 8.0 2.0, 8.0 5.0, 4.0 5.0, 4.0
3.0))";
+ validateRasterizeGeomExtent(
+ wktPolygon,
+ new double[] {4.3333, 5.6665, 3.3334, 4.6666},
+ testRaster_frac,
+ metadata_frac,
+ false);
+
+ GridCoverage2D testRaster_neg =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, -4.3333, -4.6666, 0.3333, -0.3333, 0, 0, 4326);
+ double[] metadata_neg = metadata(testRaster_neg);
+
+ // polygon larger than raster extent
+ wktPolygon = "Polygon((-8.0 -2.0, -2.0 -2.0, -2.0 -6.0, -8.0 -6.0, -8.0
-2.0))";
+ validateRasterizeGeomExtent(
+ wktPolygon,
+ new double[] {-4.3333, -3.0001, -5.9998, -4.6666},
+ testRaster_neg,
+ metadata_neg,
+ false);
+
+ // horizontal line
+ String wktLine = "LINESTRING (5.0 2.0, 6.0 2.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {5.0, 6.0, 1.0, 3.0}, testRaster, metadata,
false);
+
+ // vertical line
+ wktLine = "LINESTRING (5.0 2.0, 5.0 3.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 6.0, 2.0, 3.0}, testRaster, metadata,
false);
+
+ // diagonal line
+ wktLine = "LINESTRING (5.0 2.0, 6.0 3.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {5.0, 6.0, 2.0, 3.0}, testRaster, metadata,
false);
+
+ // point tests
+ String wktPoint = "POINT (5.0 2.0)";
+ validateRasterizeGeomExtent(
+ wktPoint, new double[] {4.0, 6.0, 1.0, 3.0}, testRaster, metadata,
false);
+
+ // intersecting 2 pixels
+ wktPoint = "POINT (5.0 2.5)";
+ validateRasterizeGeomExtent(
+ wktPoint, new double[] {4.0, 6.0, 2.0, 3.0}, testRaster, metadata,
false);
+
+ // within pixel
+ wktPoint = "POINT (5.25 2.25)";
+ validateRasterizeGeomExtent(
+ wktPoint, new double[] {5.0, 6.0, 2.0, 3.0}, testRaster, metadata,
false);
+ }
+
+ private void validateRasterizeGeomExtent(
+ String wkt,
+ double[] expectedEnvelope,
+ GridCoverage2D raster,
+ double[] metadata,
+ boolean allTouched)
+ throws ParseException {
+ Geometry geom = wktReader.read(wkt);
+ ReferencedEnvelope envelope =
+ Rasterization.rasterizeGeomExtent(geom, raster, metadata, allTouched);
+ if (expectedEnvelope == null) {
+ Assert.assertNull(envelope);
+ } else {
+ Assert.assertEquals(expectedEnvelope[0], envelope.getMinX(),
FP_TOLERANCE);
+ Assert.assertEquals(expectedEnvelope[1], envelope.getMaxX(),
FP_TOLERANCE);
+ Assert.assertEquals(expectedEnvelope[2], envelope.getMinY(),
FP_TOLERANCE);
+ Assert.assertEquals(expectedEnvelope[3], envelope.getMaxY(),
FP_TOLERANCE);
+ }
+ }
+}
diff --git a/spark/common/src/test/resources/allNoData.tiff
b/spark/common/src/test/resources/allNoData.tiff
new file mode 100644
index 0000000000..2f5623148f
Binary files /dev/null and b/spark/common/src/test/resources/allNoData.tiff
differ
diff --git
a/spark/common/src/test/resources/rasterization/test_rasterization.tiff
b/spark/common/src/test/resources/rasterization/test_rasterization.tiff
new file mode 100644
index 0000000000..0904a95ff2
Binary files /dev/null and
b/spark/common/src/test/resources/rasterization/test_rasterization.tiff differ