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 d56fa57c [SEDONA-335] Add RS_PixelAsPoint (#933)
d56fa57c is described below

commit d56fa57cf3e1c7538f1b1d80aa145582457a223b
Author: Nilesh Gajwani <[email protected]>
AuthorDate: Wed Aug 2 13:23:00 2023 -0700

    [SEDONA-335] Add RS_PixelAsPoint (#933)
---
 .../sedona/common/raster/PixelFunctions.java       | 33 ++++++++++-
 .../apache/sedona/common/raster/FunctionsTest.java | 67 ++++++++++++++++++++--
 .../sedona/common/raster/RasterAccessorsTest.java  | 12 +++-
 .../sedona/common/raster/RasterTestBase.java       | 12 ++++
 docs/api/sql/Raster-operators.md                   | 34 +++++++++++
 .../scala/org/apache/sedona/sql/UDF/Catalog.scala  |  3 +-
 .../expressions/raster/PixelFunctions.scala        |  6 ++
 .../org/apache/sedona/sql/rasteralgebraTest.scala  | 15 +++++
 8 files changed, 174 insertions(+), 8 deletions(-)

diff --git 
a/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java 
b/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
index 64a591d3..5ac5561e 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
@@ -18,14 +18,19 @@
  */
 package org.apache.sedona.common.raster;
 
+import org.geotools.coverage.grid.GridCoordinates2D;
 import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.coverage.grid.GridEnvelope2D;
+import org.geotools.coverage.grid.GridGeometry2D;
 import org.geotools.geometry.DirectPosition2D;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.Point;
+import org.geotools.referencing.CRS;
+import org.locationtech.jts.geom.*;
 import org.opengis.coverage.PointOutsideCoverageException;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.metadata.spatial.PixelOrientation;
 import org.opengis.referencing.operation.TransformException;
 
+import java.awt.geom.Point2D;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -35,11 +40,35 @@ import java.util.stream.DoubleStream;
 
 public class PixelFunctions
 {
+    private static GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
     public static Double value(GridCoverage2D rasterGeom, Geometry geometry, 
int band) throws TransformException
     {
         return values(rasterGeom, Collections.singletonList(geometry), 
band).get(0);
     }
 
+    public static Geometry getPixelAsPoint(GridCoverage2D raster, int colX, 
int rowY) throws TransformException {
+        GridGeometry2D gridGeometry2D = raster.getGridGeometry();
+        GridEnvelope2D gridEnvelope2D = gridGeometry2D.getGridRange2D();
+        GridCoordinates2D gridCoordinates2D = new GridCoordinates2D(colX - 1, 
rowY - 1);
+        int srid = 0;
+        String srs = CRS.toSRS(raster.getCoordinateReferenceSystem2D(), true);
+        if (!"Generic cartesian 2D".equalsIgnoreCase(srs)) {
+           srid = Integer.parseInt(srs);
+        }
+        if (gridEnvelope2D.contains(gridCoordinates2D)) {
+            Point2D point2D = 
gridGeometry2D.getGridToCRS2D(PixelOrientation.UPPER_LEFT).transform(gridCoordinates2D,
 null);
+            DirectPosition2D directPosition2D = new 
DirectPosition2D(gridGeometry2D.getCoordinateReferenceSystem2D(), 
point2D.getX(), point2D.getY());
+            Coordinate pointCoord = new Coordinate(directPosition2D.getX(), 
directPosition2D.getY());
+            if (srid != 0) {
+                GeometryFactory factory = new GeometryFactory(new 
PrecisionModel(), srid);
+                return factory.createPoint(pointCoord);
+            }
+            return GEOMETRY_FACTORY.createPoint(pointCoord);
+        }else {
+            throw new IndexOutOfBoundsException(String.format("Specified pixel 
coordinates (%d, %d) do not lie in the raster", colX, rowY));
+        }
+    }
+
     public static List<Double> values(GridCoverage2D rasterGeom, 
List<Geometry> geometries, int band) throws TransformException {
         int numBands = rasterGeom.getNumSampleDimensions();
         if (band < 1 || band > numBands) {
diff --git 
a/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java 
b/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
index 287d6456..d98441f5 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/FunctionsTest.java
@@ -22,14 +22,12 @@ import org.locationtech.jts.geom.Point;
 import org.opengis.referencing.FactoryException;
 import org.opengis.referencing.operation.TransformException;
 
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 public class FunctionsTest extends RasterTestBase {
 
@@ -67,6 +65,67 @@ public class FunctionsTest extends RasterTestBase {
         assertEquals(255d, PixelFunctions.value(multiBandRaster, 
point(4.5d,4.5d), 4), 0.1d);
     }
 
+    @Test
+    public void testPixelAsPointUpperLeft() throws FactoryException, 
TransformException {
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, 5, 
10, 123, -230, 8);
+        Geometry actualPoint = PixelFunctions.getPixelAsPoint(emptyRaster, 1, 
1);
+        Coordinate coordinates = actualPoint.getCoordinate();
+        assertEquals(123, coordinates.x, 1e-9);
+        assertEquals(-230, coordinates.y, 1e-9);
+        assertEquals(0, actualPoint.getSRID());
+    }
+
+    @Test
+    public void testPixelAsPointMiddle() throws FactoryException, 
TransformException {
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 5, 
10, 123, -230, 8);
+        Geometry actualPoint = PixelFunctions.getPixelAsPoint(emptyRaster, 3, 
5);
+        Coordinate coordinates = actualPoint.getCoordinate();
+        assertEquals(139, coordinates.x, 1e-9);
+        assertEquals(-262, coordinates.y, 1e-9);
+        assertEquals(0, actualPoint.getSRID());
+    }
+
+    @Test
+    public void testPixelAsPointCustomSRIDPlanar() throws FactoryException, 
TransformException {
+        int srid = 3857;
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, 5, 
5, -123, 54, 5, 5, 0, 0, srid);
+        Geometry actualPoint = PixelFunctions.getPixelAsPoint(emptyRaster, 1, 
1);
+        Coordinate coordinates = actualPoint.getCoordinate();
+        assertEquals(-123, coordinates.x, 1e-9);
+        assertEquals(54, coordinates.y, 1e-9);
+        assertEquals(srid, actualPoint.getSRID());
+    }
+
+    @Test
+    public void testPixelAsPointSRIDSpherical() throws FactoryException, 
TransformException {
+        int srid = 4326;
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, 5, 
10, -123, 54, 5, -10, 0, 0, srid);
+        Geometry actualPoint = PixelFunctions.getPixelAsPoint(emptyRaster, 2, 
3);
+        Coordinate coordinates = actualPoint.getCoordinate();
+        assertEquals(-118, coordinates.x, 1e-9);
+        assertEquals(34, coordinates.y, 1e-9);
+        assertEquals(srid, actualPoint.getSRID());
+    }
+
+    @Test
+    public void testPixelAsPointOutOfBounds() throws FactoryException {
+        GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 5, 
10, 123, -230, 8);
+        Exception e = assertThrows(IndexOutOfBoundsException.class, () -> 
PixelFunctions.getPixelAsPoint(emptyRaster, 6, 1));
+        String expectedMessage = "Specified pixel coordinates (6, 1) do not 
lie in the raster";
+        assertEquals(expectedMessage, e.getMessage());
+    }
+
+    @Test
+    public void testPixelAsPointFromRasterFile() throws IOException, 
TransformException {
+        GridCoverage2D raster = rasterFromGeoTiff(resourceFolder + 
"raster/test1.tiff");
+        Geometry actualPoint = PixelFunctions.getPixelAsPoint(raster, 1, 1);
+        Coordinate coordinate = actualPoint.getCoordinate();
+        double expectedX = -13095817.809482181;
+        double expectedY = 4021262.7487925636;
+        assertEquals(expectedX, coordinate.getX(), 0.2d);
+        assertEquals(expectedY, coordinate.getY(), 0.2d);
+    }
+
     @Test
     public void values() throws TransformException {
         // The function 'value' is implemented using 'values'.
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 af9a487a..c6557c02 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
@@ -24,6 +24,8 @@ import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.opengis.referencing.FactoryException;
 
+import java.io.IOException;
+
 import static org.junit.Assert.assertEquals;
 
 public class RasterAccessorsTest extends RasterTestBase
@@ -71,6 +73,14 @@ public class RasterAccessorsTest extends RasterTestBase
         GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, 10, 
20, 0, 0, 8);
         assertEquals(20, RasterAccessors.getHeight(emptyRaster));
         assertEquals(10, RasterAccessors.getWidth(emptyRaster));
+
+    }
+
+    @Test
+    public void testWidthAndHeightFromRasterFile() throws IOException {
+        GridCoverage2D raster = rasterFromGeoTiff(resourceFolder + 
"raster/test1.tiff");
+        assertEquals(512, RasterAccessors.getWidth(raster));
+        assertEquals(517, RasterAccessors.getHeight(raster));
     }
 
     @Test
@@ -112,7 +122,7 @@ public class RasterAccessorsTest extends RasterTestBase
         GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(2, 10, 
15, 0, 0, 1, -2, 0, 0, 0);
         assertEquals(-2, RasterAccessors.getScaleY(emptyRaster), 1e-9);
     }
-
+    
     @Test
     public void testMetaData()
             throws FactoryException
diff --git 
a/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java 
b/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java
index d75a34b9..4bd37374 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java
@@ -15,6 +15,8 @@ package org.apache.sedona.common.raster;
 
 import org.geotools.coverage.grid.GridCoverage2D;
 import org.geotools.coverage.grid.GridCoverageFactory;
+import org.geotools.data.DataSourceException;
+import org.geotools.gce.geotiff.GeoTiffReader;
 import org.geotools.gce.geotiff.GeoTiffWriter;
 import org.geotools.geometry.Envelope2D;
 import org.geotools.geometry.jts.ReferencedEnvelope;
@@ -30,11 +32,15 @@ import java.awt.Color;
 import java.awt.image.BufferedImage;
 import java.awt.image.WritableRaster;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 
 public class RasterTestBase {
     String arc = "NCOLS 2\nNROWS 2\nXLLCORNER 378922\nYLLCORNER 
4072345\nCELLSIZE 30\nNODATA_VALUE 0\n0 1 2 3\n";
+
+    String resourceFolder = System.getProperty("user.dir") + 
"/../core/src/test/resources/";
+
     GridCoverage2D oneBandRaster;
     GridCoverage2D multiBandRaster;
     byte[] geoTiff;
@@ -101,4 +107,10 @@ public class RasterTestBase {
         }
         return factory.create("test", image, new 
Envelope2D(DefaultGeographicCRS.WGS84, 0, 0, 10, 10));
     }
+
+    GridCoverage2D rasterFromGeoTiff(String filePath) throws IOException {
+        File geoTiffFile = new File(filePath);
+        GridCoverage2D raster = new GeoTiffReader(geoTiffFile).read(null);
+        return raster;
+    }
 }
