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 8d5c76d5 [SEDONA-338] Fix `RS_MakeEmptyRaster` and make affine 
transformation parameters compliant with the GDAL/PostGIS convention (#940)
8d5c76d5 is described below

commit 8d5c76d52d855f6ee7de6e0132117cfa52ab40c7
Author: Kristin Cowalcijk <[email protected]>
AuthorDate: Thu Aug 3 00:36:14 2023 +0800

    [SEDONA-338] Fix `RS_MakeEmptyRaster` and make affine transformation 
parameters compliant with the GDAL/PostGIS convention (#940)
---
 .../apache/sedona/common/raster/MapAlgebra.java    | 30 +------
 .../sedona/common/raster/RasterAccessors.java      | 69 +++++++--------
 .../sedona/common/raster/RasterConstructors.java   | 48 ++++++-----
 .../apache/sedona/common/utils/RasterUtils.java    | 91 ++++++++++++++++++++
 .../sedona/common/raster/RasterAccessorsTest.java  | 98 ++++++++++++++++------
 .../common/raster/RasterConstructorsTest.java      |  5 +-
 docs/api/sql/Raster-loader.md                      | 14 ++--
 docs/api/sql/Raster-operators.md                   |  6 +-
 .../org/apache/sedona/sql/rasteralgebraTest.scala  |  2 +-
 9 files changed, 234 insertions(+), 129 deletions(-)

diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/MapAlgebra.java 
b/common/src/main/java/org/apache/sedona/common/raster/MapAlgebra.java
index 67e8ad18..98a77204 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/MapAlgebra.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/MapAlgebra.java
@@ -18,25 +18,15 @@
  */
 package org.apache.sedona.common.raster;
 
-import com.sun.media.imageioimpl.common.BogusColorSpace;
-import org.geotools.coverage.CoverageFactoryFinder;
+import org.apache.sedona.common.utils.RasterUtils;
 import org.geotools.coverage.GridSampleDimension;
 import org.geotools.coverage.grid.GridCoverage2D;
-import org.geotools.coverage.grid.GridCoverageFactory;
 
 import javax.media.jai.RasterFactory;
-
 import java.awt.Point;
-import java.awt.Transparency;
-import java.awt.color.ColorSpace;
-import java.awt.image.BufferedImage;
-import java.awt.image.ColorModel;
-import java.awt.image.ComponentColorModel;
-import java.awt.image.DataBuffer;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
 import java.awt.image.WritableRaster;
-import java.util.Arrays;
 
 public class MapAlgebra
 {
@@ -134,7 +124,7 @@ public class MapAlgebra
         System.arraycopy(originalSampleDimensions, 0, sampleDimensions, 0, 
originalSampleDimensions.length);
         sampleDimensions[numBand - 1] = new GridSampleDimension("band" + 
numBand);
         // Construct a GridCoverage2D with the copied image.
-        return createCompatibleGridCoverage2D(gridCoverage2D, wr, 
sampleDimensions);
+        return RasterUtils.create(wr, gridCoverage2D.getGridGeometry(), 
sampleDimensions);
     }
 
     private static GridCoverage2D copyRasterAndReplaceBand(GridCoverage2D 
gridCoverage2D, int bandIndex, double[] bandValues) {
@@ -155,20 +145,6 @@ public class MapAlgebra
             }
         }
         // Create a new GridCoverage2D with the copied image
-        return createCompatibleGridCoverage2D(gridCoverage2D, wr, 
gridCoverage2D.getSampleDimensions());
-    }
-
-    private static GridCoverage2D 
createCompatibleGridCoverage2D(GridCoverage2D gridCoverage2D, WritableRaster 
wr, GridSampleDimension[] bands) {
-        int rasterDataType = wr.getDataBuffer().getDataType();
-        int numBand = wr.getNumBands();
-        final ColorSpace cs = new BogusColorSpace(numBand);
-        final int[] nBits = new int[numBand];
-        Arrays.fill(nBits, DataBuffer.getDataTypeSize(rasterDataType));
-        ColorModel colorModel =
-                new ComponentColorModel(cs, nBits, false, true, 
Transparency.OPAQUE, rasterDataType);
-        final RenderedImage image = new BufferedImage(colorModel, wr, false, 
null);
-        GridCoverageFactory gridCoverageFactory = 
CoverageFactoryFinder.getGridCoverageFactory(null);
-        return gridCoverageFactory.create(gridCoverage2D.getName(), image, 
gridCoverage2D.getCoordinateReferenceSystem(),
-                gridCoverage2D.getGridGeometry().getGridToCRS(), bands, null, 
null);
+        return RasterUtils.create(wr, gridCoverage2D.getGridGeometry(), 
gridCoverage2D.getSampleDimensions());
     }
 }
diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java 
b/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
index e5841680..07aaab24 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
@@ -18,9 +18,9 @@
  */
 package org.apache.sedona.common.raster;
 
+import org.apache.sedona.common.utils.RasterUtils;
 import org.geotools.coverage.grid.GridCoverage2D;
-import org.geotools.coverage.grid.GridGeometry2D;
-import org.geotools.coverage.processing.operation.Affine;
+import org.geotools.coverage.grid.GridEnvelope2D;
 import org.geotools.geometry.Envelope2D;
 import org.geotools.referencing.CRS;
 import org.geotools.referencing.crs.DefaultEngineeringCRS;
@@ -31,9 +31,7 @@ import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.PrecisionModel;
 import org.opengis.referencing.FactoryException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.operation.MathTransform;
 
-import java.awt.image.RenderedImage;
 import java.util.Optional;
 
 public class RasterAccessors
@@ -64,30 +62,21 @@ public class RasterAccessors
     }
 
     public static double getUpperLeftX(GridCoverage2D raster) {
-        Envelope2D envelope2D = raster.getEnvelope2D();
-        return envelope2D.getMinX();
+        AffineTransform2D affine = RasterUtils.getGDALAffineTransform(raster);
+        return affine.getTranslateX();
     }
 
     public static double getUpperLeftY(GridCoverage2D raster) {
-        Envelope2D envelope2D = raster.getEnvelope2D();
-        return envelope2D.getMaxY();
+        AffineTransform2D affine = RasterUtils.getGDALAffineTransform(raster);
+        return affine.getTranslateY();
     }
 
     public static double getScaleX(GridCoverage2D raster) {
-        return getAffineTransform(raster).getScaleX();
+        return RasterUtils.getGDALAffineTransform(raster).getScaleX();
     }
 
     public static double getScaleY(GridCoverage2D raster) {
-        return getAffineTransform(raster).getScaleY();
-    }
-
-    private static AffineTransform2D getAffineTransform(GridCoverage2D raster) 
throws UnsupportedOperationException {
-        GridGeometry2D gridGeometry2D = raster.getGridGeometry();
-        MathTransform crsTransform = gridGeometry2D.getGridToCRS2D();
-        if (!(crsTransform instanceof AffineTransform2D)) {
-            throw new UnsupportedOperationException("Only AffineTransform2D is 
supported");
-        }
-        return (AffineTransform2D) crsTransform;
+        return RasterUtils.getGDALAffineTransform(raster).getScaleY();
     }
 
     public static Geometry envelope(GridCoverage2D raster) throws 
FactoryException {
@@ -100,16 +89,16 @@ public class RasterAccessors
 
     /**
      * Returns the metadata of a raster as an array of doubles.
-     * @param raster
+     * @param raster the raster
      * @return double[] with the following values:
-     * 0: minX: upper left x
-     * 1: maxY: upper left y
+     * 0: upperLeftX: upper left x
+     * 1: upperLeftY: upper left y
      * 2: width: number of pixels on x axis
      * 3: height: number of pixels on y axis
      * 4: scaleX: pixel width
      * 5: scaleY: pixel height
-     * 6: shearX: skew on x axis
-     * 7: shearY: skew on y axis
+     * 6: skewX: skew on x axis
+     * 7: skewY: skew on y axis
      * 8: srid
      * 9: numBands
      * @throws FactoryException
@@ -117,23 +106,21 @@ public class RasterAccessors
     public static double[] metadata(GridCoverage2D raster)
             throws FactoryException
     {
-        RenderedImage image = raster.getRenderedImage();
-        // Georeference metadata
-        Envelope2D envelope2D = raster.getEnvelope2D();
-        MathTransform gridToCRS = raster.getGridGeometry().getGridToCRS2D();
-        if (gridToCRS instanceof AffineTransform2D) {
-            AffineTransform2D affine = (AffineTransform2D) gridToCRS;
+        // Get Geo-reference metadata
+        GridEnvelope2D gridRange = raster.getGridGeometry().getGridRange2D();
+        AffineTransform2D affine = RasterUtils.getGDALAffineTransform(raster);
 
-            // Get the affine parameters
-            double scaleX = affine.getScaleX();
-            double scaleY = affine.getScaleY();
-            double shearX = affine.getShearX();
-            double shearY = affine.getShearY();
-            return new double[] {envelope2D.getMinX(), envelope2D.getMaxY(), 
image.getWidth(), image.getHeight(), scaleX, scaleY, shearX, shearY, 
srid(raster), raster.getNumSampleDimensions()};
-        }
-        else {
-            // Handle the case where gridToCRS is not an AffineTransform2D
-            throw new UnsupportedOperationException("Only AffineTransform2D is 
supported");
-        }
+        // Get the affine parameters
+        double upperLeftX = affine.getTranslateX();
+        double upperLeftY = affine.getTranslateY();
+        double scaleX = affine.getScaleX();
+        double scaleY = affine.getScaleY();
+        double skewX = affine.getShearX();
+        double skewY = affine.getShearY();
+        return new double[] {
+                upperLeftX, upperLeftY,
+                gridRange.getWidth(), gridRange.getHeight(),
+                scaleX, scaleY, skewX, skewY,
+                srid(raster), raster.getNumSampleDimensions()};
     }
 }
diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/RasterConstructors.java 
b/common/src/main/java/org/apache/sedona/common/raster/RasterConstructors.java
index 9111cf3e..019748f3 100644
--- 
a/common/src/main/java/org/apache/sedona/common/raster/RasterConstructors.java
+++ 
b/common/src/main/java/org/apache/sedona/common/raster/RasterConstructors.java
@@ -14,19 +14,18 @@
 package org.apache.sedona.common.raster;
 
 import org.apache.sedona.common.raster.inputstream.ByteArrayImageInputStream;
-import org.geotools.coverage.CoverageFactoryFinder;
+import org.apache.sedona.common.utils.RasterUtils;
 import org.geotools.coverage.grid.GridCoverage2D;
-import org.geotools.coverage.grid.GridCoverageFactory;
 import org.geotools.coverage.grid.GridEnvelope2D;
 import org.geotools.coverage.grid.GridGeometry2D;
 import org.geotools.gce.arcgrid.ArcGridReader;
 import org.geotools.gce.geotiff.GeoTiffReader;
-import org.geotools.geometry.jts.ReferencedEnvelope;
 import org.geotools.referencing.CRS;
 import org.geotools.referencing.crs.DefaultEngineeringCRS;
 import org.geotools.referencing.operation.transform.AffineTransform2D;
 import org.opengis.referencing.FactoryException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.MathTransform;
 
 import javax.media.jai.RasterFactory;
@@ -47,26 +46,28 @@ public class RasterConstructors
     }
 
     /**
-     * Create a new empty raster with the given number of empty bands
-     * The bounding envelope is defined by the upper left corner and the scale
+     * Create a new empty raster with the given number of empty bands.
+     * The bounding envelope is defined by the upper left corner and the scale.
      * The math formula of the envelope is: minX = upperLeftX = lowerLeftX, 
minY (lowerLeftY) = upperLeftY - height * pixelSize
-     * The raster is defined by the width and height
-     * The affine transform is defined by the skewX and skewY
-     * The upper left corner is defined by the upperLeftX and upperLeftY
-     * The scale is defined by the scaleX and scaleY
-     * SRID is default to 0 which means the default CRS (Cartesian 2D)
+     * <ul>
+     *   <li>The raster is defined by the width and height
+     *   <li>The upper left corner is defined by the upperLeftX and upperLeftY
+     *   <li>The scale is defined by pixelSize. The scaleX is equal to 
pixelSize and scaleY is equal to -pixelSize
+     *   <li>skewX and skewY are zero, which means no shear or rotation.
+     *   <li>SRID is default to 0 which means the default CRS (Generic 2D)
+     * </ul>
      * @param numBand the number of bands
-     * @param widthInPixel
-     * @param heightInPixel
+     * @param widthInPixel the width of the raster, in pixel
+     * @param heightInPixel the height of the raster, in pixel
      * @param upperLeftX the upper left corner of the raster. Note that: the 
minX of the envelope is equal to the upperLeftX
-     * @param upperLeftY the upper left corner of the raster. Note that: the 
minY of the envelope is equal to the upperLeftY - height * scaleY
+     * @param upperLeftY the upper left corner of the raster. Note that: the 
minY of the envelope is equal to the upperLeftY - height * pixelSize
      * @param pixelSize the size of the pixel in the unit of the CRS
-     * @return
+     * @return the new empty raster
      */
     public static GridCoverage2D makeEmptyRaster(int numBand, int 
widthInPixel, int heightInPixel, double upperLeftX, double upperLeftY, double 
pixelSize)
             throws FactoryException
     {
-        return makeEmptyRaster(numBand, widthInPixel, heightInPixel, 
upperLeftX, upperLeftY, pixelSize, pixelSize, 0, 0, 0);
+        return makeEmptyRaster(numBand, widthInPixel, heightInPixel, 
upperLeftX, upperLeftY, pixelSize, -pixelSize, 0, 0, 0);
     }
 
     /**
@@ -75,13 +76,13 @@ public class RasterConstructors
      * @param widthInPixel the width of the raster, in pixel
      * @param heightInPixel the height of the raster, in pixel
      * @param upperLeftX the upper left corner of the raster, in the CRS unit. 
Note that: the minX of the envelope is equal to the upperLeftX
-     * @param upperLeftY the upper left corner of the raster, in the CRS unit. 
Note that: the minY of the envelope is equal to the upperLeftY - height * scaleY
+     * @param upperLeftY the upper left corner of the raster, in the CRS unit. 
Note that: the minY of the envelope is equal to the upperLeftY + height * scaleY
      * @param scaleX the scale of the raster (pixel size on X), in the CRS unit
      * @param scaleY the scale of the raster (pixel size on Y), in the CRS unit
      * @param skewX the skew of the raster on X, in the CRS unit
      * @param skewY the skew of the raster on Y, in the CRS unit
      * @param srid the srid of the CRS. 0 means the default CRS (Cartesian 2D)
-     * @return
+     * @return the new empty raster
      * @throws FactoryException
      */
     public static GridCoverage2D makeEmptyRaster(int numBand, int 
widthInPixel, int heightInPixel, double upperLeftX, double upperLeftY, double 
scaleX, double scaleY, double skewX, double skewY, int srid)
@@ -93,13 +94,14 @@ public class RasterConstructors
         } else {
             crs = CRS.decode("EPSG:" + srid);
         }
+
         // Create a new empty raster
         WritableRaster raster = 
RasterFactory.createBandedRaster(DataBuffer.TYPE_DOUBLE, widthInPixel, 
heightInPixel, numBand, null);
-        MathTransform transform = new AffineTransform2D(scaleX, skewY, skewX, 
-scaleY, upperLeftX + scaleX / 2, upperLeftY - scaleY / 2);
-        GridGeometry2D gridGeometry = new GridGeometry2D(new GridEnvelope2D(0, 
0, widthInPixel, heightInPixel), transform, crs);
-        ReferencedEnvelope referencedEnvelope = new 
ReferencedEnvelope(gridGeometry.getEnvelope2D());
-        // Create a new coverage
-        GridCoverageFactory gridCoverageFactory = 
CoverageFactoryFinder.getGridCoverageFactory(null);
-        return gridCoverageFactory.create("genericCoverage", raster, 
referencedEnvelope);
+        MathTransform transform = new AffineTransform2D(scaleX, skewY, skewX, 
scaleY, upperLeftX, upperLeftY);
+        GridGeometry2D gridGeometry = new GridGeometry2D(
+                new GridEnvelope2D(0, 0, widthInPixel, heightInPixel),
+                PixelInCell.CELL_CORNER,
+                transform, crs, null);
+        return RasterUtils.create(raster, gridGeometry, null);
     }
 }
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
new file mode 100644
index 00000000..a120c88b
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/utils/RasterUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.utils;
+
+import com.sun.media.imageioimpl.common.BogusColorSpace;
+import org.geotools.coverage.CoverageFactoryFinder;
+import org.geotools.coverage.GridSampleDimension;
+import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.coverage.grid.GridCoverageFactory;
+import org.geotools.coverage.grid.GridGeometry2D;
+import org.geotools.referencing.operation.transform.AffineTransform2D;
+import org.opengis.metadata.spatial.PixelOrientation;
+import org.opengis.referencing.operation.MathTransform;
+
+import java.awt.Transparency;
+import java.awt.color.ColorSpace;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.ComponentColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import java.util.Arrays;
+
+/**
+ * Utility functions for working with GridCoverage2D objects.
+ */
+public class RasterUtils {
+    private RasterUtils() {}
+
+    private static final GridCoverageFactory gridCoverageFactory = 
CoverageFactoryFinder.getGridCoverageFactory(null);
+
+    /**
+     * Create a new empty raster from the given WritableRaster object.
+     * @param raster The raster object to be wrapped as an image.
+     * @param gridGeometry The grid geometry of the raster.
+     * @param bands The bands of the raster.
+     * @return A new GridCoverage2D object.
+     */
+    public static GridCoverage2D create(WritableRaster raster, GridGeometry2D 
gridGeometry, GridSampleDimension[] bands) {
+        int numBand = raster.getNumBands();
+        int rasterDataType = raster.getDataBuffer().getDataType();
+
+        // Construct a color model for the rendered image. This color model 
should be able to be serialized and
+        // deserialized. The color model object automatically constructed by 
grid coverage factory may not be
+        // serializable, please refer to 
https://issues.apache.org/jira/browse/SEDONA-319 for more details.
+        final ColorSpace cs = new BogusColorSpace(numBand);
+        final int[] nBits = new int[numBand];
+        Arrays.fill(nBits, DataBuffer.getDataTypeSize(rasterDataType));
+        ColorModel colorModel =
+                new ComponentColorModel(cs, nBits, false, true, 
Transparency.OPAQUE, rasterDataType);
+
+        final RenderedImage image = new BufferedImage(colorModel, raster, 
false, null);
+        return gridCoverageFactory.create("genericCoverage", image, 
gridGeometry, bands, null, null);
+    }
+
+    /**
+     * Get a GDAL-compliant affine transform from the given raster, where the 
grid coordinate indicates the upper left
+     * corner of the pixel. PostGIS also follows GDAL convention.
+     * @param raster The raster to get the affine transform from.
+     * @return The affine transform.
+     */
+    public static AffineTransform2D getGDALAffineTransform(GridCoverage2D 
raster) {
+        return getAffineTransform(raster, PixelOrientation.UPPER_LEFT);
+    }
+
+    public static AffineTransform2D getAffineTransform(GridCoverage2D raster, 
PixelOrientation orientation) throws UnsupportedOperationException {
+        GridGeometry2D gridGeometry2D = raster.getGridGeometry();
+        MathTransform crsTransform = 
gridGeometry2D.getGridToCRS2D(orientation);
+        if (!(crsTransform instanceof AffineTransform2D)) {
+            throw new UnsupportedOperationException("Only AffineTransform2D is 
supported");
+        }
+        return (AffineTransform2D) crsTransform;
+    }
+}
diff --git 
a/common/src/test/java/org/apache/sedona/common/raster/RasterAccessorsTest.java 
b/common/src/test/java/org/apache/sedona/common/raster/RasterAccessorsTest.java
index d70ad0f8..af9a487a 100644
--- 
a/common/src/test/java/org/apache/sedona/common/raster/RasterAccessorsTest.java
+++ 
b/common/src/test/java/org/apache/sedona/common/raster/RasterAccessorsTest.java
@@ -20,6 +20,7 @@ package org.apache.sedona.common.raster;
 
 import org.geotools.coverage.grid.GridCoverage2D;
 import org.junit.Test;
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.opengis.referencing.FactoryException;
 
@@ -34,10 +35,32 @@ public class RasterAccessorsTest extends RasterTestBase
         assertEquals(3600.0d, envelope.getArea(), 0.1d);
         assertEquals(378922.0d + 30.0d, envelope.getCentroid().getX(), 0.1d);
         assertEquals(4072345.0d + 30.0d, envelope.getCentroid().getY(), 0.1d);
-
         assertEquals(4326, 
RasterAccessors.envelope(multiBandRaster).getSRID());
     }
 
