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)
+ }
}
}