You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by ji...@apache.org on 2026/05/17 07:01:44 UTC
(sedona) branch master updated: [SEDONA-754] feat: make rasterization orientation-agnostic by supporting positive scaleY (#2961)
This is an automated email from the ASF dual-hosted git repository.
jiayuasu 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 c7d905c6b7 [SEDONA-754] feat: make rasterization orientation-agnostic by supporting positive scaleY (#2961)
c7d905c6b7 is described below
commit c7d905c6b745128c7256388ac6e824f9f6247214
Author: Pranav Toggi <pr...@gmail.com>
AuthorDate: Sun May 17 00:01:40 2026 -0700
[SEDONA-754] feat: make rasterization orientation-agnostic by supporting positive scaleY (#2961)
---
.../apache/sedona/common/raster/Rasterization.java | 88 +++++++---
.../apache/sedona/common/utils/RasterUtils.java | 166 +++++++++++++++++++
.../common/raster/RasterBandAccessorsTest.java | 21 +++
.../common/raster/RasterBandEditorsTest.java | 143 +++++++++++++++-
.../common/raster/RasterConstructorsTest.java | 181 +++++++++++++++++++++
.../sedona/common/raster/RasterizationTests.java | 160 +++++++++++++++++-
.../sedona/common/utils/RasterUtilsTest.java | 129 +++++++++++++++
docs/api/sql/Raster-Operators/RS_AsRaster.md | 1 -
.../rasterization/test_rasterization.tiff | Bin 524672 -> 1218 bytes
9 files changed, 860 insertions(+), 29 deletions(-)
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 f5f32aecff..b70b8e9665 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
@@ -18,6 +18,9 @@
*/
package org.apache.sedona.common.raster;
+import static org.apache.sedona.common.utils.RasterUtils.getRasterScaleY;
+import static org.apache.sedona.common.utils.RasterUtils.getRasterUpperLeftY;
+
import java.awt.image.WritableRaster;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -69,6 +72,11 @@ public class Rasterization {
rasterized = coverageFactory.create("rasterized", params.writableRaster, rasterExtent);
}
+ // If original raster is bottom-up, flip the rasterized result vertically
+ if (params.bottomUp) {
+ rasterized = RasterUtils.flipVerticallyGridSpace(rasterized);
+ }
+
// Return results compatible with the original function
List<Object> objects = new ArrayList<>();
objects.add(params.writableRaster);
@@ -136,16 +144,22 @@ public class Rasterization {
int x = startX;
int y = startY;
- for (double worldY = geomExtent.getMinY();
- worldY < geomExtent.getMaxY();
- worldY += params.scaleY, y++) {
+ for (double worldY = geomExtent.getMaxY();
+ worldY > geomExtent.getMinY();
+ worldY += params.scaleY, y--) {
x = startX;
for (double worldX = geomExtent.getMinX();
worldX < geomExtent.getMaxX();
worldX += params.scaleX, x++) {
- // Flip y-axis (since raster Y starts from top-left)
- int yIndex = -y - 1;
+ // Make zero indexed
+ int yIndex = y - 1;
+
+ // Adjust for bottom-up rasters
+ // Reverse the y index
+ if (params.bottomUp) {
+ yIndex = params.writableRaster.getHeight() - 1 - yIndex;
+ }
// Create envelope for this pixel
double cellMaxX = worldX + params.scaleX;
@@ -182,9 +196,9 @@ public class Rasterization {
}
double x0 = (start.x - params.upperLeftX) / params.scaleX;
- double y0 = (params.upperLeftY - start.y) / params.scaleY;
+ double y0 = (start.y - params.upperLeftY) / params.scaleY;
double x1 = (end.x - params.upperLeftX) / params.scaleX;
- double y1 = (params.upperLeftY - end.y) / params.scaleY;
+ double y1 = (end.y - params.upperLeftY) / params.scaleY;
// Apply Bresenham for this segment
drawLineBresenham(params, x0, y0, x1, y1, value, 0.2);
@@ -221,6 +235,12 @@ public class Rasterization {
int rasterX = (int) (Math.floor(x));
int rasterY = (int) (Math.floor(y));
+ // Adjust for bottom-up rasters
+ // Reverse the y index
+ if (params.bottomUp) {
+ rasterY = params.writableRaster.getHeight() - 1 - rasterY;
+ }
+
// Only write if within raster bounds
if (rasterX >= 0
&& rasterX < params.writableRaster.getWidth()
@@ -338,9 +358,9 @@ public class Rasterization {
// Using BigDecimal to avoid floating point errors
double upperLeftX = metadata[0];
- double upperLeftY = metadata[1];
+ double upperLeftY = getRasterUpperLeftY(metadata);
double scaleX = metadata[4];
- double scaleY = metadata[5];
+ double scaleY = getRasterScaleY(metadata);
// Compute the aligned min/max values
double alignedMinX =
@@ -464,13 +484,14 @@ public class Rasterization {
upperLeftY = geomExtent.getMaxY();
} else {
upperLeftX = metadata[0];
- upperLeftY = metadata[1];
+ upperLeftY = getRasterUpperLeftY(metadata);
}
WritableRaster writableRaster;
if (useGeometryExtent) {
int geomExtentWidth = (int) (Math.round(geomExtent.getWidth() / metadata[4]));
- int geomExtentHeight = (int) (Math.round(geomExtent.getHeight() / -metadata[5]));
+ int geomExtentHeight =
+ (int) (Math.round(geomExtent.getHeight() / -getRasterScaleY(metadata)));
writableRaster =
RasterFactory.createBandedRaster(
@@ -483,19 +504,30 @@ public class Rasterization {
RasterUtils.getDataTypeCode(pixelType), rasterWidth, rasterHeight, 1, null);
}
+ boolean bottomUp = metadata[5] > 0;
+
return new RasterizationParams(
- writableRaster, pixelType, metadata[4], -metadata[5], upperLeftX, upperLeftY);
+ writableRaster,
+ pixelType,
+ metadata[4],
+ getRasterScaleY(metadata),
+ upperLeftX,
+ upperLeftY,
+ bottomUp);
}
private static void validateRasterMetadata(double[] rasterMetadata) {
if (rasterMetadata[4] < 0) {
- throw new IllegalArgumentException("ScaleX cannot be negative");
- }
- if (rasterMetadata[5] > 0) {
- throw new IllegalArgumentException("ScaleY must be negative");
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid raster metadata: scaleX is negative (%.2f). Right-to-left rasters are not supported.",
+ rasterMetadata[4]));
}
if (rasterMetadata[6] != 0 || rasterMetadata[7] != 0) {
- throw new IllegalArgumentException("SkewX and SkewY must be zero");
+ throw new IllegalArgumentException(
+ String.format(
+ "Invalid raster metadata: skewX is %.2f and skewY is %.2f. Both values must be zero.",
+ rasterMetadata[6], rasterMetadata[7]));
}
}
@@ -506,6 +538,7 @@ public class Rasterization {
double scaleY;
double upperLeftX;
double upperLeftY;
+ boolean bottomUp;
RasterizationParams(
WritableRaster writableRaster,
@@ -513,13 +546,15 @@ public class Rasterization {
double scaleX,
double scaleY,
double upperLeftX,
- double upperLeftY) {
+ double upperLeftY,
+ boolean bottomUp) {
this.writableRaster = writableRaster;
this.pixelType = pixelType;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.upperLeftX = upperLeftX;
this.upperLeftY = upperLeftY;
+ this.bottomUp = bottomUp;
}
}
@@ -602,15 +637,15 @@ public class Rasterization {
// Using BigDecimal to avoid floating point errors
double yStart =
Math.round(
- (BigDecimal.valueOf(params.upperLeftY)
- .subtract(BigDecimal.valueOf(worldP1.y))
+ (BigDecimal.valueOf(worldP1.y)
+ .subtract(BigDecimal.valueOf(params.upperLeftY))
.divide(BigDecimal.valueOf(params.scaleY), RoundingMode.CEILING))
.doubleValue());
double yEnd =
Math.round(
- (BigDecimal.valueOf(params.upperLeftY)
- .subtract(BigDecimal.valueOf(worldP2.y))
+ (BigDecimal.valueOf(worldP2.y)
+ .subtract(BigDecimal.valueOf(params.upperLeftY))
.divide(BigDecimal.valueOf(params.scaleY), RoundingMode.FLOOR))
.doubleValue());
@@ -619,7 +654,7 @@ public class Rasterization {
yStart = Math.min((params.writableRaster.getHeight() - 0.5), Math.abs(yStart) - 0.5);
double p1X = (worldP1.x - params.upperLeftX) / params.scaleX;
- double p1Y = (params.upperLeftY - worldP1.y) / params.scaleY;
+ double p1Y = (worldP1.y - params.upperLeftY) / params.scaleY;
if (worldP1.x == worldP2.x) {
// Vertical line case: directly set xIntercept. Avoid divide by zero error when
@@ -695,6 +730,13 @@ public class Rasterization {
for (Map.Entry<Integer, List<int[]>> entry : scanlineFillRanges.entrySet()) {
int y = entry.getKey();
+
+ // Adjust for bottom-up rasters
+ // Reverse the y index
+ if (params.bottomUp) {
+ y = params.writableRaster.getHeight() - 1 - y;
+ }
+
for (int[] range : entry.getValue()) {
if (range.length == 1) {
params.writableRaster.setSample(range[0], y, 0, value);
diff --git a/common/src/main/java/org/apache/sedona/common/utils/RasterUtils.java b/common/src/main/java/org/apache/sedona/common/utils/RasterUtils.java
index f962bb9260..6113ee943d 100644
--- a/common/src/main/java/org/apache/sedona/common/utils/RasterUtils.java
+++ b/common/src/main/java/org/apache/sedona/common/utils/RasterUtils.java
@@ -984,4 +984,170 @@ public class RasterUtils {
false);
return modifiedRaster;
}
+
+ /**
+ * This function calculates the upper-left Y world coordinate of the raster. It accounts for both
+ * bottom-up and top-down raster conventions.
+ *
+ * <p>For bottom-up rasters (metadata[5] > 0), the upper-left Y world coordinate is directly taken
+ * from metadata[1]. - For top-down rasters (metadata[5] < 0), the upper-left Y coordinate is
+ * adjusted by adding the product of the raster height and the scale factor (metadata[5]).
+ *
+ * @param metadata The metadata array containing raster properties.
+ * @return The upper-left Y world coordinate of the raster.
+ */
+ public static double getRasterUpperLeftY(double[] metadata) {
+ if (metadata[5] < 0) {
+ return metadata[1];
+ } else {
+ int rasterHeight = (int) metadata[3];
+ return metadata[1] + (rasterHeight * metadata[5]);
+ }
+ }
+
+ /**
+ * This function calculates the Y scale factor of the raster. It ensures that the scale factor is
+ * always negative, regardless of the raster convention.
+ *
+ * <p>For bottom-up rasters (metadata[5] > 0), the scale factor is returned as-is. - For top-down
+ * rasters (metadata[5] < 0), the scale factor is negated to maintain consistency.
+ *
+ * @param metadata The metadata array containing raster properties.
+ * @return The Y scale factor of the raster.
+ */
+ public static double getRasterScaleY(double[] metadata) {
+ if (metadata[5] < 0) {
+ return metadata[5];
+ } else {
+ return -metadata[5];
+ }
+ }
+
+ /**
+ * GRID SPACE: Grid space refers to the raster's index space (col, row). Pixel values remain at
+ * the same array indices, but the grid-to-CRS affine transform is rewritten.
+ *
+ * <p>EFFECT: - The raster is reinterpreted spatially (top-down ↔ bottom-up). - Pixel values are
+ * NOT moved in memory. - Pixel world coordinates DO change. - The world envelope (bounding box)
+ * is preserved.
+ *
+ * <p>Use this when you want to change how the raster is oriented in CRS space without touching
+ * the pixel data.
+ */
+ public static GridCoverage2D flipVerticallyGridSpace(GridCoverage2D raster) {
+
+ GridGeometry2D gridGeometry = raster.getGridGeometry();
+
+ MathTransform mt = gridGeometry.getGridToCRS2D();
+ if (!(mt instanceof AffineTransform2D)) {
+ throw new UnsupportedOperationException(
+ "flipVertically only supports AffineTransform2D grid-to-CRS transforms");
+ }
+
+ AffineTransform2D affine = (AffineTransform2D) mt;
+
+ RenderedImage image = raster.getRenderedImage();
+ int height = image.getHeight();
+
+ /*
+ * Affine transform definition (GeoTools / GDAL compatible):
+ *
+ * worldX = scaleX * col + shearX * row + translateX
+ * worldY = shearY * col + scaleY * row + translateY
+ */
+ double scaleX = affine.getScaleX();
+ double shearX = affine.getShearX();
+ double shearY = affine.getShearY();
+ double scaleY = affine.getScaleY();
+ double translateX = affine.getTranslateX();
+ double translateY = affine.getTranslateY();
+
+ /*
+ * Apply grid-space vertical flip:
+ *
+ * row = (H - 1) - row'
+ */
+ double newShearX = -shearX;
+ double newScaleY = -scaleY;
+
+ double newTranslateX = translateX + shearX * (height - 1);
+ double newTranslateY = translateY + scaleY * (height - 1);
+
+ AffineTransform2D flippedAffine =
+ new AffineTransform2D(scaleX, shearY, newShearX, newScaleY, newTranslateX, newTranslateY);
+
+ GridGeometry2D newGridGeometry =
+ new GridGeometry2D(
+ gridGeometry.getGridRange(),
+ flippedAffine,
+ gridGeometry.getCoordinateReferenceSystem());
+
+ return RasterUtils.clone(
+ raster.getRenderedImage(),
+ newGridGeometry,
+ raster.getSampleDimensions(),
+ raster,
+ null,
+ true);
+ }
+
+ /**
+ * PIXEL SPACE: Pixel space refers to pixel values anchored at their world coordinates. Pixel
+ * values are physically rearranged so that each value remains at the same CRS location.
+ *
+ * <p>EFFECT: - Raster rows are reversed in memory. - The affine transform is updated accordingly.
+ * - Pixel world coordinates are preserved. - The world envelope (bounding box) is preserved.
+ *
+ * <p>Use this when pixel values must remain fixed at their geographic locations.
+ */
+ public static GridCoverage2D flipVerticallyPixelSpace(GridCoverage2D raster) {
+ GridGeometry2D gridGeometry = raster.getGridGeometry();
+
+ MathTransform mt = gridGeometry.getGridToCRS2D();
+ if (!(mt instanceof AffineTransform2D)) {
+ throw new UnsupportedOperationException(
+ "flipVertically only supports AffineTransform2D grid-to-CRS transforms");
+ }
+
+ AffineTransform2D affine = (AffineTransform2D) mt;
+
+ RenderedImage image = raster.getRenderedImage();
+ int height = image.getHeight();
+
+ // Flip the affine transformation
+ double scaleX = affine.getScaleX();
+ double shearX = affine.getShearX();
+ double shearY = affine.getShearY();
+ double scaleY = affine.getScaleY();
+ double translateX = affine.getTranslateX();
+ double translateY = affine.getTranslateY();
+
+ double newShearX = -shearX;
+ double newScaleY = -scaleY;
+ double newTranslateX = translateX + shearX * (height - 1);
+ double newTranslateY = translateY + scaleY * (height - 1);
+
+ AffineTransform2D flippedAffine =
+ new AffineTransform2D(scaleX, shearY, newShearX, newScaleY, newTranslateX, newTranslateY);
+
+ // Create a new WritableRaster to preserve pixel values
+ Raster originalRaster = RasterUtils.getRaster(image);
+ WritableRaster flippedRaster = originalRaster.createCompatibleWritableRaster();
+
+ // Copy pixel values to the flipped raster
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < image.getWidth(); x++) {
+ flippedRaster.setPixel(x, height - y - 1, originalRaster.getPixel(x, y, (double[]) null));
+ }
+ }
+
+ GridGeometry2D newGridGeometry =
+ new GridGeometry2D(
+ gridGeometry.getGridRange(),
+ flippedAffine,
+ gridGeometry.getCoordinateReferenceSystem());
+
+ return RasterUtils.clone(
+ flippedRaster, newGridGeometry, raster.getSampleDimensions(), raster, null, true);
+ }
}
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 0eaea40341..a3d2a474ea 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
@@ -18,6 +18,7 @@
*/
package org.apache.sedona.common.raster;
+import static org.apache.sedona.common.utils.RasterUtils.flipVerticallyPixelSpace;
import static org.junit.Assert.*;
import java.io.IOException;
@@ -127,6 +128,7 @@ public class RasterBandAccessorsTest extends RasterTestBase {
public void testZonalStats() throws FactoryException, ParseException, IOException {
GridCoverage2D raster =
rasterFromGeoTiff(resourceFolder + "raster_geotiff_color/FAA_UTM18N_NAD83.tif");
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
String polygon =
"POLYGON ((236722 4204770, 243900 4204770, 243900 4197590, 221170 4197590, 236722 4204770))";
Geometry geom = Constructors.geomFromWKT(polygon, RasterAccessors.srid(raster));
@@ -180,6 +182,15 @@ public class RasterBandAccessorsTest extends RasterTestBase {
() ->
RasterBandAccessors.getZonalStats(
raster, nonIntersectingGeom, 1, "sum", false, false, false));
+
+ // Test bottom-up raster case
+ geom = Constructors.geomFromWKT(polygon, RasterAccessors.srid(raster));
+ actual = RasterBandAccessors.getZonalStats(raster_bottom_up, geom, 1, "sum", false, false);
+ assertEquals(1.0795427E7, actual, 0d);
+
+ actual = RasterBandAccessors.getZonalStats(raster_bottom_up, geom, 2, "mean", false, false);
+ expected = 220.7711988936036;
+ assertEquals(expected, actual, FP_TOLERANCE);
}
@Test
@@ -206,6 +217,7 @@ public class RasterBandAccessorsTest extends RasterTestBase {
public void testZonalStatsWithNoData() throws IOException, FactoryException, ParseException {
GridCoverage2D raster =
rasterFromGeoTiff(resourceFolder + "raster/raster_with_no_data/test5.tiff");
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
String polygon =
"POLYGON((-167.750000 87.750000, -155.250000 87.750000, -155.250000 40.250000, -180.250000 40.250000, -167.750000 87.750000))";
// Testing implicit CRS transformation
@@ -238,6 +250,15 @@ public class RasterBandAccessorsTest extends RasterTestBase {
actual = RasterBandAccessors.getZonalStats(raster, geom, 1, "sd", false, true);
expected = 74.81287592054916;
assertEquals(expected, actual, FP_TOLERANCE);
+
+ // Test bottom-up raster case
+ actual = RasterBandAccessors.getZonalStats(raster_bottom_up, geom, 1, "sum", false, true);
+ expected = 3229013.0;
+ assertEquals(expected, actual, 0d);
+
+ actual = RasterBandAccessors.getZonalStats(raster_bottom_up, geom, 1, "mean", false, true);
+ expected = 226.61330619692416;
+ assertEquals(expected, actual, FP_TOLERANCE);
}
@Test
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 9b637c54a4..fbd49d456a 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
@@ -19,6 +19,7 @@
package org.apache.sedona.common.raster;
import static org.apache.sedona.common.raster.RasterBandEditors.rasterUnion;
+import static org.apache.sedona.common.utils.RasterUtils.flipVerticallyPixelSpace;
import static org.junit.Assert.*;
import java.io.IOException;
@@ -180,6 +181,9 @@ public class RasterBandEditorsTest extends RasterTestBase {
throws FactoryException, IOException, ParseException, TransformException {
GridCoverage2D raster =
rasterFromGeoTiff(resourceFolder + "raster_geotiff_color/FAA_UTM18N_NAD83.tif");
+
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
+
String polygon =
"POLYGON ((-8682522.873537656 4572703.890837922, -8673439.664183248 4572993.532747675, -8673155.57366801 4563873.2099182755, -8701890.325907696 4562931.7093397, -8682522.873537656 4572703.890837922))";
Geometry geom = Constructors.geomFromWKT(polygon, 3857);
@@ -199,6 +203,27 @@ public class RasterBandEditorsTest extends RasterTestBase {
Double[] actualValues = PixelFunctions.values(clippedRaster, points, 1).toArray(new Double[0]);
Double[] expectedValues = new Double[] {null, null, 0.0, 0.0, null};
assertTrue(Arrays.equals(expectedValues, actualValues));
+
+ // Test for bottom-up raster
+ clippedRaster = RasterBandEditors.clip(raster_bottom_up, 1, geom, false, 200, false);
+ clippedMetadata = Arrays.stream(RasterAccessors.metadata(clippedRaster), 0, 9).toArray();
+ originalMetadata = Arrays.stream(RasterAccessors.metadata(raster_bottom_up), 0, 9).toArray();
+ assertArrayEquals(originalMetadata, clippedMetadata, 0.01d);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(clippedRaster)[5],
+ 0.0);
+
+ points = new ArrayList<>();
+ points.add(Constructors.geomFromWKT("POINT(223802 4.21769e+06)", 26918));
+ points.add(Constructors.geomFromWKT("POINT(224759 4.20453e+06)", 26918));
+ 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(254668 4.21769e+06)", 26918));
+ actualValues = PixelFunctions.values(clippedRaster, points, 1).toArray(new Double[0]);
+ assertTrue(Arrays.equals(expectedValues, actualValues));
}
@Test
@@ -206,9 +231,13 @@ public class RasterBandEditorsTest extends RasterTestBase {
// Test for AOI geometries smaller than a pixel
GridCoverage2D raster =
RasterConstructors.makeEmptyRaster(1, "F", 4, 4, 401805.039562261, 2095852.150947876, 30);
+
raster = RasterEditors.setSrid(raster, 5070);
- double[] bandValues = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
- raster = MapAlgebra.addBandFromArray(raster, bandValues, 1);
+ double[] bandValues_top_down = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ raster = MapAlgebra.addBandFromArray(raster, bandValues_top_down, 1);
+
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
+
String polygon =
"POLYGON ((401866.1071465613 2095803.8989834636, 401850.9983055725 2095803.1375789386, 401850.039562261 2095822.150947876, 401865.14840359957 2095822.9124532426, 401880.2572475523 2095823.6738547424, 401881.2159852499 2095804.6604855175, 401866.1071465613 2095803.8989834636))";
Geometry geom = Constructors.geomFromWKT(polygon, 5070);
@@ -222,6 +251,22 @@ public class RasterBandEditorsTest extends RasterTestBase {
assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
assertTrue(Arrays.equals(expectedValues, actualValues));
+ // Test for bottom-up raster
+ clippedRaster = RasterBandEditors.clip(raster_bottom_up, 1, geom, false, 0, true);
+ bandNoDataValue = RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(clippedRaster)[5],
+ 0.0);
+
+ expectedValues = new double[] {0.0, 7.0, 0.0, 0.0};
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertTrue(Arrays.equals(expectedValues, actualValues));
+
+ // Test for geometry in different CRS
Geometry geomTransformed = FunctionsGeoTools.transform(geom, "EPSG:5070", "EPSG:4326");
clippedRaster = RasterBandEditors.clip(raster, 1, geomTransformed, false, 0, true);
@@ -232,6 +277,21 @@ public class RasterBandEditorsTest extends RasterTestBase {
assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
assertTrue(Arrays.equals(expectedValues, actualValues));
+
+ // Test for bottom-up raster
+ clippedRaster = RasterBandEditors.clip(raster_bottom_up, 1, geom, false, 0, true);
+ bandNoDataValue = RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(clippedRaster)[5],
+ 0.0);
+
+ expectedValues = new double[] {0.0, 7.0, 0.0, 0.0};
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertTrue(Arrays.equals(expectedValues, actualValues));
}
@Test
@@ -279,6 +339,24 @@ public class RasterBandEditorsTest extends RasterTestBase {
assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
assertArrayEquals(expectedValues, actualValues, 0.0);
+
+ // Test for bottom-up raster
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
+ clippedRaster = RasterBandEditors.clip(raster_bottom_up, 1, geom, true, 0, true);
+
+ bandNoDataValue = RasterBandAccessors.getBandNoDataValue(clippedRaster);
+ expectedBandNoDataValue = 0.0;
+ actualValues = MapAlgebra.bandAsArray(clippedRaster, 1);
+ expectedValues =
+ new double[] {
+ 91.0, 92.0, 93.0, 94.0, 0.0, 96.0, 97.0, 98.0, 99.0, 100.0, 81.0, 82.0, 83.0, 84.0, 0.0,
+ 86.0, 87.0, 88.0, 89.0, 90.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0,
+ 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.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, 0.0, 42.0, 43.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ };
+
+ assertEquals(expectedBandNoDataValue, bandNoDataValue, FP_TOLERANCE);
+ assertArrayEquals(expectedValues, actualValues, 0.0);
}
@Test
@@ -287,6 +365,8 @@ public class RasterBandEditorsTest extends RasterTestBase {
ClassNotFoundException {
GridCoverage2D raster =
rasterFromGeoTiff(resourceFolder + "raster_geotiff_color/FAA_UTM18N_NAD83.tif");
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
+
String polygon =
"POLYGON ((236722 4204770, 243900 4204770, 243900 4197590, 221170 4197590, 236722 4204770))";
Geometry geom = Constructors.geomFromWKT(polygon, RasterAccessors.srid(raster));
@@ -350,6 +430,53 @@ public class RasterBandEditorsTest extends RasterTestBase {
actualValues = PixelFunctions.values(croppedRaster, points, 1).toArray(new Double[0]);
expectedValues = new Double[] {85.0, 85.0, 127.0, 212.0, null};
assertArrayEquals(expectedValues, actualValues);
+
+ // Test for bottom-up raster
+ // Testing red band without crop
+ clippedRaster = RasterBandEditors.clip(raster_bottom_up, 1, geom, false, 200, false);
+
+ clippedMetadata = RasterAccessors.metadata(clippedRaster);
+ originalMetadata = RasterAccessors.metadata(raster_bottom_up);
+ assertArrayEquals(
+ Arrays.stream(originalMetadata, 0, 9).toArray(),
+ Arrays.stream(clippedMetadata, 0, 9).toArray(),
+ 0.01d);
+ assertEquals(1, clippedMetadata[9], FP_TOLERANCE);
+ actual = String.valueOf(clippedRaster.getSampleDimensions()[0]);
+ expected =
+ "RenderedSampleDimension(\"RED_BAND\":[200.0 ... 200.0])\n ‣ Category(\"No data\":[200...200])\n";
+ assertEquals(expected, actual);
+
+ points = new ArrayList<>();
+ points.add(Constructors.geomFromWKT("POINT(223802 4.21769e+06)", 26918));
+ points.add(Constructors.geomFromWKT("POINT(224759 4.20453e+06)", 26918));
+ 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(254668 4.21769e+06)", 26918));
+
+ actualValues = PixelFunctions.values(clippedRaster, points, 1).toArray(new Double[0]);
+ expectedValues = new Double[] {null, null, 0.0, 0.0, null};
+ assertTrue(Arrays.equals(expectedValues, actualValues));
+
+ // Testing green band with crop
+ croppedRaster = RasterBandEditors.clip(raster_bottom_up, 2, geom, false, 200, true);
+ assertEquals(0, croppedRaster.getRenderedImage().getMinX());
+ assertEquals(0, croppedRaster.getRenderedImage().getMinY());
+ croppedRaster2 = Serde.deserialize(Serde.serialize(croppedRaster));
+ assertSameCoverage(croppedRaster, croppedRaster2);
+
+ points = new ArrayList<>();
+ points.add(Constructors.geomFromWKT("POINT(236842 4.20465e+06)", 26918));
+ points.add(Constructors.geomFromWKT("POINT(236961 4.20453e+06)", 26918));
+ 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);
}
@Test
@@ -357,6 +484,7 @@ public class RasterBandEditorsTest extends RasterTestBase {
throws FactoryException, IOException, ParseException, TransformException {
GridCoverage2D raster =
rasterFromGeoTiff(resourceFolder + "raster_geotiff_color/FAA_UTM18N_NAD83.tif");
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
// Construct a polygon that does not intersect with the raster
Geometry nonIntersectingGeom =
@@ -376,6 +504,17 @@ public class RasterBandEditorsTest extends RasterTestBase {
RasterBandEditors.clip(raster, 1, nonIntersectingGeom, false, 200, false);
assertNull(result);
raster.dispose(true);
+
+ // Test for bottom-up raster
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ RasterBandEditors.clip(
+ raster_bottom_up, 1, nonIntersectingGeom, false, 200, false, false));
+
+ result = RasterBandEditors.clip(raster_bottom_up, 1, nonIntersectingGeom, false, 200, false);
+ assertNull(result);
+ raster_bottom_up.dispose(true);
}
@Test
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 7e8b7643b3..7bfb37c30d 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
@@ -18,6 +18,7 @@
*/
package org.apache.sedona.common.raster;
+import static org.apache.sedona.common.utils.RasterUtils.flipVerticallyPixelSpace;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -81,6 +82,7 @@ public class RasterConstructorsTest extends RasterTestBase {
// Polygon
GridCoverage2D raster =
RasterConstructors.makeEmptyRaster(2, 255, 255, 1, -1, 2, -2, 0, 0, 4326);
+ GridCoverage2D raster_bottom_up = flipVerticallyPixelSpace(raster);
Geometry geom =
Constructors.geomFromWKT("POLYGON((15 -15, 18 -20, 15 -24, 24 -25, 15 -15))", 0);
@@ -108,6 +110,22 @@ public class RasterConstructorsTest extends RasterTestBase {
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+ expected =
+ new double[] {
+ 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.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, 0.0
+ };
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ assertArrayEquals(expected, actual, 0.1d);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
// MultiPolygon
geom =
Constructors.geomFromWKT(
@@ -134,6 +152,23 @@ public class RasterConstructorsTest extends RasterTestBase {
};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0
+ };
+ assertArrayEquals(expected, actual, 0.1d);
+
// MultiLineString
geom =
Constructors.geomFromWKT("MULTILINESTRING ((5 -5, 10 -10), (10 -10, 15 -15, 20 -20))", 0);
@@ -151,6 +186,28 @@ public class RasterConstructorsTest extends RasterTestBase {
};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 3d);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 3093151.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, 0.0,
+ 0.0, 0.0, 0.0
+ };
+
+ assertArrayEquals(expected, actual, 0.1d);
+
rasterized = RasterConstructors.asRaster(geom, raster, "d");
actual = MapAlgebra.bandAsArray(rasterized, 1);
@@ -165,6 +222,27 @@ public class RasterConstructorsTest extends RasterTestBase {
};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.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, 0.0, 0.0, 0.0, 0.0
+ };
+
+ assertArrayEquals(expected, actual, 0.1d);
+
// LinearRing
geom = Constructors.geomFromWKT("LINEARRING (10 -10, 18 -20, 15 -24, 24 -25, 10 -10)", 0);
rasterized = RasterConstructors.asRaster(geom, raster, "d", false, 3093151, 3d);
@@ -181,6 +259,28 @@ public class RasterConstructorsTest extends RasterTestBase {
};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 3d);
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 3093151.0, 3093151.0, 3093151.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0,
+ 3093151.0, 3093151.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 3093151.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0,
+ 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0
+ };
+
+ assertArrayEquals(expected, actual, 0.1d);
+
rasterized = RasterConstructors.asRaster(geom, raster, "d");
actual = MapAlgebra.bandAsArray(rasterized, 1);
@@ -194,6 +294,25 @@ public class RasterConstructorsTest extends RasterTestBase {
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+
+ // Making sure orientation is preserved
+ assertEquals(
+ RasterAccessors.metadata(raster_bottom_up)[5],
+ RasterAccessors.metadata(rasterized)[5],
+ 0.0);
+
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ };
+ assertArrayEquals(expected, actual, 0.1d);
+
// MultiPoints
geom = Constructors.geomFromWKT("MULTIPOINT ((5 -5), (10 -10), (15 -15))", 0);
rasterized = RasterConstructors.asRaster(geom, raster, "d", false, 3093151, 3d);
@@ -208,6 +327,19 @@ public class RasterConstructorsTest extends RasterTestBase {
};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 3d);
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0,
+ 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 3093151.0, 3093151.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ };
+
+ assertArrayEquals(expected, actual, 0.1d);
+
rasterized = RasterConstructors.asRaster(geom, raster, "d");
actual = MapAlgebra.bandAsArray(rasterized, 1);
@@ -220,6 +352,17 @@ public class RasterConstructorsTest extends RasterTestBase {
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ expected =
+ new double[] {
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0,
+ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0
+ };
+ assertArrayEquals(expected, actual, 0.1d);
+
// Point
geom = Constructors.geomFromWKT("POINT (5 -5)", 0);
rasterized = RasterConstructors.asRaster(geom, raster, "d", false, 3093151, 3d);
@@ -229,10 +372,20 @@ public class RasterConstructorsTest extends RasterTestBase {
expected = new double[] {3093151.0, 3093151.0, 3093151.0, 3093151.0};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 3d);
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ assertArrayEquals(expected, actual, 0.1d);
+
rasterized = RasterConstructors.asRaster(geom, raster, "d");
actual = MapAlgebra.bandAsArray(rasterized, 1);
expected = new double[] {1.0, 1.0, 1.0, 1.0};
assertArrayEquals(expected, actual, 0.1d);
+
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d");
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ assertArrayEquals(expected, actual, 0.1d);
}
@Test
@@ -240,18 +393,31 @@ public class RasterConstructorsTest extends RasterTestBase {
// Horizontal LineString
GridCoverage2D raster =
RasterConstructors.makeEmptyRaster(2, 255, 255, 1, -1, 2, -2, 0, 0, 4326);
+ GridCoverage2D raster_bottom_up =
+ RasterConstructors.makeEmptyRaster(2, 255, 255, 1, -511, 2, 2, 0, 0, 4326);
+
Geometry geom = Constructors.geomFromEWKT("LINESTRING(1 -1, 2 -1, 10 -1)");
GridCoverage2D rasterized = RasterConstructors.asRaster(geom, raster, "d", false, 3093151, 0d);
double[] actual = MapAlgebra.bandAsArray(rasterized, 1);
double[] expected = new double[] {3093151.0, 3093151.0, 3093151.0, 3093151.0, 3093151.0};
assertArrayEquals(expected, actual, 0.1d);
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 0d);
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ assertArrayEquals(expected, actual, 0.1d);
+
// Vertical LineString
geom = Constructors.geomFromEWKT("LINESTRING(1 -1, 1 -2, 1 -10)");
rasterized = RasterConstructors.asRaster(geom, raster, "d", false, 3093151, 0d);
actual = MapAlgebra.bandAsArray(rasterized, 1);
expected = new double[] {3093151.0, 3093151.0, 3093151.0, 3093151.0, 3093151.0};
assertArrayEquals(expected, actual, 0.1d);
+
+ // Test bottom-up raster case
+ rasterized = RasterConstructors.asRaster(geom, raster_bottom_up, "d", false, 3093151, 0d);
+ actual = MapAlgebra.bandAsArray(rasterized, 1);
+ assertArrayEquals(expected, actual, 0.1d);
}
@Test
@@ -320,6 +486,9 @@ public class RasterConstructorsTest extends RasterTestBase {
public void testAsRasterWithRasterExtent2() throws FactoryException, ParseException {
GridCoverage2D raster =
RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0.5, 0.1, -0.1, 0, 0, 4326);
+ GridCoverage2D raster_bottom_up =
+ RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0.0, 0.1, 0.1, 0, 0, 4326);
+
Geometry geom =
Constructors.geomFromWKT("POLYGON((0.1 0.1, 0.1 0.4, 0.4 0.4, 0.4 0.1, 0.1 0.1))", 0);
GridCoverage2D rasterized =
@@ -332,6 +501,18 @@ public class RasterConstructorsTest extends RasterTestBase {
assertEquals(5, RasterAccessors.getHeight(rasterized));
double sum = Arrays.stream(MapAlgebra.bandAsArray(rasterized, 1)).sum();
assertEquals(900, sum, 1e-6); // Covers 3x3 grid
+
+ // Test bottom-up raster case
+ rasterized =
+ RasterConstructors.asRasterWithRasterExtent(geom, raster_bottom_up, "d", false, 100d, 0d);
+ assertEquals(0, rasterized.getEnvelope2D().getMinX(), 1e-6);
+ assertEquals(0, rasterized.getEnvelope2D().getMinY(), 1e-6);
+ assertEquals(0.5, rasterized.getEnvelope2D().getWidth(), 1e-6);
+ assertEquals(0.5, rasterized.getEnvelope2D().getHeight(), 1e-6);
+ assertEquals(5, RasterAccessors.getWidth(rasterized));
+ assertEquals(5, RasterAccessors.getHeight(rasterized));
+ sum = Arrays.stream(MapAlgebra.bandAsArray(rasterized, 1)).sum();
+ assertEquals(900, sum, 1e-6); // Covers 3x3 grid
}
@Test
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
index 9d03220c6e..8050541ee1 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/RasterizationTests.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/RasterizationTests.java
@@ -21,7 +21,6 @@ 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;
@@ -34,8 +33,7 @@ public class RasterizationTests extends RasterTestBase {
private final WKTReader wktReader = new WKTReader();
@Test
- public void testRasterizeGeomExtent()
- throws ParseException, FactoryException, TransformException {
+ public void testRasterizeGeomExtent() throws ParseException, FactoryException {
GridCoverage2D testRaster = RasterConstructors.makeEmptyRaster(1, "F", 4, 4, 4, 4, 1);
double[] metadata = metadata(testRaster);
@@ -114,11 +112,167 @@ public class RasterizationTests extends RasterTestBase {
validateRasterizeGeomExtent(
wktLine, new double[] {5.0, 6.0, 1.0, 3.0}, testRaster, metadata, false);
+ // horizontal line at the top-left edge
+ wktLine = "LINESTRING (4.0 4.0, 5.5 4.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 6.0, 3.0, 4.0}, testRaster, metadata, false);
+
+ // horizontal line at the top-right edge
+ wktLine = "LINESTRING (6.5 4.0, 8.0 4.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {6.0, 8.0, 3.0, 4.0}, testRaster, metadata, false);
+
+ // horizontal line at the bottom edge
+ wktLine = "LINESTRING (4.0 0.0, 8.0 0.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 8.0, 0.0, 1.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);
+
+ // vertical line at the right edge
+ wktLine = "LINESTRING (8.0 0.0, 8.0 3.5)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {7.0, 8.0, 0.0, 4.0}, testRaster, metadata, false);
+
+ // vertical line at the left edge
+ wktLine = "LINESTRING (4.0 0.0, 4.0 3.5)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 5.0, 0.0, 4.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);
+ }
+
+ @Test
+ public void testRasterizeGeomExtentWithBottomUpRaster() throws ParseException, FactoryException {
+ GridCoverage2D testRaster = RasterConstructors.makeEmptyRaster(1, 4, 4, 4, 0, 1, 1, 0, 0, 4326);
+ 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, 3.3334, 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, -5.9998, 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);
+
+ // horizontal line at the upper left edge
+ wktLine = "LINESTRING (4.0 4.0, 5.5 4.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 6.0, 3.0, 4.0}, testRaster, metadata, false);
+
+ // horizontal line at the top-right edge
+ wktLine = "LINESTRING (6.5 4.0, 8.0 4.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {6.0, 8.0, 3.0, 4.0}, testRaster, metadata, false);
+
+ // horizontal line at the bottom edge
+ wktLine = "LINESTRING (4.0 0.0, 8.0 0.0)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 8.0, 0.0, 1.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);
+ // vertical line at the right edge
+ wktLine = "LINESTRING (8.0 0.0, 8.0 3.5)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {7.0, 8.0, 0.0, 4.0}, testRaster, metadata, false);
+
+ // vertical line at the left edge
+ wktLine = "LINESTRING (4.0 0.0, 4.0 3.5)";
+ validateRasterizeGeomExtent(
+ wktLine, new double[] {4.0, 5.0, 0.0, 4.0}, testRaster, metadata, false);
+
// diagonal line
wktLine = "LINESTRING (5.0 2.0, 6.0 3.0)";
validateRasterizeGeomExtent(
diff --git a/common/src/test/java/org/apache/sedona/common/utils/RasterUtilsTest.java b/common/src/test/java/org/apache/sedona/common/utils/RasterUtilsTest.java
index 72b8ffc288..e34bbb0803 100644
--- a/common/src/test/java/org/apache/sedona/common/utils/RasterUtilsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/utils/RasterUtilsTest.java
@@ -18,12 +18,28 @@
*/
package org.apache.sedona.common.utils;
+import static org.apache.sedona.common.Functions.setSRID;
+import static org.apache.sedona.common.raster.GeometryFunctions.envelope;
+import static org.apache.sedona.common.raster.PixelFunctions.value;
+import static org.apache.sedona.common.raster.PixelFunctions.values;
+import static org.geotools.filter.function.StaticGeometry.geomFromWKT;
+import static org.junit.Assert.assertEquals;
+
import java.awt.Color;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.sedona.common.raster.MapAlgebra;
+import org.apache.sedona.common.raster.RasterAccessors;
+import org.apache.sedona.common.raster.RasterConstructors;
+import org.geotools.api.referencing.FactoryException;
+import org.geotools.api.referencing.operation.TransformException;
import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
+import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.util.NumberRange;
import org.junit.Assert;
import org.junit.Test;
+import org.locationtech.jts.geom.Geometry;
public class RasterUtilsTest {
@Test
@@ -133,4 +149,117 @@ public class RasterUtilsTest {
Assert.assertEquals("GrayScale", band3.getCategory(100).getName().toString());
Assert.assertEquals("GrayScale", band3.getCategory(199.999).getName().toString());
}
+
+ @Test
+ public void testFlipVerticallyPixelSpace() throws FactoryException, TransformException {
+ GridCoverage2D raster_top_down =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, 401805.039562261, 2095852.150947876, 30, -30, 0, 0, 5070);
+ GridCoverage2D raster_bottom_up =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, 401805.039562261, 2095732.150947876, 30, 30, 0, 0, 5070);
+
+ double[] bandValues_top_down = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ double[] bandValues_bottom_up = {13, 14, 15, 16, 9, 10, 11, 12, 5, 6, 7, 8, 1, 2, 3, 4};
+
+ raster_top_down = MapAlgebra.addBandFromArray(raster_top_down, bandValues_top_down, 1);
+ raster_bottom_up = MapAlgebra.addBandFromArray(raster_bottom_up, bandValues_bottom_up, 1);
+
+ GridCoverage2D raster_flipped = RasterUtils.flipVerticallyPixelSpace(raster_bottom_up);
+
+ double[] raster_bottom_up_metadata = RasterAccessors.metadata(raster_bottom_up);
+ double[] raster_flipped_metadata = RasterAccessors.metadata(raster_flipped);
+
+ assertEquals(-raster_bottom_up_metadata[5], raster_flipped_metadata[5], 0);
+ assertEquals(envelope(raster_bottom_up), envelope(raster_flipped));
+
+ List<Geometry> testGeometries =
+ Arrays.asList(
+ setSRID(geomFromWKT("POINT (401820.039562261 2095837.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095837.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401820.039562261 2095747.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095747.150947876)"), 5070));
+
+ List<Double> values_bottom_up = values(raster_bottom_up, testGeometries);
+ List<Double> values_flipped = values(raster_flipped, testGeometries);
+ Assert.assertArrayEquals(values_bottom_up.toArray(), values_flipped.toArray());
+
+ raster_flipped = RasterUtils.flipVerticallyPixelSpace(raster_top_down);
+
+ double[] raster_top_down_metadata = RasterAccessors.metadata(raster_top_down);
+ raster_flipped_metadata = RasterAccessors.metadata(raster_flipped);
+
+ assertEquals(-raster_top_down_metadata[5], raster_flipped_metadata[5], 0);
+ assertEquals(envelope(raster_top_down), envelope(raster_flipped));
+
+ List<Double> values_top_down = values(raster_top_down, testGeometries);
+ values_flipped = values(raster_flipped, testGeometries);
+ Assert.assertArrayEquals(values_top_down.toArray(), values_flipped.toArray());
+ }
+
+ @Test
+ public void testFlipVerticallyGridSpace() throws FactoryException, TransformException {
+ GridCoverage2D raster_top_down =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, 401805.039562261, 2095852.150947876, 30, -30, 0, 0, 5070);
+ GridCoverage2D raster_bottom_up =
+ RasterConstructors.makeEmptyRaster(
+ 1, "F", 4, 4, 401805.039562261, 2095732.150947876, 30, 30, 0, 0, 5070);
+
+ double[] bandValues_top_down = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ double[] bandValues_bottom_up = {13, 14, 15, 16, 9, 10, 11, 12, 5, 6, 7, 8, 1, 2, 3, 4};
+
+ raster_top_down = MapAlgebra.addBandFromArray(raster_top_down, bandValues_top_down, 1);
+ raster_bottom_up = MapAlgebra.addBandFromArray(raster_bottom_up, bandValues_bottom_up, 1);
+
+ GridCoverage2D raster_flipped = RasterUtils.flipVerticallyGridSpace(raster_bottom_up);
+
+ double[] raster_bottom_up_metadata = RasterAccessors.metadata(raster_bottom_up);
+ double[] raster_flipped_metadata = RasterAccessors.metadata(raster_flipped);
+
+ assertEquals(-raster_bottom_up_metadata[5], raster_flipped_metadata[5], 0);
+ assertEquals(envelope(raster_bottom_up), envelope(raster_flipped));
+
+ List<Geometry> testGeometries =
+ Arrays.asList(
+ setSRID(geomFromWKT("POINT (401820.039562261 2095837.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095837.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401820.039562261 2095747.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095747.150947876)"), 5070));
+
+ List<Geometry> testGeometriesFlipped =
+ Arrays.asList(
+ setSRID(geomFromWKT("POINT (401820.039562261 2095747.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095747.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401820.039562261 2095837.150947876)"), 5070),
+ setSRID(geomFromWKT("POINT (401910.039562261 2095837.150947876)"), 5070));
+
+ for (int i = 0; i < testGeometries.size(); i++) {
+ Double valueBottomUp = value(raster_bottom_up, testGeometries.get(i));
+ Double valueFlipped = value(raster_flipped, testGeometriesFlipped.get(i));
+ Assert.assertEquals(
+ "Pixel values should match at corresponding positions",
+ valueBottomUp,
+ valueFlipped,
+ 1e-6f);
+ }
+
+ raster_flipped = RasterUtils.flipVerticallyGridSpace(raster_top_down);
+
+ double[] raster_top_down_metadata = RasterAccessors.metadata(raster_top_down);
+ raster_flipped_metadata = RasterAccessors.metadata(raster_flipped);
+
+ assertEquals(-raster_top_down_metadata[5], raster_flipped_metadata[5], 0);
+ assertEquals(envelope(raster_top_down), envelope(raster_flipped));
+
+ for (int i = 0; i < testGeometries.size(); i++) {
+ Double valueTopDown = value(raster_top_down, testGeometries.get(i));
+ Double valueFlipped = value(raster_flipped, testGeometriesFlipped.get(i));
+ Assert.assertEquals(
+ "Pixel values should match at corresponding positions",
+ valueTopDown,
+ valueFlipped,
+ 1e-6f);
+ }
+ }
}
diff --git a/docs/api/sql/Raster-Operators/RS_AsRaster.md b/docs/api/sql/Raster-Operators/RS_AsRaster.md
index be0dd7aa29..dcecf82ee4 100644
--- a/docs/api/sql/Raster-Operators/RS_AsRaster.md
+++ b/docs/api/sql/Raster-Operators/RS_AsRaster.md
@@ -59,7 +59,6 @@ Since: `v1.5.0`
The function doesn't support rasters that have any one of the following properties:
```
ScaleX < 0
- ScaleY > 0
SkewX != 0
SkewY != 0
```
diff --git a/spark/common/src/test/resources/rasterization/test_rasterization.tiff b/spark/common/src/test/resources/rasterization/test_rasterization.tiff
index 0904a95ff2..e35d883be1 100644
Binary files a/spark/common/src/test/resources/rasterization/test_rasterization.tiff and b/spark/common/src/test/resources/rasterization/test_rasterization.tiff differ