+    @Test
+    public void testEnvelopeUsingSkewedRaster() throws FactoryException {
+        GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 100, 
100, 5, 4, 3, -2, 0.1, 0.15, 3857);
+        Geometry envelope = RasterAccessors.envelope(raster);
+        Envelope env = envelope.getEnvelopeInternal();
+        // The expected values were obtained by running the following query in 
PostGIS:
+        // SELECT ST_AsText(ST_Envelope(ST_MakeEmptyRaster(100, 100, 5, 4, 3, 
-2, 0.1, 0.15, 3857)));
+        assertEquals(5, env.getMinX(), 1e-9);
+        assertEquals(315, env.getMaxX(), 1e-9);
+        assertEquals(-196, env.getMinY(), 1e-9);
+        assertEquals(19, env.getMaxY(), 1e-9);
+
+        raster = RasterConstructors.makeEmptyRaster(1, 800, 700, 5, 4, 0.3, 
-0.2, -0.1, -0.15, 3857);
+        envelope = RasterAccessors.envelope(raster);
+        env = envelope.getEnvelopeInternal();
+        // The expected values were obtained by running the following query in 
PostGIS:
+        // SELECT ST_AsText(ST_Envelope(ST_MakeEmptyRaster(800, 700, 5, 4, 
0.3, -0.2, -0.1, -0.15, 3857)));
+        assertEquals(-65, env.getMinX(), 1e-9);
+        assertEquals(245, env.getMaxX(), 1e-9);
+        assertEquals(-256, env.getMinY(), 1e-9);
+        assertEquals(4, env.getMaxY(), 1e-9);
+    }
+
     @Test
     public void testNumBands() {
         assertEquals(1, RasterAccessors.numBands(oneBandRaster));
@@ -75,18 +98,18 @@ public class RasterAccessorsTest extends RasterTestBase
 
         gridCoverage2D = RasterConstructors.makeEmptyRaster(10, 7, 8, 5, 6, 9);
         upperLeftY = RasterAccessors.getUpperLeftY(gridCoverage2D);
-        assertEquals(6, upperLeftY, 0.1d);    
+        assertEquals(6, upperLeftY, 0.1d);
     }
 
     @Test
     public void testScaleX() throws UnsupportedOperationException, 
