diff --git a/common/src/main/java/org/apache/sedona/common/raster/RasterBandAccessors.java b/common/src/main/java/org/apache/sedona/common/raster/RasterBandAccessors.java new file mode 100644 index 0000000000..f65f3197b7 --- /dev/null +++ b/common/src/main/java/org/apache/sedona/common/raster/RasterBandAccessors.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sedona.common.raster; + +import org.geotools.coverage.GridSampleDimension; +import org.geotools.coverage.grid.GridCoverage2D; + +public class RasterBandAccessors { + + public static Double getBandNoDataValue(GridCoverage2D raster, int band) { + if (band > raster.getNumSampleDimensions()) { + throw new IllegalArgumentException("Provided band index is not present in the raster"); + } + GridSampleDimension bandSampleDimension = raster.getSampleDimension(band - 1); + if (bandSampleDimension.getNoDataValues() == null) return null; + return raster.getSampleDimension(band - 1).getNoDataValues()[0]; + } + + public static Double getBandNoDataValue(GridCoverage2D raster) { + return getBandNoDataValue(raster, 1); + } +} 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 new file mode 100644 index 0000000000..b4de342d82 --- /dev/null +++ b/common/src/test/java/org/apache/sedona/common/raster/RasterBandAccessorsTest.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sedona.common.raster; + +import org.geotools.coverage.grid.GridCoverage2D; +import org.junit.Test; +import org.opengis.referencing.FactoryException; + +import java.io.IOException; + +import static org.junit.Assert.*; + +public class RasterBandAccessorsTest extends RasterTestBase { + + @Test + public void testBandNoDataValueCustomBand() throws FactoryException { + int width = 5, height = 10; + GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, width, height, 53, 51, 1, 1, 0, 0, 4326); + double[] values = new double[width * height]; + for (int i = 0; i < values.length; i++) { + values[i] = i + 1; + } + emptyRaster = MapAlgebra.addBandFromArray(emptyRaster, values, 2, 1d); + assertNotNull(RasterBandAccessors.getBandNoDataValue(emptyRaster, 2)); + assertEquals(1, RasterBandAccessors.getBandNoDataValue(emptyRaster, 2), 1e-9); + assertNull(RasterBandAccessors.getBandNoDataValue(emptyRaster)); + } + + @Test + public void testBandNoDataValueDefaultBand() throws FactoryException { + int width = 5, height = 10; + GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, width, height, 53, 51, 1, 1, 0, 0, 4326); + double[] values = new double[width * height]; + for (int i = 0; i < values.length; i++) { + values[i] = i + 1; + } + emptyRaster = MapAlgebra.addBandFromArray(emptyRaster, values, 1, 1d); + assertNotNull(RasterBandAccessors.getBandNoDataValue(emptyRaster)); + assertEquals(1, RasterBandAccessors.getBandNoDataValue(emptyRaster), 1e-9); + } + + @Test + public void testBandNoDataValueDefaultNoData() throws FactoryException { + int width = 5, height = 10; + GridCoverage2D emptyRaster = RasterConstructors.makeEmptyRaster(1, width, height, 53, 51, 1, 1, 0, 0, 4326); + double[] values = new double[width * height]; + for (int i = 0; i < values.length; i++) { + values[i] = i + 1; + } + assertNull(RasterBandAccessors.getBandNoDataValue(emptyRaster, 1)); + } + + @Test + public void testBandNoDataValueIllegalBand() throws FactoryException, IOException { + GridCoverage2D raster = rasterFromGeoTiff(resourceFolder + "raster/raster_with_no_data/test5.tiff"); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RasterBandAccessors.getBandNoDataValue(raster, 2)); + assertEquals("Provided band index is not present in the raster", exception.getMessage()); + } + +} diff --git a/core/src/test/resources/raster/raster_with_no_data/test5.TIFF b/core/src/test/resources/raster/raster_with_no_data/test5.tiff similarity index 100% rename from core/src/test/resources/raster/raster_with_no_data/test5.TIFF rename to core/src/test/resources/raster/raster_with_no_data/test5.tiff diff --git a/docs/api/sql/Raster-operators.md b/docs/api/sql/Raster-operators.md index 718de0ed18..4d009478af 100644 --- a/docs/api/sql/Raster-operators.md +++ b/docs/api/sql/Raster-operators.md @@ -309,6 +309,38 @@ Output: `3` !!!Tip For non-skewed rasters, you can provide any value for longitude and the intended value of world latitude, to get the desired answer +## Raster Band Accessors + +### RS_BandNoDataValue + +Introduction: Returns the no data value of the given band of the given raster. If no band is given, band 1 is assumed. The band parameter is 1-indexed. If there is no no data value associated with the given band, RS_BandNoDataValue returns null. + +!!!Note + If the given band does not lie in the raster, RS_BandNoDataValue throws an IllegalArgumentException + +Format: `RS_BandNoDataValue (raster: Raster, band: Int = 1)` + +Since: `1.5.0` + +Spark SQL example: +```sql +SELECT RS_BandNoDataValue(raster, 1) from rasters; +``` + +Output: `0.0` + +```sql +SELECT RS_BandNoDataValue(raster) from rasters_without_nodata; +``` + +Output: `null` + +```sql +SELECT RS_BandNoDataValue(raster, 3) from rasters; +``` + +Output: `IllegalArgumentException: Provided band index is not present in the raster.` + ## Raster based operators 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 d48999ba44..5c99689aef 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 @@ -222,7 +222,8 @@ object Catalog { function[RS_Contains](), function[RS_WorldToRasterCoord](), function[RS_WorldToRasterCoordX](), - function[RS_WorldToRasterCoordY]() + function[RS_WorldToRasterCoordY](), + function[RS_BandNoDataValue]() ) val aggregateExpressions: Seq[Aggregator[Geometry, Geometry, Geometry]] = Seq( diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterAccessors.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterAccessors.scala index ff57a4df65..add8eaf3e5 100644 --- a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterAccessors.scala +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterAccessors.scala @@ -18,7 +18,7 @@ */ package org.apache.spark.sql.sedona_sql.expressions.raster -import org.apache.sedona.common.raster.{GeometryFunctions, RasterAccessors} +import org.apache.sedona.common.raster.RasterAccessors import org.apache.spark.sql.catalyst.expressions.Expression import org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._ import org.apache.spark.sql.sedona_sql.expressions.InferredExpression diff --git a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandAccessors.scala b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandAccessors.scala new file mode 100644 index 0000000000..b3dce5820d --- /dev/null +++ b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterBandAccessors.scala @@ -0,0 +1,32 @@ +/* + * 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.spark.sql.sedona_sql.expressions.raster + +import org.apache.sedona.common.raster.RasterBandAccessors +import org.apache.spark.sql.catalyst.expressions.Expression +import org.apache.spark.sql.sedona_sql.expressions.InferrableFunctionConverter._ +import org.apache.spark.sql.sedona_sql.expressions.InferredExpression + +case class RS_BandNoDataValue(inputExpressions: Seq[Expression]) extends InferredExpression(inferrableFunction2(RasterBandAccessors.getBandNoDataValue), inferrableFunction1(RasterBandAccessors.getBandNoDataValue)) { + protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) = { + copy(inputExpressions = newChildren) + } +} + 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 91207d647a..23fa501022 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 @@ -21,7 +21,7 @@ package org.apache.sedona.sql import org.apache.spark.sql.connector.catalog.TableChange.ColumnPosition.first import org.apache.spark.sql.functions.{collect_list, expr} import org.geotools.coverage.grid.GridCoverage2D -import org.junit.Assert.assertEquals +import org.junit.Assert.{assertEquals, assertNull} import org.locationtech.jts.geom.{Coordinate, Geometry} import org.scalatest.{BeforeAndAfter, GivenWhenThen} @@ -671,5 +671,27 @@ class rasteralgebraTest extends TestBaseScala with BeforeAndAfter with GivenWhen assert(sparkSession.sql("SELECT RS_WITHIN(RS_MakeEmptyRaster(1, 20, 20, 2, 22, 1), ST_GeomFromWKT('POLYGON ((0 0, 0 50, 100 50, 100 0, 0 0))'))").first().getBoolean(0)) assert(!sparkSession.sql("SELECT RS_WITHIN(RS_MakeEmptyRaster(1, 100, 100, 0, 50, 1), ST_GeomFromWKT('POLYGON ((2 2, 2 25, 20 25, 20 2, 2 2))'))").first().getBoolean(0)) } + + it("Passed RS_BandNoDataValue - noDataValueFor for raster from geotiff - default band") { + var df = sparkSession.read.format("binaryFile").load(resourceFolder + "raster/raster_with_no_data/test5.tiff") + df = df.selectExpr("RS_FromGeoTiff(content) as raster") + val result = df.selectExpr("RS_BandNoDataValue(raster)").first().getDouble(0) + assertEquals(0, result, 1e-9) + } + + it("Passed RS_BandNoDataValue - null noDataValueFor for raster from geotiff") { + var df = sparkSession.read.format("binaryFile").load(resourceFolder + "raster/test1.tiff") + df = df.selectExpr("RS_FromGeoTiff(content) as raster") + val result = df.selectExpr("RS_BandNoDataValue(raster)").first().get(0) + assertNull(result) + } + + it("Passed RS_BandNoDataValue - noDataValueFor for raster from geotiff - explicit band") { + var df = sparkSession.read.format("binaryFile").load(resourceFolder + "raster/raster_with_no_data/test5.tiff") + df = df.selectExpr("RS_FromGeoTiff(content) as raster") + val result = df.selectExpr("RS_BandNoDataValue(raster, 1)").first().getDouble(0) + assertEquals(0, result, 1e-9) + } + } }