Coverage Processor Tutorial

Welcome

Welcome to the coverage processor tutorial. This tutorial assumes that you have followed one of the quickstart tutorials.

Please ensure you have your IDE set up with access to the GeoTools jars (either through maven or a directory of Jar files). The maven dependencies required will be listed at the start of the in the pre-requisites.

This workbook introduces performing common operations – such as multiple, resample, crop, etc. – directly on coverage objects.

Image Tiling Application

The ImageLab tutorial covered loading and rendering coverages; this tutorial will demonstrate performing basic operations – such as crop and scale – directly on a coverage using the CoverageProcessor and friends, as well as use the Arguments tool to make command line processing a little simpler. We will be creating a simple utility application to “tile” a coverage (for the sake of simplicity simply subdividing the envelope) and optionally scaling the resulting tiles.

Pre-requisites

Many of these may already be in our pom.xml from earlier examples, but at a minimum ensure that the following dependencies are available

    <dependencies>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-shapefile</artifactId>
            <version>${geotools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-epsg-hsql</artifactId>
            <version>${geotools.version}</version>
        </dependency>
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-geotiff</artifactId>
            <version>${geotools.version}</version>
        </dependency>
    </dependencies>

Create the file ImageTiler.java in the package org.geotools.tutorial.coverage and copy and paste in the following code, which contains our boilerplate imports, fields and getters/setters:

package org.geotools.tutorial.coverage;

import java.io.File;
import java.io.IOException;
import org.geotools.api.geometry.Bounds;
import org.geotools.api.parameter.ParameterValueGroup;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.coverage.grid.io.GridFormatFinder;
import org.geotools.coverage.processing.CoverageProcessor;
import org.geotools.coverage.processing.Operations;
import org.geotools.gce.geotiff.GeoTiffFormat;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.Arguments;
import org.geotools.util.factory.Hints;

/**
 * Simple tiling of a coverage based simply on the number vertical/horizontal tiles desired and
 * subdividing the geographic envelope. Uses coverage processing operations.
 */
public class ImageTiler {

    private final int NUM_HORIZONTAL_TILES = 16;
    private final int NUM_VERTICAL_TILES = 8;

    private Integer numberOfHorizontalTiles = NUM_HORIZONTAL_TILES;
    private Integer numberOfVerticalTiles = NUM_VERTICAL_TILES;
    private Double tileScale;
    private File inputFile;
    private File outputDirectory;

    private String getFileExtension(File file) {
        String name = file.getName();
        try {
            return name.substring(name.lastIndexOf(".") + 1);
        } catch (Exception e) {
            return "";
        }
    }

    public Integer getNumberOfHorizontalTiles() {
        return numberOfHorizontalTiles;
    }

    public void setNumberOfHorizontalTiles(Integer numberOfHorizontalTiles) {
        this.numberOfHorizontalTiles = numberOfHorizontalTiles;
    }

    public Integer getNumberOfVerticalTiles() {
        return numberOfVerticalTiles;
    }

    public void setNumberOfVerticalTiles(Integer numberOfVerticalTiles) {
        this.numberOfVerticalTiles = numberOfVerticalTiles;
    }

    public File getInputFile() {
        return inputFile;
    }

    public void setInputFile(File inputFile) {
        this.inputFile = inputFile;
    }

    public File getOutputDirectory() {
        return outputDirectory;
    }

    public void setOutputDirectory(File outputDirectory) {
        this.outputDirectory = outputDirectory;
    }

    public Double getTileScale() {
        return tileScale;
    }

    public void setTileScale(Double tileScale) {
        this.tileScale = tileScale;
    }

Note

Please note that this isn’t the entire code listing – we’ll finish it off as we go along – so don’t worry right now if your IDE complains.

Argument Processing

Since we are creating a command line application, we’re going to want to process command line arguments. GeoTools includes a class named Arguments to facilitate this; we will use this class to parse two mandatory arguments – input file and output directory – and a handful of optional arguments – vertical and horizontal tile counts, and tile scaling.

    public static void main(String[] args) throws Exception {

        // GeoTools provides utility classes to parse command line arguments
        Arguments processedArgs = new Arguments(args);
        ImageTiler tiler = new ImageTiler();

        try {
            tiler.setInputFile(new File(processedArgs.getRequiredString("-f")));
            tiler.setOutputDirectory(new File(processedArgs.getRequiredString("-o")));
            tiler.setNumberOfHorizontalTiles(processedArgs.getOptionalInteger("-htc"));
            tiler.setNumberOfVerticalTiles(processedArgs.getOptionalInteger("-vtc"));
            tiler.setTileScale(processedArgs.getOptionalDouble("-scale"));
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
            printUsage();
            System.exit(1);
        }

        tiler.tile();
    }

    private static void printUsage() {
        System.out.println(
                "Usage: -f inputFile -o outputDirectory [-tw tileWidth<default:256> "
                        + "-th tileHeight<default:256> ");
        System.out.println(
                "-htc horizontalTileCount<default:16> -vtc verticalTileCount<default:8>");
    }

Loading the coverage

First we need to load the coverage; GeoTools provides GridFormatFinder and AbstractGridFormat in order to do this abstractly. Note: there is a slight quirk with GeoTiff handling as of this writing that we handle separately.

    private void tile() throws IOException {
        AbstractGridFormat format = GridFormatFinder.findFormat(this.getInputFile());
        String fileExtension = this.getFileExtension(this.getInputFile());

        // working around a bug/quirk in geotiff loading via format.getReader which doesn't set this
        // correctly
        Hints hints = null;
        if (format instanceof GeoTiffFormat) {
            hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE);
        }

        GridCoverage2DReader gridReader = format.getReader(this.getInputFile(), hints);
        GridCoverage2D gridCoverage = gridReader.read(null);

Subdividing the coverage

Next we’ll subdivide the coverage based on the requested horizontal and vertical tile counts by asking the coverage for its envelope and dividing that envelope horizontally and vertically by our tile counts. This will give us our tile envelope width and height. Then we’ll loop over our horizontal and vertical tile counts to crop and scale.

        ReferencedEnvelope coverageEnvelope = gridCoverage.getEnvelope2D();
        double coverageMinX = coverageEnvelope.getMinX();
        double coverageMaxX = coverageEnvelope.getMaxX();
        double coverageMinY = coverageEnvelope.getMinY();
        double coverageMaxY = coverageEnvelope.getMaxY();

        int htc =
                this.getNumberOfHorizontalTiles() != null
                        ? this.getNumberOfHorizontalTiles()
                        : NUM_HORIZONTAL_TILES;
        int vtc =
                this.getNumberOfVerticalTiles() != null
                        ? this.getNumberOfVerticalTiles()
                        : NUM_VERTICAL_TILES;

        double geographicTileWidth = (coverageMaxX - coverageMinX) / (double) htc;
        double geographicTileHeight = (coverageMaxY - coverageMinY) / (double) vtc;

        CoordinateReferenceSystem targetCRS = gridCoverage.getCoordinateReferenceSystem();

        // make sure to create our output directory if it doesn't already exist
        File tileDirectory = this.getOutputDirectory();
        if (!tileDirectory.exists()) {
            tileDirectory.mkdirs();
        }

        // iterate over our tile counts
        for (int i = 0; i < htc; i++) {
            for (int j = 0; j < vtc; j++) {

                System.out.println("Processing tile at indices i: " + i + " and j: " + j);
                // create the envelope of the tile
                Bounds envelope =
                        getTileEnvelope(
                                coverageMinX,
                                coverageMinY,
                                geographicTileWidth,
                                geographicTileHeight,
                                targetCRS,
                                i,
                                j);

                GridCoverage2D finalCoverage = cropCoverage(gridCoverage, envelope);

                if (this.getTileScale() != null) {
                    finalCoverage = scaleCoverage(finalCoverage);
                }

                // use the AbstractGridFormat's writer to write out the tile
                File tileFile = new File(tileDirectory, i + "_" + j + "." + fileExtension);
                format.getWriter(tileFile).write(finalCoverage, null);
            }
        }
    }

Creating our tile envelope

We’ll create the envelope of our tile based on our indexes and target enveloped width and height:

    private Bounds getTileEnvelope(
            double coverageMinX,
            double coverageMinY,
            double geographicTileWidth,
            double geographicTileHeight,
            CoordinateReferenceSystem targetCRS,
            int horizontalIndex,
            int verticalIndex) {

        double envelopeStartX = (horizontalIndex * geographicTileWidth) + coverageMinX;
        double envelopeEndX = envelopeStartX + geographicTileWidth;
        double envelopeStartY = (verticalIndex * geographicTileHeight) + coverageMinY;
        double envelopeEndY = envelopeStartY + geographicTileHeight;

        return new ReferencedEnvelope(
                envelopeStartX, envelopeEndX, envelopeStartY, envelopeEndY, targetCRS);
    }

Cropping

Now that we have the tile envelope width and height we’ll iterate over our tile counts and crop based on our target envelope. In this example we will manually create our parameters and use the coverage processor to perform the CoverageCrop operation. We’ll encounter slightly simpler ways to perform coverage operations in the next step.

    private GridCoverage2D cropCoverage(GridCoverage2D gridCoverage, Bounds envelope) {
        CoverageProcessor processor = CoverageProcessor.getInstance();

        // An example of manually creating the operation and parameters we want
        final ParameterValueGroup param = processor.getOperation("CoverageCrop").getParameters();
        param.parameter("Source").setValue(gridCoverage);
        param.parameter("Envelope").setValue(envelope);

        return (GridCoverage2D) processor.doOperation(param);
    }

Scaling

We can use the Scale operation to optionally scale our tiles. In this example we’ll use the Operations class to make our lives a little easier. This class wraps operations and provides a slightly more type safe interface to them. Here we will scale our X and Y dimensions by the same factor in order to preserve the aspect ratio of our original coverage.

    /**
     * Scale the coverage based on the set tileScale
     *
     * <p>As an alternative to using parameters to do the operations, we can use the Operations
     * class to do them in a slightly more type safe way.
     *
     * @param coverage the coverage to scale
     * @return the scaled coverage
     */
    private GridCoverage2D scaleCoverage(GridCoverage2D coverage) {
        Operations ops = new Operations(null);
        coverage =
                (GridCoverage2D)
                        ops.scale(coverage, this.getTileScale(), this.getTileScale(), 0, 0);
        return coverage;
    }
}