FactoryException {
-        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 10, 
15, 0, 0, 1, 2, 0, 0, 0);
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 10, 
15, 0, 0, 1, -2, 0, 0, 0);
         assertEquals(1, RasterAccessors.getScaleX(emptyRaster), 1e-9);
     }
 
     @Test
     public void testScaleY() throws UnsupportedOperationException, 
FactoryException {
-        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 10, 
15, 0, 0, 1, 2, 0, 0, 0);
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 10, 
15, 0, 0, 1, -2, 0, 0, 0);
         assertEquals(-2, RasterAccessors.getScaleY(emptyRaster), 1e-9);
     }
 
@@ -103,16 +126,16 @@ public class RasterAccessorsTest extends RasterTestBase
 
         GridCoverage2D gridCoverage2D = 
RasterConstructors.makeEmptyRaster(numBands, widthInPixel, heightInPixel, 
upperLeftX, upperLeftY, pixelSize);
         double[] metadata = RasterAccessors.metadata(gridCoverage2D);
-        assertEquals(upperLeftX, metadata[0], 0.1d);
-        assertEquals(upperLeftY, metadata[1], 0.1d);
-        assertEquals(widthInPixel, metadata[2], 0.1d);
-        assertEquals(heightInPixel, metadata[3], 0.1d);
-        assertEquals(pixelSize, metadata[4], 0.1d);
-        assertEquals(-1 * pixelSize, metadata[5], 0.1d);
-        assertEquals(0, metadata[6], 0.1d);
-        assertEquals(0, metadata[7], 0.1d);
-        assertEquals(0, metadata[8], 0.1d);
-        assertEquals(numBands, metadata[9], 0.1d);
+        assertEquals(upperLeftX, metadata[0], 1e-9);
+        assertEquals(upperLeftY, metadata[1], 1e-9);
+        assertEquals(widthInPixel, metadata[2], 1e-9);
+        assertEquals(heightInPixel, metadata[3], 1e-9);
+        assertEquals(pixelSize, metadata[4], 1e-9);
+        assertEquals(-1 * pixelSize, metadata[5], 1e-9);
+        assertEquals(0, metadata[6], 1e-9);
+        assertEquals(0, metadata[7], 1e-9);
+        assertEquals(0, metadata[8], 1e-9);
+        assertEquals(numBands, metadata[9], 1e-9);
         assertEquals(10, metadata.length);
 
         upperLeftX = 5;