diff --git a/docs/api/sql/Raster-operators.md b/docs/api/sql/Raster-operators.md
index f2f4f650..43528767 100644
--- a/docs/api/sql/Raster-operators.md
+++ b/docs/api/sql/Raster-operators.md
@@ -1,3 +1,37 @@
+## Pixel Functions
+
+### RS_PixelAsPoint
+
+Introduction: Returns a point geometry of the specified pixel's upper-left 
corner. The pixel coordinates specified are 1-indexed.
+
+!!!Note
+    If the pixel coordinates specified do not exist in the raster (out of 
bounds), RS_PixelAsPoint throws an IndexOutOfBoundsException.
+
+
+Format: `RS_PixelAsPoint(raster: Raster, colX: int, rowY: int)`
+
+Since: `1.5.0`
+
+Spark SQL examples:
+
+```sql
+SELECT ST_AsText(RS_PixelAsPoint(raster, 2, 1)) from rasters
+```
+
+Output: 
+```
+POINT (123.19, -12)
+```
+
+```sql
+SELECT ST_AsText(RS_PixelAsPoint(raster, 6, 2)) from rasters
+```
+
+Output:
+```
+IndexOutOfBoundsException: Specified pixel coordinates (6, 2) do not lie in 
the raster
+```
+
 ## Raster Accessors
 
 ### RS_Height
