GeoTools

OSGeo

Table Of Contents

Previous topic

JAI-EXT Tutorial

Next topic

Map Style Tutorial

This Page

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

Previous tutorials 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 (simply by subdividing the geographic envelope) and optionally scale 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 and copy and paste in the following code, which contains our boilerplate imports, fields and getters/setters:

package org.geotools.tutorial.coverage;

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.factory.Hints;
import org.geotools.gce.geotiff.GeoTiffFormat;
import org.geotools.geometry.Envelope2D;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.resources.Arguments;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

import java.io.File;
import java.io.IOException;

/**
 * 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;
    }

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.

    /**
     * Argument parsing and initial setup.
     *
     * @param args Program arguments
     * @throws Exception
     */
    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.

        Envelope2D coverageEnvelope = gridCoverage.getEnvelope2D();
        double coverageMinX = coverageEnvelope.getBounds().getMinX();
        double coverageMaxX = coverageEnvelope.getBounds().getMaxX();
        double coverageMinY = coverageEnvelope.getBounds().getMinY();
        double coverageMaxY = coverageEnvelope.getBounds().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++) {

                //create the envelope of the tile
                Envelope envelope = getTileEnvelope(coverageMinX, coverageMinY, geographicTileWidth,
                        geographicTileHeight, targetCRS, i, j);

                GridCoverage2D finalCoverage = cropCoverage(gridCoverage, envelope);

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

                //docs start output
                File tileFile = new File(tileDirectory, i + "_" + j + "." + fileExtension);
                format.getWriter(tileFile).write(finalCoverage, null);
                //docs end output
            }
        }

    }
}

Creating our tile envelope

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

    /**
     * Create the target tile envelope.
     * @param coverageMinX minimum x of our coverage
     * @param coverageMinY minimum y of our coverage
     * @param geographicTileWidth our target tile envelope width
     * @param geographicTileHeight our target tile envelope height
     * @param targetCRS the target tile CRS
     * @param horizontalIndex horizontal index of the tile envelope
     * @param verticalIndex vertical index of the tile envelope
     * @return tile envelope
     */
    private Envelope 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.

    /**
     * Crop the coverage to the given envelope
     * @param gridCoverage coverage to crp
     * @param envelope envelope to crop it to
     * @return the cropped coverage
     */
    private GridCoverage2D cropCoverage(GridCoverage2D gridCoverage, Envelope 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
     *
     * 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;
    }

Output

Finally we’ll use the AbstractGridFormat instance we retrieved earlier to save our tile, plus finish off our class definition.

                File tileFile = new File(tileDirectory, i + "_" + j + "." + fileExtension);
                format.getWriter(tileFile).write(finalCoverage, null);

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.

../../_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 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"

Extra Resources

  • We can verify that our tiles look OK by loading them into the GeoServer ImageMosaic. store.
  • See the Coverage Processor. documentation for more information about the operations available on coverages