@@ -126,17 +149,44 @@ public class RasterAccessorsTest extends RasterTestBase
 
         metadata = RasterAccessors.metadata(gridCoverage2D);
 
-        assertEquals(upperLeftX, metadata[0], 0.1d);
-        assertEquals(upperLeftY, metadata[1], 0.1d);
-        assertEquals(widthInPixel, metadata[2], 0.1d);
-        assertEquals(heightInPixel, metadata[3], 0.1d);
-        assertEquals(pixelSize, metadata[4], 0.1d);
-        assertEquals(-1 * pixelSize, metadata[5], 0.1d);
-        assertEquals(0, metadata[6], 0.1d);
-        assertEquals(0, metadata[7], 0.1d);
-        assertEquals(0, metadata[8], 0.1d);
-        assertEquals(numBands, metadata[9], 0.1d);
+        assertEquals(upperLeftX, metadata[0], 1e-9);
+        assertEquals(upperLeftY, metadata[1], 1e-9);
+        assertEquals(widthInPixel, metadata[2], 1e-9);
+        assertEquals(heightInPixel, metadata[3], 1e-9);
+        assertEquals(pixelSize, metadata[4], 1e-9);
+        assertEquals(-1 * pixelSize, metadata[5], 1e-9);
+        assertEquals(0, metadata[6], 1e-9);
+        assertEquals(0, metadata[7], 1e-9);
+        assertEquals(0, metadata[8], 1e-9);
+        assertEquals(numBands, metadata[9], 1e-9);
+
+        assertEquals(10, metadata.length);
+    }
 