diff --git a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala 
b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
index 19510e1b..f9259db1 100644
--- a/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
+++ b/sql/common/src/main/scala/org/apache/sedona/sql/UDF/Catalog.scala
@@ -210,7 +210,8 @@ object Catalog {
     function[RS_UpperLeftX](),
     function[RS_UpperLeftY](),
     function[RS_ScaleX](),
-    function[RS_ScaleY]()
+    function[RS_ScaleY](),
+    function[RS_PixelAsPoint]()
   )
 
   val aggregateExpressions: Seq[Aggregator[Geometry, Geometry, Geometry]] = 
Seq(
diff --git 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/PixelFunctions.scala
 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/PixelFunctions.scala
index 97b9e673..ebf7aa6f 100644
--- 
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/PixelFunctions.scala
+++ 
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/PixelFunctions.scala
@@ -36,6 +36,12 @@ case class RS_Value(inputExpressions: Seq[Expression]) 
extends InferredExpressio
   }
 }
 
+case class RS_PixelAsPoint(inputExpressions: Seq[Expression]) extends 
InferredExpression(PixelFunctions.getPixelAsPoint _) {
+  protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = 
{
+    copy(inputExpressions = newChildren)
+  }
+}
+
 case class RS_Values(inputExpressions: Seq[Expression]) extends Expression 
with CodegenFallback with ExpectsInputTypes {
 
   override def nullable: Boolean = true
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 88d4d33d..5a80a2c0 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
@@ -516,5 +516,20 @@ class rasteralgebraTest extends TestBaseScala with 
BeforeAndAfter with GivenWhen
       val expected: Double = -72.32861272132695
       assertEquals(expected, result, 1e-9)
     }
+
+    it("Passed RS_PixelAsPoint with raster") {
+      val widthInPixel = 5
+      val heightInPixel = 10
+      val upperLeftX = 123.19
+      val upperLeftY = -12
+      val cellSize = 4
+      val numBands = 2
+      val result = sparkSession.sql(s"SELECT 
RS_PixelAsPoint(RS_MakeEmptyRaster($numBands, $widthInPixel, $heightInPixel, 
$upperLeftX, $upperLeftY, $cellSize), 2, 1)").first().getAs[Geometry](0);
+      val expectedX = 127.19
+      val expectedY = -12
+      val actualCoordinates = result.getCoordinate;
+      assertEquals(expectedX, actualCoordinates.x, 1e-5)
+      assertEquals(expectedY, actualCoordinates.y, 1e-5)
+    }
   }
 }

Reply via email to