Running the application

Before we can run the application we’ll need sample data. The Natural Earth 50m data will do nicely.

Running with an IDE

If you’ve been following along in an IDE then the built in run functionality is the easiest way to run our application. For example, in Eclipse we can select Run -> Run Configurations from the menu and create a new Java Application Config with the following configuration.

../../_images/runconf.png

Under the Arguments tab we’ll point our application at the downloaded raster data, give it a tile count of 16x8, output it to a temp director and scale the tiles by two.

-f /Users/devon/Downloads/NE2_50M_SR_W/NE2_50M_SR_W.tif -htc 16 -vtc 8 -o /Users/devon/tmp/tiles -scale 2.0

Be sure to replace the filename and directory with your own locations.

Note

If you’re using Eclipse then these paths can be replaced by prompts. In the Run dialog choose Variables and use the file_prompt and folder_prompt variables.

../../_images/runargs.png

Finally, hit Run to run our application. You may see some warning/info messages related to ImageIO, but these are normal.

IDEs other than Eclipse, such as Netbeans and IntelliJ, have very similar options for running a Java application.

Running With Maven

If you’re not using an IDE then the easiest way to run our application is to use the Maven exec task to run our application for us, as detailed in the Maven Quickstart. We simply need to add the Maven Shade plugin to our pom.xml

mvn exec:java -Dexec.mainClass=org.geotools.tutorial.ImageTiler -Dexec.args="-f /Users/devon/Downloads/NE2_50M_SR_W/NE2_50M_SR_W.tif -htc 16 -vtc 8 -o /Users/devon/tmp/tiles -scale 2.0"

Things to Try

  • See the Coverage Processor documentation for more information about the operations available on coverages. One of the operations available in the CoverageProcessor is Resample (see the Operations class), which we can use to reproject our coverage very easily. Try reprojecting the coverage into EPSG:3587 (Google’s Web Mercator projection).

  • We can verify that our tiles look OK by loading them into the GeoServer ImageMosaic store. Alternatively we could programmatically create an ImageMosaicReader pointed at our directory files and read from it.