+    @Test
+    public void testMetaDataUsingSkewedRaster() throws FactoryException {
+        int widthInPixel = 3;
+        int heightInPixel = 4;
+        double upperLeftX = 100.0;
+        double upperLeftY = 200.0;
+        double scaleX = 2.0;
+        double scaleY = -3.0;
+        double skewX = 0.1;
+        double skewY = 0.2;
+        int numBands = 1;
+
+        GridCoverage2D gridCoverage2D = 
RasterConstructors.makeEmptyRaster(numBands, widthInPixel, heightInPixel, 
upperLeftX, upperLeftY, scaleX, scaleY, skewX, skewY, 3857);
+        double[] metadata = RasterAccessors.metadata(gridCoverage2D);
+        assertEquals(upperLeftX, metadata[0], 1e-9);
+        assertEquals(upperLeftY, metadata[1], 1e-9);
+        assertEquals(widthInPixel, metadata[2], 1e-9);
+        assertEquals(heightInPixel, metadata[3], 1e-9);
+        assertEquals(scaleX, metadata[4], 1e-9);
+        assertEquals(scaleY, metadata[5], 1e-9);
+        assertEquals(skewX, metadata[6], 1e-9);
+        assertEquals(skewY, metadata[7], 1e-9);
+        assertEquals(3857, metadata[8], 1e-9);
+        assertEquals(numBands, metadata[9], 1e-9);
         assertEquals(10, metadata.length);
     }
 }
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 6f397e7d..5c1c2a47 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
@@ -80,12 +80,11 @@ public class RasterConstructorsTest
         assertEquals(0d, 
gridCoverage2D.getRenderedImage().getData().getPixel(0, 0, (double[])null)[0], 
0.001);
         assertEquals(1, gridCoverage2D.getNumSampleDimensions());
 
