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 fa76e52b [SEDONA-292] Bridge Sedona Raster and Map Algebra operators
(#857)
fa76e52b is described below
commit fa76e52ba2befbb2e279ff638cb9e2784bc1a1ab
Author: Jia Yu <[email protected]>
AuthorDate: Tue Jun 13 21:27:38 2023 -0700
[SEDONA-292] Bridge Sedona Raster and Map Algebra operators (#857)
---
.../apache/sedona/common/raster/Constructors.java | 38 ------
.../org/apache/sedona/common/raster/Functions.java | 125 -----------------
.../apache/sedona/common/raster/MapAlgebra.java | 147 ++++++++++++++++++++
.../sedona/common/raster/PixelFunctions.java | 86 ++++++++++++
.../sedona/common/raster/RasterAccessors.java | 102 ++++++++++++++
.../sedona/common/raster/RasterConstructors.java | 118 ++++++++++++++++
.../apache/sedona/common/raster/RasterEditors.java | 44 ++++++
.../raster/{Outputs.java => RasterOutputs.java} | 2 +-
.../org/apache/sedona/common/raster/Serde.java | 7 +-
.../sedona/common/raster/ConstructorsTest.java | 55 --------
.../apache/sedona/common/raster/FunctionsTest.java | 65 ++++-----
.../sedona/common/raster/MapAlgebraTest.java | 150 +++++++++++++++++++++
.../sedona/common/raster/RasterAccessorsTest.java | 102 ++++++++++++++
.../common/raster/RasterConstructorsTest.java | 91 +++++++++++++
.../sedona/common/raster/RasterTestBase.java | 15 ++-
.../org/apache/sedona/common/raster/SerdeTest.java | 3 +-
docs/api/sql/Raster-loader.md | 63 +++++++++
docs/api/sql/Raster-operators.md | 101 +++++++++++++-
docs/api/sql/Raster-writer.md | 3 +
.../scala/org/apache/sedona/sql/UDF/Catalog.scala | 4 +
.../raster/{Functions.scala => MapAlgebra.scala} | 144 ++++----------------
.../expressions/raster/PixelFunctions.scala | 85 ++++++++++++
.../expressions/raster/RasterAccessors.scala | 121 +++++++++++++++++
...Constructors.scala => RasterConstructors.scala} | 44 +++++-
.../{Constructors.scala => RasterEditors.scala} | 51 ++-----
.../raster/{Outputs.scala => RasterOutputs.scala} | 12 +-
.../org/apache/sedona/sql/rasteralgebraTest.scala | 64 ++++++++-
.../org/apache/sedona/sql/serdeAwareTest.scala | 2 +-
28 files changed, 1396 insertions(+), 448 deletions(-)
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/Constructors.java
b/common/src/main/java/org/apache/sedona/common/raster/Constructors.java
deleted file mode 100644
index e39a80b7..00000000
--- a/common/src/main/java/org/apache/sedona/common/raster/Constructors.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Licensed 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
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * 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.geotools.gce.arcgrid.ArcGridReader;
-import org.geotools.gce.geotiff.GeoTiffReader;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-
-public class Constructors {
-
- public static GridCoverage2D fromArcInfoAsciiGrid(byte[] bytes) throws
IOException {
- try (ByteArrayInputStream inputStream = new
ByteArrayInputStream(bytes)) {
- ArcGridReader reader = new ArcGridReader(inputStream);
- return reader.read(null);
- }
- }
-
- public static GridCoverage2D fromGeoTiff(byte[] bytes) throws IOException {
- try (ByteArrayInputStream inputStream = new
ByteArrayInputStream(bytes)) {
- GeoTiffReader geoTiffReader = new GeoTiffReader(inputStream);
- return geoTiffReader.read(null);
- }
- }
-}
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/Functions.java
b/common/src/main/java/org/apache/sedona/common/raster/Functions.java
deleted file mode 100644
index 809f9b33..00000000
--- a/common/src/main/java/org/apache/sedona/common/raster/Functions.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/**
- * Licensed 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
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * 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.CoverageFactoryFinder;
-import org.geotools.coverage.grid.GridCoordinates2D;
-import org.geotools.coverage.grid.GridCoverage2D;
-import org.geotools.coverage.grid.GridCoverageFactory;
-import org.geotools.coverage.grid.GridGeometry2D;
-import org.geotools.gce.geotiff.GeoTiffWriter;
-import org.geotools.geometry.DirectPosition2D;
-import org.geotools.geometry.Envelope2D;
-import org.geotools.geometry.jts.ReferencedEnvelope;
-import org.geotools.referencing.CRS;
-import org.geotools.referencing.crs.DefaultEngineeringCRS;
-import org.locationtech.jts.geom.*;
-import org.opengis.referencing.FactoryException;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.operation.TransformException;
-
-import java.awt.image.Raster;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.DoublePredicate;
-import java.util.stream.Collectors;
-import java.util.stream.DoubleStream;
-
-public class Functions {
-
- public static Geometry envelope(GridCoverage2D raster) throws
FactoryException {
- Envelope2D envelope2D = raster.getEnvelope2D();
-
- Envelope envelope = new Envelope(envelope2D.getMinX(),
envelope2D.getMaxX(), envelope2D.getMinY(), envelope2D.getMaxY());
- int srid = srid(raster);
- return new GeometryFactory(new PrecisionModel(),
srid).toGeometry(envelope);
- }
-
- public static int numBands(GridCoverage2D raster) {
- return raster.getNumSampleDimensions();
- }
-
- public static GridCoverage2D setSrid(GridCoverage2D raster, int srid)
throws FactoryException {
- CoordinateReferenceSystem crs;
- if (srid == 0) {
- crs = DefaultEngineeringCRS.CARTESIAN_2D;
- } else {
- crs = CRS.decode("EPSG:" + srid);
- }
- ReferencedEnvelope referencedEnvelope = new
ReferencedEnvelope(raster.getEnvelope2D(), crs);
- GridCoverageFactory gridCoverageFactory =
CoverageFactoryFinder.getGridCoverageFactory(null);
- return gridCoverageFactory.create(raster.getName().toString(),
raster.getRenderedImage(), referencedEnvelope);
- }
-
- public static int srid(GridCoverage2D raster) throws FactoryException {
- CoordinateReferenceSystem crs = raster.getCoordinateReferenceSystem();
- if (crs instanceof DefaultEngineeringCRS) {
- // GeoTools defaults to internal non-standard epsg codes, like
404000, if crs is missing.
- // We need to check for this case and return 0 instead.
- if (((DefaultEngineeringCRS) crs).isWildcard()) {
- return 0;
- }
- }
- return Optional.ofNullable(CRS.lookupEpsgCode(crs, true)).orElse(0);
- }
-
- public static Double value(GridCoverage2D rasterGeom, Geometry geometry,
int band) throws TransformException {
- return values(rasterGeom, Collections.singletonList(geometry),
band).get(0);
- }
-
- public static List<Double> values(GridCoverage2D rasterGeom,
List<Geometry> geometries, int band) throws TransformException {
- int numBands = rasterGeom.getNumSampleDimensions();
- if (band < 1 || band > numBands) {
- // Invalid band index. Return nulls.
- return geometries.stream().map(geom -> (Double)
null).collect(Collectors.toList());
- }
- Raster raster = rasterGeom.getRenderedImage().getData();
- GridGeometry2D gridGeometry = rasterGeom.getGridGeometry();
- double[] noDataValues = rasterGeom.getSampleDimension(band -
1).getNoDataValues();
- DoublePredicate isNoData = d -> noDataValues != null &&
DoubleStream.of(noDataValues).anyMatch(noDataValue ->
Double.compare(noDataValue, d) == 0);
- double[] pixelBuffer = new double[numBands];
-
- List<Double> result = new ArrayList<>(geometries.size());
- for (Geometry geom : geometries) {
- if (geom == null) {
- result.add(null);
- } else {
- Point point = ensurePoint(geom);
- DirectPosition2D directPosition2D = new
DirectPosition2D(point.getX(), point.getY());
- GridCoordinates2D gridCoordinates2D =
gridGeometry.worldToGrid(directPosition2D);
- try {
- double pixel = raster.getPixel(gridCoordinates2D.x,
gridCoordinates2D.y, pixelBuffer)[band - 1];
- if (isNoData.test(pixel)) {
- result.add(null);
- } else {
- result.add(pixel);
- }
- } catch (ArrayIndexOutOfBoundsException exc) {
- // Points outside the extent should return null
- result.add(null);
- }
- }
- }
- return result;
- }
-
- private static Point ensurePoint(Geometry geometry) {
- if (geometry instanceof Point) {
- return (Point) geometry;
- }
- throw new IllegalArgumentException("Attempting to get the value of a
pixel with a non-point geometry.");
- }
-}
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
new file mode 100644
index 00000000..0a24cce2
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/raster/MapAlgebra.java
@@ -0,0 +1,147 @@
+/*
+ * 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.CoverageFactoryFinder;
+import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.coverage.grid.GridCoverageFactory;
+
+import javax.media.jai.RasterFactory;
+
+import java.awt.Point;
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+
+public class MapAlgebra
+{
+ /**
+ * Returns the values of the given band as a 1D array.
+ * @param rasterGeom
+ * @param bandIndex starts at 1
+ * @return
+ */
+ public static double[] bandAsArray(GridCoverage2D rasterGeom, int
bandIndex) {
+ int numBands = rasterGeom.getNumSampleDimensions();
+ if (bandIndex < 1 || bandIndex > numBands) {
+ // Invalid band index. Return nulls.
+ return null;
+ }
+
+ Raster raster = rasterGeom.getRenderedImage().getData();
+ // Get the width and height of the raster
+ int width = raster.getWidth();
+ int height = raster.getHeight();
+
+ // Array to hold the band values
+ double[] bandValues = new double[width * height];
+
+ // Iterate over each pixel
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ // Get the value and store it in the 1D array
+ bandValues[y * width + x] = raster.getSample(x, y, bandIndex -
1);
+ }
+ }
+ return bandValues;
+ }
+
+ /**
+ * Adds a new band to the given raster, using the given array as the band
values.
+ * @param rasterGeom
+ * @param bandValues
+ * @param bandIndex starts at 1, and no larger than numBands + 1
+ * @return
+ */
+ public static GridCoverage2D addBandFromArray(GridCoverage2D rasterGeom,
double[] bandValues, int bandIndex) {
+ int numBands = rasterGeom.getNumSampleDimensions();
+ // Allow the band index to be one larger than the number of bands,
which will append the band to the end
+ if (bandIndex < 1 || bandIndex > numBands + 1) {
+ throw new IllegalArgumentException("Band index is out of bounds.
Must be between 1 and " + (numBands + 1) + ")");
+ }
+
+ if (bandIndex == numBands + 1) {
+ return copyRasterAndAppendBand(rasterGeom, bandValues);
+ }
+ else {
+ return copyRasterAndReplaceBand(rasterGeom, bandIndex, bandValues);
+ }
+ }
+
+
+ /**
+ * Adds a new band to the given raster, using the given array as the band
values. The new band is appended to the end.
+ * @param rasterGeom
+ * @param bandValues
+ * @return
+ */
+ public static GridCoverage2D addBandFromArray(GridCoverage2D rasterGeom,
double[] bandValues) {
+ return addBandFromArray(rasterGeom, bandValues,
rasterGeom.getNumSampleDimensions() + 1);
+ }
+
+ /**
+ * This is an experimental method as it does not copy the original raster
properties (e.g. color model, sample model, etc.)
+ * TODO: Copy the original raster properties
+ * @param gridCoverage2D
+ * @param bandValues
+ * @return
+ */
+ private static GridCoverage2D copyRasterAndAppendBand(GridCoverage2D
gridCoverage2D, double[] bandValues) {
+ // Get the original image and its properties
+ RenderedImage originalImage = gridCoverage2D.getRenderedImage();
+ Raster raster = originalImage.getData();
+ Point location =
gridCoverage2D.getRenderedImage().getData().getBounds().getLocation();
+ WritableRaster wr =
RasterFactory.createBandedRaster(raster.getDataBuffer().getDataType(),
originalImage.getWidth(), originalImage.getHeight(),
gridCoverage2D.getNumSampleDimensions() + 1, location);
+ // Copy the raster data and append the new band values
+ for (int i = 0; i < raster.getWidth(); i++) {
+ for (int j = 0; j < raster.getHeight(); j++) {
+ double[] pixels = raster.getPixel(i, j, (double[]) null);
+ double[] copiedPixels = new double[pixels.length + 1];
+ System.arraycopy(pixels, 0, copiedPixels, 0, pixels.length);
+ copiedPixels[pixels.length] = bandValues[j * raster.getWidth()
+ i];
+ wr.setPixel(i, j, copiedPixels);
+ }
+ }
+ // Create a new GridCoverage2D with the copied image
+ GridCoverageFactory gridCoverageFactory =
CoverageFactoryFinder.getGridCoverageFactory(null);
+ return gridCoverageFactory.create(gridCoverage2D.getName(), wr,
gridCoverage2D.getEnvelope());
+ }
+
+ private static GridCoverage2D copyRasterAndReplaceBand(GridCoverage2D
gridCoverage2D, int bandIndex, double[] bandValues) {
+ // Do not allow the band index to be out of bounds
+ if (bandIndex < 1 || bandIndex >
gridCoverage2D.getNumSampleDimensions()) {
+ throw new IllegalArgumentException("Band index is out of bounds.
Must be between 1 and " + gridCoverage2D.getNumSampleDimensions() + ")");
+ }
+ // Get the original image and its properties
+ RenderedImage originalImage = gridCoverage2D.getRenderedImage();
+ Raster raster = originalImage.getData();
+ WritableRaster wr = raster.createCompatibleWritableRaster();
+ // Copy the raster data and replace the band values
+ for (int i = 0; i < raster.getWidth(); i++) {
+ for (int j = 0; j < raster.getHeight(); j++) {
+ double[] bands = raster.getPixel(i, j, (double[]) null);
+ bands[bandIndex - 1] = bandValues[j * raster.getWidth() + i];
+ wr.setPixel(i, j, bands);
+ }
+ }
+ // Create a new GridCoverage2D with the copied image
+ GridCoverageFactory gridCoverageFactory =
CoverageFactoryFinder.getGridCoverageFactory(null);
+ return gridCoverageFactory.create(gridCoverage2D.getName(), wr,
gridCoverage2D.getEnvelope());
+ }
+}
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
new file mode 100644
index 00000000..6d6ca6c3
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/raster/PixelFunctions.java
@@ -0,0 +1,86 @@
+/*
+ * 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.GridCoordinates2D;
+import org.geotools.coverage.grid.GridCoverage2D;
+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.opengis.referencing.operation.TransformException;
+
+import java.awt.image.Raster;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.DoublePredicate;
+import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
+
+public class PixelFunctions
+{
+ public static Double value(GridCoverage2D rasterGeom, Geometry geometry,
int band) throws TransformException
+ {
+ return values(rasterGeom, Collections.singletonList(geometry),
band).get(0);
+ }
+
+ public static List<Double> values(GridCoverage2D rasterGeom,
List<Geometry> geometries, int band) throws TransformException {
+ int numBands = rasterGeom.getNumSampleDimensions();
+ if (band < 1 || band > numBands) {
+ // Invalid band index. Return nulls.
+ return geometries.stream().map(geom -> (Double)
null).collect(Collectors.toList());
+ }
+ Raster raster = rasterGeom.getRenderedImage().getData();
+ GridGeometry2D gridGeometry = rasterGeom.getGridGeometry();
+ double[] noDataValues = rasterGeom.getSampleDimension(band -
1).getNoDataValues();
+ DoublePredicate isNoData = d -> noDataValues != null &&
DoubleStream.of(noDataValues).anyMatch(noDataValue ->
Double.compare(noDataValue, d) == 0);
+ double[] pixelBuffer = new double[numBands];
+
+ List<Double> result = new ArrayList<>(geometries.size());
+ for (Geometry geom : geometries) {
+ if (geom == null) {
+ result.add(null);
+ } else {
+ Point point = ensurePoint(geom);
+ DirectPosition2D directPosition2D = new
DirectPosition2D(point.getX(), point.getY());
+ GridCoordinates2D gridCoordinates2D =
gridGeometry.worldToGrid(directPosition2D);
+ try {
+ double pixel = raster.getPixel(gridCoordinates2D.x,
gridCoordinates2D.y, pixelBuffer)[band - 1];
+ if (isNoData.test(pixel)) {
+ result.add(null);
+ } else {
+ result.add(pixel);
+ }
+ } catch (ArrayIndexOutOfBoundsException exc) {
+ // Points outside the extent should return null
+ result.add(null);
+ }
+ }
+ }
+ return result;
+ }
+
+ private static Point ensurePoint(Geometry geometry) {
+ if (geometry instanceof Point) {
+ return (Point) geometry;
+ }
+ throw new IllegalArgumentException("Attempting to get the value of a
pixel with a non-point geometry.");
+ }
+}
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
new file mode 100644
index 00000000..9f37aea6
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterAccessors.java
@@ -0,0 +1,102 @@
+/*
+ * 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.geotools.geometry.Envelope2D;
+import org.geotools.referencing.CRS;
+import org.geotools.referencing.crs.DefaultEngineeringCRS;
+import org.geotools.referencing.operation.transform.AffineTransform2D;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+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
+{
+ public static int srid(GridCoverage2D raster) throws FactoryException
+ {
+ CoordinateReferenceSystem crs = raster.getCoordinateReferenceSystem();
+ if (crs instanceof DefaultEngineeringCRS) {
+ // GeoTools defaults to internal non-standard epsg codes, like
404000, if crs is missing.
+ // We need to check for this case and return 0 instead.
+ if (((DefaultEngineeringCRS) crs).isWildcard()) {
+ return 0;
+ }
+ }
+ return Optional.ofNullable(CRS.lookupEpsgCode(crs, true)).orElse(0);
+ }
+
+ public static int numBands(GridCoverage2D raster) {
+ return raster.getNumSampleDimensions();
+ }
+
+ public static Geometry envelope(GridCoverage2D raster) throws
FactoryException {
+ Envelope2D envelope2D = raster.getEnvelope2D();
+
+ Envelope envelope = new Envelope(envelope2D.getMinX(),
envelope2D.getMaxX(), envelope2D.getMinY(), envelope2D.getMaxY());
+ int srid = srid(raster);
+ return new GeometryFactory(new PrecisionModel(),
srid).toGeometry(envelope);
+ }
+
+ /**
+ * Returns the metadata of a raster as an array of doubles.
+ * @param raster
+ * @return double[] with the following values:
+ * 0: minX: upper left x
+ * 1: maxY: 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
+ * 8: srid
+ * 9: numBands
+ * @throws FactoryException
+ */
+ 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 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");
+ }
+ }
+}
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
new file mode 100644
index 00000000..245cc0a4
--- /dev/null
+++
b/common/src/main/java/org/apache/sedona/common/raster/RasterConstructors.java
@@ -0,0 +1,118 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.CoverageFactoryFinder;
+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.operation.MathTransform;
+
+import javax.media.jai.RasterFactory;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.WritableRaster;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+public class RasterConstructors
+{
+
+ public static GridCoverage2D fromArcInfoAsciiGrid(byte[] bytes) throws
IOException {
+ try (ByteArrayInputStream inputStream = new
ByteArrayInputStream(bytes)) {
+ ArcGridReader reader = new ArcGridReader(inputStream);
+ return reader.read(null);
+ }
+ }
+
+ public static GridCoverage2D fromGeoTiff(byte[] bytes) throws IOException {
+ try (ByteArrayInputStream inputStream = new
ByteArrayInputStream(bytes)) {
+ GeoTiffReader geoTiffReader = new GeoTiffReader(inputStream);
+ return geoTiffReader.read(null);
+ }
+ }
+
+ /**
+ * 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)
+ * @param numBand the number of bands
+ * @param widthInPixel
+ * @param heightInPixel
+ * @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 pixelSize the size of the pixel in the unit of the CRS
+ * @return
+ */
+ 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);
+ }
+
+ /**
+ * Create a new empty raster with the given number of empty bands
+ * @param numBand the number of bands
+ * @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 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
+ * @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)
+ throws FactoryException
+ {
+ CoordinateReferenceSystem crs;
+ if (srid == 0) {
+ crs = DefaultEngineeringCRS.GENERIC_2D;
+ } else {
+ crs = CRS.decode("EPSG:" + srid);
+ }
+ // If scaleY is not defined, use scaleX
+ // MAX_VALUE is used to indicate that the scaleY is not defined
+ double actualScaleY = scaleY;
+ if (scaleY == Integer.MAX_VALUE) {
+ actualScaleY = scaleX;
+ }
+ System.out.println("makeEmptyRaster: " + numBand + ", " + widthInPixel
+ ", " + heightInPixel + ", " + upperLeftX + ", " + upperLeftY + ", " + scaleX
+ ", " + actualScaleY + ", " + skewX + ", " + skewY + ", " + srid);
+ // Create a new empty raster
+ WritableRaster raster =
RasterFactory.createBandedRaster(DataBuffer.TYPE_DOUBLE, widthInPixel,
heightInPixel, numBand, null);
+ MathTransform transform = new AffineTransform2D(scaleX, skewY, skewX,
-actualScaleY, upperLeftX + scaleX / 2, upperLeftY - actualScaleY / 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);
+ }
+}
diff --git
a/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
b/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
new file mode 100644
index 00000000..32cdc3f5
--- /dev/null
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterEditors.java
@@ -0,0 +1,44 @@
+/*
+ * 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.CoverageFactoryFinder;
+import org.geotools.coverage.grid.GridCoverage2D;
+import org.geotools.coverage.grid.GridCoverageFactory;
+import org.geotools.geometry.jts.ReferencedEnvelope;
+import org.geotools.referencing.CRS;
+import org.geotools.referencing.crs.DefaultEngineeringCRS;
+import org.opengis.referencing.FactoryException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+public class RasterEditors
+{
+ public static GridCoverage2D setSrid(GridCoverage2D raster, int srid)
throws FactoryException
+ {
+ CoordinateReferenceSystem crs;
+ if (srid == 0) {
+ crs = DefaultEngineeringCRS.CARTESIAN_2D;
+ } else {
+ crs = CRS.decode("EPSG:" + srid);
+ }
+ ReferencedEnvelope referencedEnvelope = new
ReferencedEnvelope(raster.getEnvelope2D(), crs);
+ GridCoverageFactory gridCoverageFactory =
CoverageFactoryFinder.getGridCoverageFactory(null);
+ return gridCoverageFactory.create(raster.getName().toString(),
raster.getRenderedImage(), referencedEnvelope);
+ }
+}
diff --git a/common/src/main/java/org/apache/sedona/common/raster/Outputs.java
b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
similarity index 99%
rename from common/src/main/java/org/apache/sedona/common/raster/Outputs.java
rename to
common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
index 9d6f5bed..8fbf8217 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/Outputs.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/RasterOutputs.java
@@ -33,7 +33,7 @@ import javax.imageio.ImageWriteParam;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-public class Outputs
+public class RasterOutputs
{
public static byte[] asGeoTiff(GridCoverage2D raster, String
compressionType, float compressionQuality) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
diff --git a/common/src/main/java/org/apache/sedona/common/raster/Serde.java
b/common/src/main/java/org/apache/sedona/common/raster/Serde.java
index 1762a543..2a9e4d7e 100644
--- a/common/src/main/java/org/apache/sedona/common/raster/Serde.java
+++ b/common/src/main/java/org/apache/sedona/common/raster/Serde.java
@@ -17,8 +17,13 @@ import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import javax.media.jai.remote.SerializableRenderedImage;
+
import java.awt.image.RenderedImage;
-import java.io.*;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
public class Serde {
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/ConstructorsTest.java
b/common/src/test/java/org/apache/sedona/common/raster/ConstructorsTest.java
deleted file mode 100644
index f2ec505b..00000000
--- a/common/src/test/java/org/apache/sedona/common/raster/ConstructorsTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/**
- * Licensed 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
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * 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.locationtech.jts.geom.Geometry;
-import org.opengis.referencing.FactoryException;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-import static org.junit.Assert.*;
-
-public class ConstructorsTest extends RasterTestBase {
-
- @Test
- public void fromArcInfoAsciiGrid() throws IOException, FactoryException {
- GridCoverage2D gridCoverage2D =
Constructors.fromArcInfoAsciiGrid(arc.getBytes(StandardCharsets.UTF_8));
-
- Geometry envelope = Functions.envelope(gridCoverage2D);
- assertEquals(3600, envelope.getArea(), 0.1);
- assertEquals(378922d + 30, envelope.getCentroid().getX(), 0.1);
- assertEquals(4072345d + 30, envelope.getCentroid().getY(), 0.1);
- assertEquals(2, gridCoverage2D.getRenderedImage().getTileHeight());
- assertEquals(2, gridCoverage2D.getRenderedImage().getTileWidth());
- assertEquals(0d,
gridCoverage2D.getSampleDimension(0).getNoDataValues()[0], 0.1);
- assertEquals(3d,
gridCoverage2D.getRenderedImage().getData().getPixel(1, 1, (double[])null)[0],
0.1);
- }
-
- @Test
- public void fromGeoTiff() throws IOException, FactoryException {
- GridCoverage2D gridCoverage2D = Constructors.fromGeoTiff(geoTiff);
-
- Geometry envelope = Functions.envelope(gridCoverage2D);
- assertEquals(100, envelope.getArea(), 0.1);
- assertEquals(5, envelope.getCentroid().getX(), 0.1);
- assertEquals(5, envelope.getCentroid().getY(), 0.1);
- assertEquals(10, gridCoverage2D.getRenderedImage().getTileHeight());
- assertEquals(10, gridCoverage2D.getRenderedImage().getTileWidth());
- assertEquals(10d,
gridCoverage2D.getRenderedImage().getData().getPixel(5, 5, (double[])null)[0],
0.1);
- assertEquals(4, gridCoverage2D.getNumSampleDimensions());
- }
-}
\ No newline at end of file
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 90e0d68f..287d6456 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
@@ -26,64 +26,45 @@ import java.util.Arrays;
import java.util.List;
import java.util.Objects;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
public class FunctionsTest extends RasterTestBase {
- @Test
- public void envelope() throws FactoryException {
- Geometry envelope = Functions.envelope(oneBandRaster);
- 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, Functions.envelope(multiBandRaster).getSRID());
- }
-
- @Test
- public void testNumBands() {
- assertEquals(1, Functions.numBands(oneBandRaster));
- assertEquals(4, Functions.numBands(multiBandRaster));
- }
-
@Test
public void testSetSrid() throws FactoryException {
- assertEquals(0, Functions.srid(oneBandRaster));
- assertEquals(4326, Functions.srid(multiBandRaster));
+ assertEquals(0, RasterAccessors.srid(oneBandRaster));
+ assertEquals(4326, RasterAccessors.srid(multiBandRaster));
- GridCoverage2D oneBandRasterWithUpdatedSrid =
Functions.setSrid(oneBandRaster, 4326);
- assertEquals(4326, Functions.srid(oneBandRasterWithUpdatedSrid));
- assertEquals(4326,
Functions.envelope(oneBandRasterWithUpdatedSrid).getSRID());
-
assertTrue(Functions.envelope(oneBandRasterWithUpdatedSrid).equalsTopo(Functions.envelope(oneBandRaster)));
+ GridCoverage2D oneBandRasterWithUpdatedSrid =
RasterEditors.setSrid(oneBandRaster, 4326);
+ assertEquals(4326, RasterAccessors.srid(oneBandRasterWithUpdatedSrid));
+ assertEquals(4326,
RasterAccessors.envelope(oneBandRasterWithUpdatedSrid).getSRID());
+
assertTrue(RasterAccessors.envelope(oneBandRasterWithUpdatedSrid).equalsTopo(RasterAccessors.envelope(oneBandRaster)));
- GridCoverage2D multiBandRasterWithUpdatedSrid =
Functions.setSrid(multiBandRaster, 0);
- assertEquals(0 , Functions.srid(multiBandRasterWithUpdatedSrid));
- }
-
- @Test
- public void testSrid() throws FactoryException {
- assertEquals(0, Functions.srid(oneBandRaster));
- assertEquals(4326, Functions.srid(multiBandRaster));
+ GridCoverage2D multiBandRasterWithUpdatedSrid =
RasterEditors.setSrid(multiBandRaster, 0);
+ assertEquals(0 , RasterAccessors.srid(multiBandRasterWithUpdatedSrid));
}
@Test
public void value() throws TransformException {
- assertNull("Points outside of the envelope should return null.",
Functions.value(oneBandRaster, point(1, 1), 1));
- assertNull("Invalid band should return null.",
Functions.value(oneBandRaster, point(378923, 4072346), 0));
- assertNull("Invalid band should return null.",
Functions.value(oneBandRaster, point(378923, 4072346), 2));
+ assertNull("Points outside of the envelope should return null.",
PixelFunctions.value(oneBandRaster, point(1, 1), 1));
+ assertNull("Invalid band should return null.",
PixelFunctions.value(oneBandRaster, point(378923, 4072346), 0));
+ assertNull("Invalid band should return null.",
PixelFunctions.value(oneBandRaster, point(378923, 4072346), 2));
- Double value = Functions.value(oneBandRaster, point(378923, 4072346),
1);
+ Double value = PixelFunctions.value(oneBandRaster, point(378923,
4072346), 1);
assertNotNull(value);
assertEquals(2.0d, value, 0.1d);
- assertNull("Null should be returned for no data values.",
Functions.value(oneBandRaster, point(378923, 4072376), 1));
+ assertNull("Null should be returned for no data values.",
PixelFunctions.value(oneBandRaster, point(378923, 4072376), 1));
}
@Test
public void valueWithMultibandRaster() throws TransformException {
// Multiband raster
- assertEquals(9d, Functions.value(multiBandRaster, point(4.5d,4.5d),
3), 0.1d);
- assertEquals(255d, Functions.value(multiBandRaster, point(4.5d,4.5d),
4), 0.1d);
+ assertEquals(9d, PixelFunctions.value(multiBandRaster,
point(4.5d,4.5d), 3), 0.1d);
+ assertEquals(255d, PixelFunctions.value(multiBandRaster,
point(4.5d,4.5d), 4), 0.1d);
}
@Test
@@ -91,19 +72,19 @@ public class FunctionsTest extends RasterTestBase {
// The function 'value' is implemented using 'values'.
// These test only cover bits not already covered by tests for 'value'
List<Geometry> points = Arrays.asList(new Geometry[]{point(378923,
4072346), point(378924, 4072346)});
- List<Double> values = Functions.values(oneBandRaster, points, 1);
+ List<Double> values = PixelFunctions.values(oneBandRaster, points, 1);
assertEquals(2, values.size());
assertTrue(values.stream().allMatch(Objects::nonNull));
- values = Functions.values(oneBandRaster, points, 0);
+ values = PixelFunctions.values(oneBandRaster, points, 0);
assertEquals(2, values.size());
assertTrue("All values should be null for invalid band index.",
values.stream().allMatch(Objects::isNull));
- values = Functions.values(oneBandRaster, points, 2);
+ values = PixelFunctions.values(oneBandRaster, points, 2);
assertEquals(2, values.size());
assertTrue("All values should be null for invalid band index.",
values.stream().allMatch(Objects::isNull));
- values = Functions.values(oneBandRaster, Arrays.asList(new
Geometry[]{point(378923, 4072346), null}), 1);
+ values = PixelFunctions.values(oneBandRaster, Arrays.asList(new
Geometry[]{point(378923, 4072346), null}), 1);
assertEquals(2, values.size());
assertNull("Null geometries should return null values.",
values.get(1));
}
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/MapAlgebraTest.java
b/common/src/test/java/org/apache/sedona/common/raster/MapAlgebraTest.java
new file mode 100644
index 00000000..7850d74a
--- /dev/null
+++ b/common/src/test/java/org/apache/sedona/common/raster/MapAlgebraTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class MapAlgebraTest extends RasterTestBase
+{
+ @Test
+ public void testAddBandAsArrayAppend()
+ throws FactoryException
+ {
+ GridCoverage2D raster = createEmptyRaster(1);
+ double[] band1 = new double[raster.getRenderedImage().getWidth() *
raster.getRenderedImage().getHeight()];
+ for (int i = 0; i < band1.length; i++) {
+ band1[i] = i;
+ }
+ double[] band2 = new double[raster.getRenderedImage().getWidth() *
raster.getRenderedImage().getHeight()];
+ for (int i = 0; i < band2.length; i++) {
+ band2[i] = i * 2;
+ }
+ // Replace the first band
+ GridCoverage2D rasterWithBand1 = MapAlgebra.addBandFromArray(raster,
band1, 1);
+ assertEquals(1, RasterAccessors.numBands(rasterWithBand1));
+ assertEquals(raster.getEnvelope(), rasterWithBand1.getEnvelope());
+ assertEquals(raster.getCoordinateReferenceSystem2D(),
rasterWithBand1.getCoordinateReferenceSystem2D());
+ assertEquals(RasterAccessors.srid(raster),
RasterAccessors.srid(rasterWithBand1));
+
+ // Append a new band
+ GridCoverage2D rasterWithBand2 =
MapAlgebra.addBandFromArray(rasterWithBand1, band2);
+ assertEquals(2, RasterAccessors.numBands(rasterWithBand2));
+ assertEquals(raster.getEnvelope(), rasterWithBand2.getEnvelope());
+ assertEquals(raster.getCoordinateReferenceSystem2D(),
rasterWithBand2.getCoordinateReferenceSystem2D());
+ assertEquals(RasterAccessors.srid(raster),
RasterAccessors.srid(rasterWithBand2));
+
+ // Check the value of the first band when use the raster with only one
band
+ double[] firstBand = MapAlgebra.bandAsArray(rasterWithBand1, 1);
+ for (int i = 0; i < firstBand.length; i++) {
+ assertEquals(i, firstBand[i], 0.1);
+ }
+ // Check the value of the first band when use the raster with two bands
+ firstBand = MapAlgebra.bandAsArray(rasterWithBand2, 1);
+ for (int i = 0; i < firstBand.length; i++) {
+ assertEquals(i, firstBand[i], 0.1);
+ }
+ // Check the value of the second band
+ double[] secondBand = MapAlgebra.bandAsArray(rasterWithBand2, 2);
+ for (int i = 0; i < secondBand.length; i++) {
+ assertEquals(i * 2, secondBand[i], 0.1);
+ }
+ }
+
+ @Test
+ public void testAddBandAsArrayReplace()
+ throws FactoryException
+ {
+ GridCoverage2D raster = createEmptyRaster(2);
+ double[] band1 = new double[raster.getRenderedImage().getWidth() *
raster.getRenderedImage().getHeight()];
+ for (int i = 0; i < band1.length; i++) {
+ band1[i] = i;
+ }
+ double[] band2 = new double[raster.getRenderedImage().getWidth() *
raster.getRenderedImage().getHeight()];
+ for (int i = 0; i < band2.length; i++) {
+ band2[i] = i * 2;
+ }
+ // Replace the first band
+ GridCoverage2D rasterWithBand1 = MapAlgebra.addBandFromArray(raster,
band1, 1);
+ assertEquals(2, RasterAccessors.numBands(rasterWithBand1));
+ assertEquals(raster.getEnvelope(), rasterWithBand1.getEnvelope());
+ assertEquals(raster.getCoordinateReferenceSystem2D(),
rasterWithBand1.getCoordinateReferenceSystem2D());
+ assertEquals(RasterAccessors.srid(raster),
RasterAccessors.srid(rasterWithBand1));
+
+ // Replace the second band
+ GridCoverage2D rasterWithBand2 =
MapAlgebra.addBandFromArray(rasterWithBand1, band2, 2);
+ assertEquals(2, RasterAccessors.numBands(rasterWithBand2));
+ assertEquals(raster.getEnvelope(), rasterWithBand2.getEnvelope());
+ assertEquals(raster.getCoordinateReferenceSystem2D(),
rasterWithBand2.getCoordinateReferenceSystem2D());
+ assertEquals(RasterAccessors.srid(raster),
RasterAccessors.srid(rasterWithBand2));
+
+ // Check the value of the first band when use the raster with only one
band
+ double[] firstBand = MapAlgebra.bandAsArray(rasterWithBand1, 1);
+ for (int i = 0; i < firstBand.length; i++) {
+ assertEquals(i, firstBand[i], 0.1);
+ }
+ // Check the value of the first band when use the raster with two bands
+ firstBand = MapAlgebra.bandAsArray(rasterWithBand2, 1);
+ for (int i = 0; i < firstBand.length; i++) {
+ assertEquals(i, firstBand[i], 0.1);
+ }
+ // Check the value of the second band
+ double[] secondBand = MapAlgebra.bandAsArray(rasterWithBand2, 2);
+ for (int i = 0; i < secondBand.length; i++) {
+ assertEquals(i * 2, secondBand[i], 0.1);
+ }
+ }
+
+ @Test
+ public void testBandAsArray()
+ throws FactoryException
+ {
+ int widthInPixel = 10;
+ int heightInPixel = 10;
+ double upperLeftX = 0;
+ double upperLeftY = 0;
+ double cellSize = 1;
+ int numbBands = 1;
+ GridCoverage2D raster = RasterConstructors.makeEmptyRaster(numbBands,
widthInPixel, heightInPixel, upperLeftX, upperLeftY, cellSize);
+ // Out of bound index should return null
+ double[] band = MapAlgebra.bandAsArray(raster, 0);
+ assertNull(band);
+ band = MapAlgebra.bandAsArray(raster, 1);
+ assertEquals(widthInPixel * heightInPixel, band.length);
+ for (int i = 0; i < band.length; i++) {
+ // The default value is 0.0
+ assertEquals(0.0, band[i], 0.1);
+ }
+ // Now set the value of the first band and check again
+ for (int i = 0; i < band.length; i++) {
+ band[i] = i;
+ }
+ double[] bandNew =
MapAlgebra.bandAsArray(MapAlgebra.addBandFromArray(raster, band, 1), 1);
+ assertEquals(band.length, bandNew.length);
+ for (int i = 0; i < band.length; i++) {
+ assertEquals(band[i], bandNew[i], 0.1);
+ }
+ }
+}
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
new file mode 100644
index 00000000..bca7d8c2
--- /dev/null
+++
b/common/src/test/java/org/apache/sedona/common/raster/RasterAccessorsTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.locationtech.jts.geom.Geometry;
+import org.opengis.referencing.FactoryException;
+
+import static org.junit.Assert.assertEquals;
+
+public class RasterAccessorsTest extends RasterTestBase
+{
+ @Test
+ public void envelope() throws FactoryException
+ {
+ Geometry envelope = RasterAccessors.envelope(oneBandRaster);
+ 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 testNumBands() {
+ assertEquals(1, RasterAccessors.numBands(oneBandRaster));
+ assertEquals(4, RasterAccessors.numBands(multiBandRaster));
+ }
+
+ @Test
+ public void testSrid() throws FactoryException {
+ assertEquals(0, RasterAccessors.srid(oneBandRaster));
+ assertEquals(4326, RasterAccessors.srid(multiBandRaster));
+ }
+
+ @Test
+ public void testMetaData()
+ throws FactoryException
+ {
+ double upperLeftX = 1;
+ double upperLeftY = 2;
+ int widthInPixel = 3;
+ int heightInPixel = 4;
+ double pixelSize = 5;
+ int numBands = 1;
+
+ 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(10, metadata.length);
+
+ upperLeftX = 5;
+ upperLeftY = 6;
+ widthInPixel = 7;
+ heightInPixel = 8;
+ pixelSize = 9;
+ numBands = 10;
+
+ gridCoverage2D = RasterConstructors.makeEmptyRaster(numBands,
widthInPixel, heightInPixel, upperLeftX, upperLeftY, pixelSize);
+
+ 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(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
new file mode 100644
index 00000000..6f397e7d
--- /dev/null
+++
b/common/src/test/java/org/apache/sedona/common/raster/RasterConstructorsTest.java
@@ -0,0 +1,91 @@
+/**
+ * Licensed 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.locationtech.jts.geom.Geometry;
+import org.opengis.referencing.FactoryException;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.Assert.assertEquals;
+
+public class RasterConstructorsTest
+ extends RasterTestBase {
+
+ @Test
+ public void fromArcInfoAsciiGrid() throws IOException, FactoryException {
+ GridCoverage2D gridCoverage2D =
RasterConstructors.fromArcInfoAsciiGrid(arc.getBytes(StandardCharsets.UTF_8));
+
+ Geometry envelope = RasterAccessors.envelope(gridCoverage2D);
+ assertEquals(3600, envelope.getArea(), 0.1);
+ assertEquals(378922d + 30, envelope.getCentroid().getX(), 0.1);
+ assertEquals(4072345d + 30, envelope.getCentroid().getY(), 0.1);
+ assertEquals(2, gridCoverage2D.getRenderedImage().getTileHeight());
+ assertEquals(2, gridCoverage2D.getRenderedImage().getTileWidth());
+ assertEquals(0d,
gridCoverage2D.getSampleDimension(0).getNoDataValues()[0], 0.1);
+ assertEquals(3d,
gridCoverage2D.getRenderedImage().getData().getPixel(1, 1, (double[])null)[0],
0.1);
+ }
+
+ @Test
+ public void fromGeoTiff() throws IOException, FactoryException {
+ GridCoverage2D gridCoverage2D =
RasterConstructors.fromGeoTiff(geoTiff);
+
+ Geometry envelope = RasterAccessors.envelope(gridCoverage2D);
+ assertEquals(100, envelope.getArea(), 0.1);
+ assertEquals(5, envelope.getCentroid().getX(), 0.1);
+ assertEquals(5, envelope.getCentroid().getY(), 0.1);
+ assertEquals(10, gridCoverage2D.getRenderedImage().getTileHeight());
+ assertEquals(10, gridCoverage2D.getRenderedImage().getTileWidth());
+ assertEquals(10d,
gridCoverage2D.getRenderedImage().getData().getPixel(5, 5, (double[])null)[0],
0.1);
+ assertEquals(4, gridCoverage2D.getNumSampleDimensions());
+ }
+
+ @Test
+ public void makeEmptyRaster() throws FactoryException {
+ double upperLeftX = 0;
+ double upperLeftY = 0;
+ int widthInPixel = 1;
+ int heightInPixel = 2;
+ double pixelSize = 2;
+ int numBands = 1;
+
+ GridCoverage2D gridCoverage2D =
RasterConstructors.makeEmptyRaster(numBands, widthInPixel, heightInPixel,
upperLeftX, upperLeftY, pixelSize);
+ Geometry envelope = RasterAccessors.envelope(gridCoverage2D);
+ assertEquals(upperLeftX, envelope.getEnvelopeInternal().getMinX(),
0.001);
+ assertEquals(upperLeftX + widthInPixel * pixelSize,
envelope.getEnvelopeInternal().getMaxX(), 0.001);
+ assertEquals(upperLeftY - heightInPixel * pixelSize,
envelope.getEnvelopeInternal().getMinY(), 0.001);
+ assertEquals(upperLeftY, envelope.getEnvelopeInternal().getMaxY(),
0.001);
+
+ assertEquals("POLYGON ((0 -4, 0 0, 2 0, 2 -4, 0 -4))",
envelope.toString());
+ double expectedWidthInDegree = pixelSize * widthInPixel;
+ double expectedHeightInDegree = pixelSize * heightInPixel;
+
+ assertEquals(expectedWidthInDegree * expectedHeightInDegree,
envelope.getArea(), 0.001);
+ assertEquals(heightInPixel,
gridCoverage2D.getRenderedImage().getTileHeight());
+ assertEquals(widthInPixel,
gridCoverage2D.getRenderedImage().getTileWidth());
+ 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);
+ 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/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java
b/common/src/test/java/org/apache/sedona/common/raster/RasterTestBase.java
index ba19e1f1..bccb4c7c 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
@@ -20,8 +20,9 @@ import org.geotools.geometry.Envelope2D;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.junit.Before;
import org.opengis.parameter.GeneralParameterValue;
+import org.opengis.referencing.FactoryException;
-import java.awt.*;
+import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -35,13 +36,23 @@ public class RasterTestBase {
@Before
public void setup() throws IOException {
- oneBandRaster =
Constructors.fromArcInfoAsciiGrid(arc.getBytes(StandardCharsets.UTF_8));
+ oneBandRaster =
RasterConstructors.fromArcInfoAsciiGrid(arc.getBytes(StandardCharsets.UTF_8));
multiBandRaster = createMultibandRaster();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
new GeoTiffWriter(bos).write(multiBandRaster, new
GeneralParameterValue[]{});
geoTiff = bos.toByteArray();
}
+ GridCoverage2D createEmptyRaster(int numBands)
+ throws FactoryException
+ {
+ int widthInPixel = 4;
+ int heightInPixel = 5;
+ double upperLeftX = 101;
+ double upperLeftY = 102;
+ double cellSize = 2;
+ return RasterConstructors.makeEmptyRaster(numBands, widthInPixel,
heightInPixel, upperLeftX, upperLeftY, cellSize);
+ }
GridCoverage2D createMultibandRaster() throws IOException {
GridCoverageFactory factory = new GridCoverageFactory();
BufferedImage image = new BufferedImage(10, 10,
BufferedImage.TYPE_INT_ARGB);
diff --git
a/common/src/test/java/org/apache/sedona/common/raster/SerdeTest.java
b/common/src/test/java/org/apache/sedona/common/raster/SerdeTest.java
index 4500e11e..1a13534b 100644
--- a/common/src/test/java/org/apache/sedona/common/raster/SerdeTest.java
+++ b/common/src/test/java/org/apache/sedona/common/raster/SerdeTest.java
@@ -18,7 +18,8 @@ import org.junit.Test;
import java.io.IOException;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
public class SerdeTest extends RasterTestBase {
diff --git a/docs/api/sql/Raster-loader.md b/docs/api/sql/Raster-loader.md
index 4ed58b52..d3b33ecb 100644
--- a/docs/api/sql/Raster-loader.md
+++ b/docs/api/sql/Raster-loader.md
@@ -47,8 +47,71 @@ var df =
spark.read.format("binaryFile").load("/some/path/*.tiff")
df = df.withColumn("raster", f.expr("RS_FromGeoTiff(content)"))
```
+### RS_MakeEmptyRaster
+
+Introduction: Returns an empty raster geometry. Every band in the raster is
initialized to `0.0`.
+
+Since: `v1.4.1`
+
+Format: `RS_MakeEmptyRaster(numBands:Int, width: Int, height: Int, upperleftX:
Double, upperleftY: Double, cellSize:Double)`
+
+* NumBands: The number of bands in the raster. If not specified, the raster
will have a single band.
+* Width: The width of the raster in pixels.
+* Height: The height of the raster in pixels.
+* UpperleftX: The X coordinate of the upper left corner of the raster, in
terms of the CRS units.
+* UpperleftY: The Y coordinate of the upper left corner of the raster, in
terms of the CRS units.
+* Cell Size (pixel size): The size of the cells in the raster, in terms of the
CRS units.
+
+It uses the default Cartesian coordinate system.
+
+Format: `RS_MakeEmptyRaster(numBands:Int, width: Int, height: Int, upperleftX:
Double, upperleftY: Double, scaleX:Double, scaleY:Double, skewX:Double,
skewY:Double, srid: Int)`
+
+* NumBands: The number of bands in the raster. If not specified, the raster
will have a single band.
+* Width: The width of the raster in pixels.
+* Height: The height of the raster in pixels.
+* UpperleftX: The X coordinate of the upper left corner of the raster, in
terms of the CRS units.
+* UpperleftY: The Y coordinate of the upper left corner of the raster, in
terms of the CRS units.
+* ScaleX (pixel size on X): The size of the cells on the X axis, in terms of
the CRS units.
+* ScaleY (pixel size on Y): The size of the cells on the Y axis, in terms of
the CRS units.
+* SkewX: The skew of the raster on the X axis, in terms of the CRS units.
+* SkewY: The skew of the raster on the Y axis, in terms of the CRS units.
+* SRID: The SRID of the raster. Use 0 if you want to use the default Cartesian
coordinate system. Use 4326 if you want to use WGS84.
+
+SQL example 1 (with 2 bands):
+
+```sql
+SELECT RS_MakeEmptyRaster(2, 10, 10, 0.0, 0.0, 1.0) as raster
+```
+
+Output:
+```
++--------------------------------------------+
+|rs_makeemptyraster(2, 10, 10, 0.0, 0.0, 1.0)|
++--------------------------------------------+
+| GridCoverage2D["g...|
++--------------------------------------------+
+```
+
+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
+```
+
+Output:
+```
++--------------------------------------------------------------+
+|rs_makeemptyraster(2, 10, 10, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0)|
++--------------------------------------------------------------+
+| GridCoverage2D["g...|
++--------------------------------------------------------------+
+```
+
## Load GeoTiff to Array[Double] format
+!!!warning
+ This function has been deprecated since v1.4.1. Please use
`RS_FromGeoTiff` instead and `binaryFile` data source to read GeoTiff files.
+
The `geotiff` loader of Sedona is a Spark built-in data source. It can read a
single geotiff image or a number of geotiff images into a DataFrame. Each
geotiff is a row in the resulting DataFrame and stored in an array of Double
type format.
Since: `v1.1.0`
diff --git a/docs/api/sql/Raster-operators.md b/docs/api/sql/Raster-operators.md
index 0b49daf0..fcaddaa3 100644
--- a/docs/api/sql/Raster-operators.md
+++ b/docs/api/sql/Raster-operators.md
@@ -1,4 +1,4 @@
-## RasterUDT based operators
+## Raster based operators
### RS_Envelope
@@ -17,6 +17,39 @@ Output:
POLYGON((0 0,20 0,20 60,0 60,0 0))
```
+### RS_MetaData
+
+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)
+- 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)
+- 6: skew in x direction (rotation x)
+- 7: skew in y direction (rotation y)
+- 8: srid of the raster
+- 9: number of bands
+
+Format: `RS_MetaData (raster: Raster)`
+
+Since: `v1.4.1`
+
+SQL example:
+```sql
+SELECT RS_MetaData(raster) FROM raster_table
+```
+
+Output:
+```
++-----------------------------------------------------------------------------------------------------------------------+
+|rs_metadata(raster)
|
++-----------------------------------------------------------------------------------------------------------------------+
+|[-1.3095817809482181E7, 4021262.7487925636, 512.0, 517.0, 72.32861272132695,
-72.32861272132695, 0.0, 0.0, 3857.0, 1.0]|
++-----------------------------------------------------------------------------------------------------------------------+
+```
+
### RS_NumBands
Introduction: Returns the number of the bands in the raster.
@@ -35,7 +68,7 @@ Output:
4
```
-## RS_SetSRID
+### RS_SetSRID
Introduction: Sets the spatial reference system identifier (SRID) of the
raster geometry.
@@ -143,7 +176,67 @@ Output:
```
-## Array[Double] based operators
+## Raster to Map Algebra operators
+
+To bridge the gap between the raster and map algebra worlds, the following
operators are provided. These operators convert a raster to a map algebra
object. The map algebra object can then be used with the map algebra operators
described in the next section.
+
+### RS_BandAsArray
+
+Introduction: Extract a band from a raster as an array of doubles.
+
+Format: `RS_BandAsArray (raster: Raster, bandIndex: Int)`.
+
+Since: `v1.4.1`
+
+BandIndex is 1-based and must be between 1 and RS_NumBands(raster). It returns
null if the bandIndex is out of range or the raster is null.
+
+SQL example:
+```sql
+SELECT RS_BandAsArray(raster, 1) FROM raster_table
+```
+
+Output:
+
+```
++--------------------+
+| band|
++--------------------+
+|[0.0, 0.0, 0.0, 0...|
++--------------------+
+```
+
+### RS_AddBandFromArray
+
+Introduction: Add a band to a raster from an array of doubles.
+
+Format: `RS_AddBandFromArray (raster: Raster, band: Array[Double],
bandIndex:Int)`.
+
+Since: `v1.4.1`
+
+The bandIndex is 1-based and must be between 1 and RS_NumBands(raster) + 1. It
throws an exception if the bandIndex is out of range or the raster is null.
+
+When the bandIndex is RS_NumBands(raster) + 1, it appends the band to the end
of the raster. Otherwise, it replaces the existing band at the bandIndex.
+
+Note that: `bandIndex == RS_NumBands(raster) + 1` is an experimental feature
and might not lead to the loss of raster metadata and properties such as color
models.
+
+SQL example:
+
+```sql
+SELECT RS_AddBandFromArray(raster,
RS_MultiplyFactor(RS_BandAsArray(RS_FromGeoTiff(content), 1), 2), 1) AS raster
FROM raster_table
+```
+
+Output:
+```
++--------------------+
+| raster|
++--------------------+
+|GridCoverage2D["g...|
++--------------------+
+```
+
+## Map Algebra operators
+
+Map algebra operators work on a single band of a raster. Each band is
represented as an array of doubles. The operators return an array of doubles.
### RS_Add
@@ -168,6 +261,8 @@ Format: `RS_Append(data: Array[Double], newBand:
Array[Double], nBands: Int)`
Since: `v1.2.1`
+Deprecated since: `v1.4.1`
+
Spark SQL example:
```scala
diff --git a/docs/api/sql/Raster-writer.md b/docs/api/sql/Raster-writer.md
index eded6509..0ddcee04 100644
--- a/docs/api/sql/Raster-writer.md
+++ b/docs/api/sql/Raster-writer.md
@@ -167,6 +167,9 @@ The newly created DataFrame can be written to disk again
but must be under a dif
## Write Array[Double] to GeoTiff files
+!!!warning
+ This function has been deprecated since v1.4.1. Please use
`RS_AsGeoTiff` instead and `raster` data source to write GeoTiff files.
+
Introduction: You can write a GeoTiff dataframe as GeoTiff images using the
spark `write` feature with the format `geotiff`. The geotiff raster column
needs to be an array of double type data.
Since: `v1.2.1`
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 dc01ea38..1b0e1dbc 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
@@ -177,10 +177,14 @@ object Catalog {
function[RS_Array](),
function[RS_Normalize](),
function[RS_Append](),
+ function[RS_AddBandFromArray](),
+ function[RS_BandAsArray](),
function[RS_FromArcInfoAsciiGrid](),
function[RS_FromGeoTiff](),
+ function[RS_MakeEmptyRaster](java.lang.Integer.MAX_VALUE, 0.0, 0.0, 0),
function[RS_Envelope](),
function[RS_NumBands](),
+ function[RS_Metadata](),
function[RS_SetSRID](),
function[RS_SRID](),
function[RS_Value](1),
diff --git
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Functions.scala
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/MapAlgebra.scala
similarity index 86%
rename from
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Functions.scala
rename to
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/MapAlgebra.scala
index d904bf9d..e08e9bff 100644
---
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Functions.scala
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/MapAlgebra.scala
@@ -16,24 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-
package org.apache.spark.sql.sedona_sql.expressions.raster
-import org.apache.sedona.common.geometrySerde.GeometrySerializer
-import org.apache.sedona.common.raster.{Functions, Serde}
+import org.apache.sedona.common.raster.{MapAlgebra, Serde}
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression}
import org.apache.spark.sql.catalyst.util.{ArrayData, GenericArrayData}
-import org.apache.spark.sql.sedona_sql.UDT.{GeometryUDT, RasterUDT}
+import org.apache.spark.sql.sedona_sql.UDT.RasterUDT
+import
org.apache.spark.sql.sedona_sql.expressions.raster.implicits.RasterInputExpressionEnhancer
import org.apache.spark.sql.sedona_sql.expressions.{SerdeAware,
UserDataGeneratator}
-import org.apache.spark.sql.sedona_sql.expressions.implicits._
-import org.apache.spark.sql.sedona_sql.expressions.raster.implicits._
import org.apache.spark.sql.types._
import org.geotools.coverage.grid.GridCoverage2D
-
-
/// Calculate Normalized Difference between two bands
case class RS_NormalizedDifference(inputExpressions: Seq[Expression])
extends Expression with CodegenFallback with UserDataGeneratator {
@@ -328,7 +323,7 @@ case class RS_Count(inputExpressions: Seq[Expression])
override def nullable: Boolean = false
- override def eval(inputRow: InternalRow): Any = {
+ override def eval(inputRow: InternalRow): Any = {
val band =
inputExpressions(0).eval(inputRow).asInstanceOf[ArrayData].toDoubleArray()
val target =
inputExpressions(1).eval(inputRow).asInstanceOf[Decimal].toDouble
findCount(band, target)
@@ -810,42 +805,16 @@ case class RS_Append(inputExpressions: Seq[Expression])
}
}
-case class RS_Envelope(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
- override def nullable: Boolean = true
-
- override def eval(input: InternalRow): Any = {
- val raster = inputExpressions(0).toRaster(input)
- if (raster == null) {
- null
- } else {
- Functions.envelope(raster).toGenericArrayData
- }
- }
-
- override def dataType: DataType = GeometryUDT
-
- override def children: Seq[Expression] = inputExpressions
-
- protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
- copy(inputExpressions = newChildren)
- }
-
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
-}
+case class RS_AddBandFromArray(inputExpressions: Seq[Expression]) extends
Expression with CodegenFallback
+ with ExpectsInputTypes with SerdeAware {
-case class RS_NumBands(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
override def nullable: Boolean = true
override def eval(input: InternalRow): Any = {
- val raster = inputExpressions(0).toRaster(input)
- if (raster == null) {
- null
- } else {
- Functions.numBands(raster)
- }
+ Option(evalWithoutSerialization(input)).map(Serde.serialize).orNull
}
- override def dataType: DataType = IntegerType
+ override def dataType: DataType = RasterUDT
override def children: Seq[Expression] = inputExpressions
@@ -853,111 +822,44 @@ case class RS_NumBands(inputExpressions:
Seq[Expression]) extends Expression wit
copy(inputExpressions = newChildren)
}
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
-}
-
-case class RS_SetSRID(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes with SerdeAware {
- override def nullable: Boolean = true
-
- override def eval(input: InternalRow): Any = {
- Option(evalWithoutSerialization(input)).map(Serde.serialize).orNull
- }
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, ArrayType,
IntegerType)
override def evalWithoutSerialization(input: InternalRow): GridCoverage2D = {
val raster = inputExpressions(0).toRaster(input)
- val srid = inputExpressions(1).eval(input).asInstanceOf[Int]
if (raster == null) {
- null
- } else {
- Functions.setSrid(raster, srid)
+ return null
}
+ val band =
inputExpressions(1).eval(input).asInstanceOf[ArrayData].toDoubleArray()
+ val bandIndex = inputExpressions(2).eval(input).asInstanceOf[Int]
+ MapAlgebra.addBandFromArray(raster, band, bandIndex)
}
-
- override def dataType: DataType = RasterUDT
-
- override def children: Seq[Expression] = inputExpressions
-
- protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
- copy(inputExpressions = newChildren)
- }
-
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, IntegerType)
}
-case class RS_SRID(inputExpressions: Seq[Expression]) extends Expression with
CodegenFallback with ExpectsInputTypes {
+case class RS_BandAsArray(inputExpressions: Seq[Expression]) extends
Expression with CodegenFallback
+ with ExpectsInputTypes {
+
override def nullable: Boolean = true
override def eval(input: InternalRow): Any = {
val raster = inputExpressions(0).toRaster(input)
if (raster == null) {
- null
- } else {
- Functions.srid(raster)
+ return null
}
- }
-
- override def dataType: DataType = IntegerType
-
- override def children: Seq[Expression] = inputExpressions
-
- protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
- copy(inputExpressions = newChildren)
- }
-
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
-}
-
-case class RS_Value(inputExpressions: Seq[Expression]) extends Expression with
CodegenFallback with ExpectsInputTypes {
-
- override def nullable: Boolean = true
-
- override def dataType: DataType = DoubleType
-
- override def eval(input: InternalRow): Any = {
- val raster = inputExpressions.head.toRaster(input)
- val geom = inputExpressions(1).toGeometry(input)
- val band = inputExpressions(2).eval(input).asInstanceOf[Int]
- if (raster == null || geom == null) {
- null
- } else {
- Functions.value(raster, geom, band)
+ val bandIndex = inputExpressions(1).eval(input).asInstanceOf[Int]
+ val band = MapAlgebra.bandAsArray(raster, bandIndex)
+ if (band == null) {
+ return null
}
+ new GenericArrayData(band)
}
- override def children: Seq[Expression] = inputExpressions
-
- protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
- copy(inputExpressions = newChildren)
- }
-
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, GeometryUDT,
IntegerType)
-}
-
-case class RS_Values(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
-
- override def nullable: Boolean = true
-
override def dataType: DataType = ArrayType(DoubleType)
- override def eval(input: InternalRow): Any = {
- val raster = inputExpressions(0).toRaster(input)
- val serializedGeometries =
inputExpressions(1).eval(input).asInstanceOf[ArrayData]
- val band = inputExpressions(2).eval(input).asInstanceOf[Int]
- if (raster == null || serializedGeometries == null) {
- null
- } else {
- val geometries = (0 until serializedGeometries.numElements()).map {
- i =>
Option(serializedGeometries.getBinary(i)).map(GeometrySerializer.deserialize).orNull
- }
- new GenericArrayData(Functions.values(raster,
java.util.Arrays.asList(geometries:_*), band).toArray)
- }
- }
-
override def children: Seq[Expression] = inputExpressions
protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
copy(inputExpressions = newChildren)
}
- override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT,
ArrayType(GeometryUDT), IntegerType)
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, IntegerType)
}
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
new file mode 100644
index 00000000..d6001651
--- /dev/null
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/PixelFunctions.scala
@@ -0,0 +1,85 @@
+/*
+ * 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.geometrySerde.GeometrySerializer
+import org.apache.sedona.common.raster.PixelFunctions
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression}
+import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
+import org.apache.spark.sql.catalyst.util.{ArrayData, GenericArrayData}
+import org.apache.spark.sql.sedona_sql.UDT.{GeometryUDT, RasterUDT}
+import
org.apache.spark.sql.sedona_sql.expressions.implicits.InputExpressionEnhancer
+import
org.apache.spark.sql.sedona_sql.expressions.raster.implicits.RasterInputExpressionEnhancer
+import org.apache.spark.sql.types.{AbstractDataType, ArrayType, DataType,
DoubleType, IntegerType}
+
+case class RS_Value(inputExpressions: Seq[Expression]) extends Expression with
CodegenFallback with ExpectsInputTypes {
+
+ override def nullable: Boolean = true
+
+ override def dataType: DataType = DoubleType
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions.head.toRaster(input)
+ val geom = inputExpressions(1).toGeometry(input)
+ val band = inputExpressions(2).eval(input).asInstanceOf[Int]
+ if (raster == null || geom == null) {
+ null
+ } else {
+ PixelFunctions.value(raster, geom, band)
+ }
+ }
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, GeometryUDT,
IntegerType)
+}
+
+case class RS_Values(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
+
+ override def nullable: Boolean = true
+
+ override def dataType: DataType = ArrayType(DoubleType)
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions(0).toRaster(input)
+ val serializedGeometries =
inputExpressions(1).eval(input).asInstanceOf[ArrayData]
+ val band = inputExpressions(2).eval(input).asInstanceOf[Int]
+ if (raster == null || serializedGeometries == null) {
+ null
+ } else {
+ val geometries = (0 until serializedGeometries.numElements()).map {
+ i =>
Option(serializedGeometries.getBinary(i)).map(GeometrySerializer.deserialize).orNull
+ }
+ new GenericArrayData(PixelFunctions.values(raster,
java.util.Arrays.asList(geometries:_*), band).toArray)
+ }
+ }
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT,
ArrayType(GeometryUDT), IntegerType)
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..1f19979e
--- /dev/null
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterAccessors.scala
@@ -0,0 +1,121 @@
+/*
+ * 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.RasterAccessors
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression}
+import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
+import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.sedona_sql.UDT.{GeometryUDT, RasterUDT}
+import org.apache.spark.sql.sedona_sql.expressions.implicits.GeometryEnhancer
+import
org.apache.spark.sql.sedona_sql.expressions.raster.implicits.RasterInputExpressionEnhancer
+import org.apache.spark.sql.types.{AbstractDataType, ArrayType, DataType,
DoubleType, IntegerType}
+
+case class RS_Envelope(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions(0).toRaster(input)
+ if (raster == null) {
+ null
+ } else {
+ RasterAccessors.envelope(raster).toGenericArrayData
+ }
+ }
+
+ override def dataType: DataType = GeometryUDT
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
+}
+
+case class RS_NumBands(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions(0).toRaster(input)
+ if (raster == null) {
+ null
+ } else {
+ RasterAccessors.numBands(raster)
+ }
+ }
+
+ override def dataType: DataType = IntegerType
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
+}
+
+case class RS_SRID(inputExpressions: Seq[Expression]) extends Expression with
CodegenFallback with ExpectsInputTypes {
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions(0).toRaster(input)
+ if (raster == null) {
+ null
+ } else {
+ RasterAccessors.srid(raster)
+ }
+ }
+
+ override def dataType: DataType = IntegerType
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
+}
+
+case class RS_Metadata(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes {
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ val raster = inputExpressions(0).toRaster(input)
+ if (raster == null) {
+ null
+ } else {
+ new GenericArrayData(RasterAccessors.metadata(raster))
+ }
+ }
+
+ override def dataType: DataType = ArrayType(DoubleType)
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT)
+}
\ No newline at end of file
diff --git
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterConstructors.scala
similarity index 59%
copy from
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
copy to
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterConstructors.scala
index c71f9963..7a385b78 100644
---
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterConstructors.scala
@@ -18,14 +18,14 @@
*/
package org.apache.spark.sql.sedona_sql.expressions.raster
-import org.apache.sedona.common.raster.{Constructors, Serde}
+import org.apache.sedona.common.raster.{RasterConstructors, Serde}
import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression}
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
+import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression, ImplicitCastInputTypes}
import org.apache.spark.sql.sedona_sql.UDT.RasterUDT
import org.apache.spark.sql.sedona_sql.expressions.SerdeAware
-import org.apache.spark.sql.types.{AbstractDataType, BinaryType, DataType}
import org.apache.spark.sql.sedona_sql.expressions.raster.implicits._
+import org.apache.spark.sql.types._
import org.geotools.coverage.grid.GridCoverage2D
@@ -53,7 +53,7 @@ case class RS_FromArcInfoAsciiGrid(inputExpressions:
Seq[Expression]) extends Ex
if (bytes == null) {
null
} else {
- Constructors.fromArcInfoAsciiGrid(bytes)
+ RasterConstructors.fromArcInfoAsciiGrid(bytes)
}
}
}
@@ -82,7 +82,41 @@ case class RS_FromGeoTiff(inputExpressions: Seq[Expression])
extends Expression
if (bytes == null) {
null
} else {
- Constructors.fromGeoTiff(bytes)
+ RasterConstructors.fromGeoTiff(bytes)
}
}
}
+
+case class RS_MakeEmptyRaster(inputExpressions: Seq[Expression]) extends
Expression with CodegenFallback
+ with ImplicitCastInputTypes with SerdeAware {
+
+ override def nullable: Boolean = true
+
+ override def eval(input: InternalRow): Any = {
+ Option(evalWithoutSerialization(input)).map(Serde.serialize).orNull
+ }
+
+ override def dataType: DataType = RasterUDT
+
+ override def children: Seq[Expression] = inputExpressions
+
+ protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
+ copy(inputExpressions = newChildren)
+ }
+
+ override def inputTypes: Seq[AbstractDataType] = Seq(IntegerType,
IntegerType, IntegerType, DecimalType, DecimalType, DecimalType, DecimalType,
DecimalType, DecimalType, IntegerType)
+
+ override def evalWithoutSerialization(input: InternalRow): GridCoverage2D = {
+ val numBands = inputExpressions(0).eval(input).asInstanceOf[Int]
+ val widthInPixels = inputExpressions(1).eval(input).asInstanceOf[Int]
+ val heightInPixel = inputExpressions(2).eval(input).asInstanceOf[Int]
+ val upperLeftX =
inputExpressions(3).eval(input).asInstanceOf[Decimal].toDouble
+ val upperLeftY =
inputExpressions(4).eval(input).asInstanceOf[Decimal].toDouble
+ val pixelSizeX =
inputExpressions(5).eval(input).asInstanceOf[Decimal].toDouble
+ val pixelSizeY =
inputExpressions(6).eval(input).asInstanceOf[Decimal].toDouble
+ val skewX = inputExpressions(7).eval(input).asInstanceOf[Decimal].toDouble
+ val skewY = inputExpressions(8).eval(input).asInstanceOf[Decimal].toDouble
+ val srid = inputExpressions(9).eval(input).asInstanceOf[Int]
+ RasterConstructors.makeEmptyRaster(numBands, widthInPixels, heightInPixel,
upperLeftX, upperLeftY, pixelSizeX, pixelSizeY, skewX, skewY, srid)
+ }
+}
\ No newline at end of file
diff --git
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterEditors.scala
similarity index 56%
rename from
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
rename to
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterEditors.scala
index c71f9963..db63fb6c 100644
---
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Constructors.scala
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterEditors.scala
@@ -18,54 +18,32 @@
*/
package org.apache.spark.sql.sedona_sql.expressions.raster
-import org.apache.sedona.common.raster.{Constructors, Serde}
+import org.apache.sedona.common.raster.{RasterEditors, Serde}
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.{ExpectsInputTypes,
Expression}
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
import org.apache.spark.sql.sedona_sql.UDT.RasterUDT
import org.apache.spark.sql.sedona_sql.expressions.SerdeAware
-import org.apache.spark.sql.types.{AbstractDataType, BinaryType, DataType}
-import org.apache.spark.sql.sedona_sql.expressions.raster.implicits._
+import
org.apache.spark.sql.sedona_sql.expressions.raster.implicits.RasterInputExpressionEnhancer
+import org.apache.spark.sql.types.{AbstractDataType, DataType, IntegerType}
import org.geotools.coverage.grid.GridCoverage2D
-
-case class RS_FromArcInfoAsciiGrid(inputExpressions: Seq[Expression]) extends
Expression with CodegenFallback
- with ExpectsInputTypes with SerdeAware {
-
+case class RS_SetSRID(inputExpressions: Seq[Expression]) extends Expression
with CodegenFallback with ExpectsInputTypes with SerdeAware {
override def nullable: Boolean = true
override def eval(input: InternalRow): Any = {
Option(evalWithoutSerialization(input)).map(Serde.serialize).orNull
}
- override def dataType: DataType = RasterUDT
-
- override def children: Seq[Expression] = inputExpressions
-
- protected def withNewChildrenInternal(newChildren: IndexedSeq[Expression]) =
{
- copy(inputExpressions = newChildren)
- }
-
- override def inputTypes: Seq[AbstractDataType] = Seq(BinaryType)
-
override def evalWithoutSerialization(input: InternalRow): GridCoverage2D = {
- val bytes = inputExpressions(0).eval(input).asInstanceOf[Array[Byte]]
- if (bytes == null) {
+ val raster = inputExpressions(0).toRaster(input)
+ val srid = inputExpressions(1).eval(input).asInstanceOf[Int]
+ if (raster == null) {
null
} else {
- Constructors.fromArcInfoAsciiGrid(bytes)
+ RasterEditors.setSrid(raster, srid)
}
}
-}
-
-case class RS_FromGeoTiff(inputExpressions: Seq[Expression]) extends
Expression with CodegenFallback
- with ExpectsInputTypes with SerdeAware {
-
- override def nullable: Boolean = true
-
- override def eval(input: InternalRow): Any = {
- Option(evalWithoutSerialization(input)).map(Serde.serialize).orNull
- }
override def dataType: DataType = RasterUDT
@@ -75,14 +53,5 @@ case class RS_FromGeoTiff(inputExpressions: Seq[Expression])
extends Expression
copy(inputExpressions = newChildren)
}
- override def inputTypes: Seq[AbstractDataType] = Seq(BinaryType)
-
- override def evalWithoutSerialization(input: InternalRow): GridCoverage2D = {
- val bytes = inputExpressions(0).eval(input).asInstanceOf[Array[Byte]]
- if (bytes == null) {
- null
- } else {
- Constructors.fromGeoTiff(bytes)
- }
- }
-}
+ override def inputTypes: Seq[AbstractDataType] = Seq(RasterUDT, IntegerType)
+}
\ No newline at end of file
diff --git
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Outputs.scala
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterOutputs.scala
similarity index 85%
rename from
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Outputs.scala
rename to
sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterOutputs.scala
index 632c9c46..195ed0c0 100644
---
a/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/Outputs.scala
+++
b/sql/common/src/main/scala/org/apache/spark/sql/sedona_sql/expressions/raster/RasterOutputs.scala
@@ -18,10 +18,10 @@
*/
package org.apache.spark.sql.sedona_sql.expressions.raster
-import org.apache.sedona.common.raster.Outputs
+import org.apache.sedona.common.raster.RasterOutputs
import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.Expression
import org.apache.spark.sql.catalyst.expressions.codegen.CodegenFallback
-import org.apache.spark.sql.catalyst.expressions.{Expression, Literal}
import
org.apache.spark.sql.sedona_sql.expressions.raster.implicits.RasterInputExpressionEnhancer
import org.apache.spark.sql.types._
import org.apache.spark.unsafe.types.UTF8String
@@ -36,10 +36,10 @@ case class RS_AsGeoTiff(inputExpressions: Seq[Expression])
extends Expression wi
if (raster == null) return null
// If there are more than one input expressions, the additional ones are
used as parameters
if (inputExpressions.length > 1) {
- Outputs.asGeoTiff(raster,
inputExpressions(1).eval(input).asInstanceOf[UTF8String].toString,
inputExpressions(2).eval(input).toString.toFloat)
+ RasterOutputs.asGeoTiff(raster,
inputExpressions(1).eval(input).asInstanceOf[UTF8String].toString,
inputExpressions(2).eval(input).toString.toFloat)
}
else {
- Outputs.asGeoTiff(raster, null, -1)
+ RasterOutputs.asGeoTiff(raster, null, -1)
}
}
@@ -61,10 +61,10 @@ case class RS_AsArcGrid(inputExpressions: Seq[Expression])
extends Expression wi
if (raster == null) return null
// If there are more than one input expressions, the additional ones are
used as parameters
if (inputExpressions.length > 1) {
- Outputs.asArcGrid(raster,
inputExpressions(1).eval(input).asInstanceOf[Int])
+ RasterOutputs.asArcGrid(raster,
inputExpressions(1).eval(input).asInstanceOf[Int])
}
else {
- Outputs.asArcGrid(raster, -1)
+ RasterOutputs.asArcGrid(raster, -1)
}
}
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 bc189785..0c35e680 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
@@ -68,8 +68,6 @@ class rasteralgebraTest extends TestBaseScala with
BeforeAndAfter with GivenWhen
var inputDf = Seq((Seq(200.0, 400.0, 600.0))).toDF("Band")
val expectedDF = Seq((Seq(600.0, 1200.0, 1800.0))).toDF("multiply")
inputDf = inputDf.selectExpr("RS_MultiplyFactor(Band, 3) as multiply")
- expectedDF.show()
- inputDf.show()
assert(inputDf.first().getAs[mutable.WrappedArray[Double]](0) ==
expectedDF.first().getAs[mutable.WrappedArray[Double]](0))
}
@@ -101,8 +99,6 @@ class rasteralgebraTest extends TestBaseScala with
BeforeAndAfter with GivenWhen
var inputDf = Seq((Seq(200.0, 400.0, 600.0), Seq(200.0, 500.0,
800.0))).toDF("Band1", "Band2")
val expectedDF = Seq((Seq(0.0, 0.11, 0.14))).toDF("normalizedDifference")
inputDf = inputDf.selectExpr("RS_NormalizedDifference(Band1,Band2) as
normalizedDifference")
- expectedDF.show()
- inputDf.show()
assert(inputDf.first().getAs[mutable.WrappedArray[Double]](0) ==
expectedDF.first().getAs[mutable.WrappedArray[Double]](0))
}
@@ -120,8 +116,6 @@ class rasteralgebraTest extends TestBaseScala with
BeforeAndAfter with GivenWhen
var inputDf = Seq((Seq(0.42, 0.36, 0.18, 0.20, 0.21, 0.2001, 0.19)),
(Seq(0.14, 0.13, 0.10, 0.86, 0.01))).toDF("Band")
val expectedDF = Seq((Seq(1.0,1.0,0.0,0.0,1.0,1.0,0.0)),
(Seq(0.0,0.0,0.0,0.0,1.0,0.0))).toDF("GreaterThan")
inputDf = inputDf.selectExpr("RS_GreaterThan(Band, 0.2) as GreaterThan")
- inputDf.show()
- expectedDF.show()
assert(inputDf.first().getAs[mutable.WrappedArray[Double]](0) ==
expectedDF.first().getAs[mutable.WrappedArray[Double]](0))
}
@@ -383,5 +377,63 @@ class rasteralgebraTest extends TestBaseScala with
BeforeAndAfter with GivenWhen
val binaryDf = rasterDf.selectExpr("RS_FromArcInfoAsciiGrid(arc) as
raster", "RS_FromArcInfoAsciiGrid(arc2) as raster2")
assertEquals(rasterDf.count(), binaryDf.count())
}
+
+ it("Passed RS_Metadata") {
+ val df = sparkSession.read.format("binaryFile").load(resourceFolder +
"raster/test1.tiff")
+ val result =
df.selectExpr("RS_Metadata(RS_FromGeoTiff(content))").first().getSeq(0)
+ assertEquals(10, result.length)
+ assertEquals(512.0, result(2), 0.001)
+ assertEquals(517.0, result(3), 0.001)
+ assertEquals(1.0, result(9), 0.001)
+ }
+
+ it("Passed RS_MakeEmptyRaster") {
+ val widthInPixel = 10
+ val heightInPixel = 10
+ val upperLeftX = 0.0
+ val upperLeftY = 0.0
+ val cellSize = 1.0
+ val numBands = 2
+ // Test without skewX, skewY, srid
+ var result = sparkSession.sql(s"SELECT
RS_Metadata(RS_MakeEmptyRaster($numBands, $widthInPixel, $heightInPixel,
$upperLeftX, $upperLeftY, $cellSize))").first().getSeq(0)
+ assertEquals(numBands, result(9), 0.001)
+
+ // Test with integer type input
+ result = sparkSession.sql(s"SELECT
RS_Metadata(RS_MakeEmptyRaster($numBands, $widthInPixel, $heightInPixel,
${upperLeftX.toInt}, ${upperLeftY.toInt},
${cellSize.toInt}))").first().getSeq(0)
+ assertEquals(numBands, result(9), 0.001)
+
+ // Test with skewX, skewY, srid
+ 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)
+ assertEquals(numBands, result(9), 0.001)
+ }
+
+ it("Passed RS_BandAsArray") {
+ val df = sparkSession.read.format("binaryFile").load(resourceFolder +
"raster/test1.tiff")
+ val metadata =
df.selectExpr("RS_Metadata(RS_FromGeoTiff(content))").first().getSeq(0)
+ val width = metadata(2).asInstanceOf[Double].toInt
+ val height = metadata(3).asInstanceOf[Double].toInt
+ val result = df.selectExpr("RS_BandAsArray(RS_FromGeoTiff(content),
1)").first().getSeq(0)
+ assertEquals(width * height, result.length)
+ val resultOutOfBound =
df.selectExpr("RS_BandAsArray(RS_FromGeoTiff(content),
100)").first().isNullAt(0)
+ assertEquals(true, resultOutOfBound)
+ }
+
+ it("Passed RS_AddBandFromArray") {
+ var df = sparkSession.read.format("binaryFile").load(resourceFolder +
"raster/test1.tiff")
+ df = df.selectExpr("RS_FromGeoTiff(content) as raster",
"RS_MultiplyFactor(RS_BandAsArray(RS_FromGeoTiff(content), 1), 2) as band")
+ val bandNewExpected:Seq[Double] = df.selectExpr("band").first().getSeq(0)
+ val bandNewActual:Seq[Double] =
df.selectExpr("RS_BandAsArray(RS_AddBandFromArray(raster, band, 1),
1)").first().getSeq(0)
+ for (i <- bandNewExpected.indices) {
+ // The band value needs to be mod 256 because the ColorModel will mod
256.
+ assertEquals(bandNewExpected(i)%256, bandNewActual(i), 0.001)
+ }
+ // Test with out of bound band index. It should fail.
+ intercept[Exception] {
+ df.selectExpr("RS_BandAsArray(RS_AddBandFromArray(raster, band, 100),
1)").collect()
+ }
+ }
}
}
diff --git
a/sql/common/src/test/scala/org/apache/sedona/sql/serdeAwareTest.scala
b/sql/common/src/test/scala/org/apache/sedona/sql/serdeAwareTest.scala
index 27f0ca54..786af274 100644
--- a/sql/common/src/test/scala/org/apache/sedona/sql/serdeAwareTest.scala
+++ b/sql/common/src/test/scala/org/apache/sedona/sql/serdeAwareTest.scala
@@ -20,7 +20,7 @@
package org.apache.sedona.sql
import org.apache.sedona.common.geometrySerde.GeometrySerializer
-import org.apache.sedona.common.raster.Constructors.fromArcInfoAsciiGrid
+import org.apache.sedona.common.raster.RasterConstructors.fromArcInfoAsciiGrid
import org.apache.sedona.common.raster.Serde
import org.apache.spark.sql.catalyst.expressions.Literal
import org.apache.spark.sql.sedona_sql.expressions.{ST_Buffer,
ST_GeomFromText, ST_Point, ST_Union}