-        gridCoverage2D = RasterConstructors.makeEmptyRaster(numBands, 
widthInPixel, heightInPixel, upperLeftX, upperLeftY, pixelSize, pixelSize + 1, 
0, 0, 0);
+        gridCoverage2D = RasterConstructors.makeEmptyRaster(numBands, 
widthInPixel, heightInPixel, upperLeftX, upperLeftY, pixelSize, -pixelSize - 1, 
0, 0, 0);
         envelope = RasterAccessors.envelope(gridCoverage2D);
         assertEquals(upperLeftX, envelope.getEnvelopeInternal().getMinX(), 
0.001);
         assertEquals(upperLeftX + widthInPixel * pixelSize, 
envelope.getEnvelopeInternal().getMaxX(), 0.001);
         assertEquals(upperLeftY - heightInPixel * (pixelSize + 1), 
envelope.getEnvelopeInternal().getMinY(), 0.001);
         assertEquals(upperLeftY, envelope.getEnvelopeInternal().getMaxY(), 
0.001);
-
     }
-}
\ No newline at end of file
+}
diff --git a/docs/api/sql/Raster-loader.md b/docs/api/sql/Raster-loader.md
index c0a15640..ba382fa9 100644
--- a/docs/api/sql/Raster-loader.md
+++ b/docs/api/sql/Raster-loader.md
@@ -80,7 +80,7 @@ Format: `RS_MakeEmptyRaster(numBands:Int, width: Int, height: 
Int, upperleftX: D
 SQL example 1 (with 2 bands):
 
 ```sql
-SELECT RS_MakeEmptyRaster(2, 10, 10, 0.0, 0.0, 1.0) as raster
+SELECT RS_MakeEmptyRaster(2, 10, 10, 0.0, 0.0, 1.0)
 ```
 
 Output:
@@ -95,16 +95,16 @@ Output:
 SQL example 1 (with 2 bands, scale, skew, and SRID):
 
 ```sql
-SELECT RS_MakeEmptyRaster(2, 10, 10, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 4326) as 
raster
+SELECT RS_MakeEmptyRaster(2, 10, 10, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 4326)
 ```
 
 Output:
 ```
-+--------------------------------------------------------------+
-|rs_makeemptyraster(2, 10, 10, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0)|
-+--------------------------------------------------------------+
-|                                          GridCoverage2D["g...|
-+--------------------------------------------------------------+
++------------------------------------------------------------------+
+|rs_makeemptyraster(2, 10, 10, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 4326)|
++------------------------------------------------------------------+
+|                                              GridCoverage2D["g...|
++------------------------------------------------------------------+
 ```
 
 ## Load GeoTiff to Array[Double] format
diff --git a/docs/api/sql/Raster-operators.md b/docs/api/sql/Raster-operators.md
index 5bead932..f2f4f650 100644
--- a/docs/api/sql/Raster-operators.md
+++ b/docs/api/sql/Raster-operators.md
@@ -164,12 +164,12 @@ true
 
 Introduction: Returns the metadata of the raster as an array of double. The 
array contains the following values:
 
-- 0: upper left x coordinate of the raster, in terms of CRS units (the minimum 
x coordinate)
-- 1: upper left y coordinate of the raster, in terms of CRS units (the maximum 
y coordinate)
+- 0: upper left x coordinate of the raster, in terms of CRS units
+- 1: upper left y coordinate of the raster, in terms of CRS units
 - 2: width of the raster, in terms of pixels
 - 3: height of the raster, in terms of pixels
 - 4: width of a pixel, in terms of CRS units (scaleX)
-- 5: height of a pixel, in terms of CRS units (scaleY)
+- 5: height of a pixel, in terms of CRS units (scaleY), may be negative
 - 6: skew in x direction (rotation x)
 - 7: skew in y direction (rotation y)
 - 8: srid of the raster
diff --git 
a/sql/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala 
b/sql/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
index 36d21b73..88d4d33d 100644
--- a/sql/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
+++ b/sql/common/src/test/scala/org/apache/sedona/sql/rasteralgebraTest.scala
@@ -445,7 +445,7 @@ class rasteralgebraTest extends TestBaseScala with 
BeforeAndAfter with GivenWhen
       val skewX = 0.0
       val skewY = 0.0
       val srid = 0
-      result = sparkSession.sql(s"SELECT 
RS_Metadata(RS_MakeEmptyRaster($numBands, $widthInPixel, $heightInPixel, 
$upperLeftX, $upperLeftY, $cellSize, $cellSize, $skewX, $skewY, 
$srid))").first().getSeq(0)
+      result = sparkSession.sql(s"SELECT 
RS_Metadata(RS_MakeEmptyRaster($numBands, $widthInPixel, $heightInPixel, 
$upperLeftX, $upperLeftY, $cellSize, -$cellSize, $skewX, $skewY, 
$srid))").first().getSeq(0)
       assertEquals(numBands, result(9), 0.001)
     }
 

Reply via email to