/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.utils.coveragetiler;
import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageWriteParam;
import org.apache.commons.cli2.Option;
import org.apache.commons.cli2.validation.InvalidArgumentException;
import org.apache.commons.cli2.validation.Validator;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.GridFormatFinder;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.coverage.grid.io.UnknownFormat;
import org.geotools.coverage.grid.io.imageio.GeoToolsWriteParams;
import org.geotools.factory.Hints;
import org.geotools.gce.geotiff.GeoTiffFormat;
import org.geotools.gce.geotiff.GeoTiffWriteParams;
import org.geotools.gce.geotiff.GeoTiffWriter;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.utils.CoverageToolsConstants;
import org.geotools.utils.progress.BaseArgumentsManager;
import org.geotools.utils.progress.ExceptionEvent;
import org.geotools.utils.progress.ProcessingEvent;
import org.geotools.utils.progress.ProcessingEventListener;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
/**
* <p>
* This utility splits rasters into smaller pieces. One can control both the
* dimension of the tile that will be generated as well as the dimension of the
* internal tiles for the, improvements. This would allows us not only to break
* a big coverage into smaller tiles, but also to do the opposite. One may want
* to compose a mosaic and retile it into bigger tiles, well this can be easily
* done with this utility.
* </p>
*
* <p>
* Example of usage:<br/>
* <code>CoverageTiler -t "8192,8192" -it "512,512" -s "/usr/home/tmp/myImage.tiff"</code>
* </p>
*
* <p>
* The tiles will be stored on the folder <code>"/usr/home/tmp/tiled"</code>,
* which will be automatically created.
* </p>
*
*
* @author Simone Giannecchini, GeoSolutions
* @author Alessio Fabiani, GeoSolutions
*
*
*
* @source $URL$
* @version 0.3
*
*/
public class CoverageTiler extends BaseArgumentsManager implements
ProcessingEventListener, Runnable {
/** Default Logger * */
private final static Logger LOGGER = Logger.getLogger(CoverageTiler.class.toString());
/** Program Version */
private final static String VERSION = "0.3";
private static final String NAME = "CoverageTiler";
private Option inputLocationOpt;
private Option outputLocationOpt;
private Option tileDimOpt;
private Option compressionTypeOpt;
private Option compressionRatioOpt;
private Option internalTileDimOpt;
private File inputLocation;
private File outputLocation;
private int tileWidth;
private int tileHeight;
private int internalTileWidth = CoverageToolsConstants.DEFAULT_INTERNAL_TILE_WIDTH;
private int internalTileHeight = CoverageToolsConstants.DEFAULT_INTERNAL_TILE_HEIGHT;
private String compressionScheme = CoverageToolsConstants.DEFAULT_COMPRESSION_SCHEME;
private double compressionRatio = CoverageToolsConstants.DEFAULT_COMPRESSION_RATIO;
/**
* Default constructor
*/
public CoverageTiler() {
super(NAME, VERSION);
// /////////////////////////////////////////////////////////////////////
// Options for the command line
// /////////////////////////////////////////////////////////////////////
inputLocationOpt = optionBuilder.withShortName("s").withLongName(
"src_coverage").withArgument(
argumentBuilder.withName("source").withMinimum(1)
.withMaximum(1).create()).withDescription(
"path where the source code is located").withRequired(true)
.create();
outputLocationOpt = optionBuilder
.withShortName("d")
.withLongName("dest_directory")
.withArgument(
argumentBuilder.withName("destination").withMinimum(0)
.withMaximum(1).create())
.withDescription(
"output directory, if none is provided, the \"tiled\" directory will be used")
.withRequired(false).create();
tileDimOpt = optionBuilder.withShortName("t").withLongName(
"tile_dimension").withArgument(
argumentBuilder.withName("t").withMinimum(1).withMaximum(1)
.create()).withDescription(
"Width and height of each tile we generate").withRequired(true)
.create();
internalTileDimOpt = optionBuilder.withShortName("it").withLongName(
"internal_tile_dimension").withArgument(
argumentBuilder.withName("it").withMinimum(0).withMaximum(1)
.create()).withDescription(
"Internal width and height of each tile we generate")
.withRequired(false).create();
compressionTypeOpt = optionBuilder
.withShortName("z")
.withLongName("compressionType")
.withDescription("compression type.")
.withArgument(
argumentBuilder.withName("compressionType")
.withMinimum(0).withMaximum(1).withValidator(
new Validator() {
public void validate(List args)
throws InvalidArgumentException {
final int size = args.size();
if (size > 1)
throw new InvalidArgumentException(
"Only one scaling algorithm at a time can be chosen");
}
}).create()).withRequired(false)
.create();
compressionRatioOpt = optionBuilder
.withShortName("r")
.withLongName("compressionRatio")
.withDescription("compression ratio.")
.withArgument(
argumentBuilder.withName("compressionRatio")
.withMinimum(0).withMaximum(1).withValidator(
new Validator() {
public void validate(List args)
throws InvalidArgumentException {
final int size = args.size();
if (size > 1)
throw new InvalidArgumentException(
"Only one scaling algorithm at a time can be chosen");
final String val = (String) args
.get(0);
final double value = Double
.parseDouble(val);
if (value <= 0 || value > 1)
throw new InvalidArgumentException(
"Invalid compressio ratio");
}
}).create()).withRequired(false)
.create();
addOption(tileDimOpt);
addOption(inputLocationOpt);
addOption(outputLocationOpt);
addOption(internalTileDimOpt);
addOption(compressionTypeOpt);
addOption(compressionRatioOpt);
// /////////////////////////////////////////////////////////////////////
//
// Help Formatter
//
// /////////////////////////////////////////////////////////////////////
finishInitialization();
}
/**
* @param args
* @throws MalformedURLException
* @throws InterruptedException
*/
public static void main(String[] args) throws MalformedURLException,
InterruptedException {
final CoverageTiler coverageTiler = new CoverageTiler();
coverageTiler.addProcessingEventListener(coverageTiler);
if (coverageTiler.parseArgs(args)) {
final Thread t = new Thread(coverageTiler, NAME);
t.setPriority(coverageTiler.getPriority());
t.start();
try {
t.join();
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
} else
LOGGER.fine("Exiting...");
}
/**
* This method is responsible for sending the process progress events to the
* logger.
*
* <p>
* It should be used to do normal logging when running this tools as command
* line tools but it should be disable when putting the tool behind a GUI.
* In such a case the GUI should register itself as a
* {@link ProcessingEventListener} and consume the processing events.
*
* @param event
* is a {@link ProcessingEvent} that informs the receiver on the
* precetnage of the progress as well as on what is happening.
*/
public void getNotification(ProcessingEvent event) {
LOGGER.info(new StringBuilder("Progress is at ").append(
event.getPercentage()).append("\n").append(
"attached message is: ").append(event.getMessage()).toString());
}
public void exceptionOccurred(ExceptionEvent event) {
LOGGER.log(Level.SEVERE, "An error occurred during processing", event
.getException());
}
/*
* (non-Javadoc)
*
* @see it.geosolutions.utils.progress.ProgressManager#run()
*/
@SuppressWarnings("deprecation")
public void run() {
// /////////////////////////////////////////////////////////////////////
//
//
// Trying to acquire a reader for the provided source file.
//
//
// /////////////////////////////////////////////////////////////////////
StringBuilder message = new StringBuilder("Acquiring a reader to ")
.append(inputLocation);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 0);
// get the format of this file, if it is recognized!
final AbstractGridFormat format = (AbstractGridFormat) GridFormatFinder.findFormat(inputLocation);
if (format == null || format instanceof UnknownFormat) {
fireException(
"Unable to decide format for this coverage",
0,
new IOException("Could not find a format for this coverage"));
return;
}
// get a reader for this file
final AbstractGridCoverage2DReader inReader =
(AbstractGridCoverage2DReader) format.getReader(inputLocation, new Hints(Hints.OVERVIEW_POLICY, OverviewPolicy.IGNORE));
if (inReader == null) {
message = new StringBuilder(
"Unable to instantiate a reader for this coverage");
if (LOGGER.isLoggable(Level.WARNING))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 0);
return;
}
// /////////////////////////////////////////////////////////////////////
//
//
// If everything went fine, let's proceed with tiling this coverage.
//
//
// /////////////////////////////////////////////////////////////////////
if (!outputLocation.exists())
outputLocation.mkdir();
// //
//
// getting source envelope and crs
//
// //
final GeneralEnvelope envelope = inReader.getOriginalEnvelope();
message = new StringBuilder("Original envelope is ").append(envelope
.toString());
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 0);
// //
//
// getting source gridrange and checking tile dimensions to be not
// bigger than the original coverage size
//
// //
final GridEnvelope range = inReader.getOriginalGridRange();
final int w = range.getSpan(0);
final int h = range.getSpan(1);
tileWidth = tileWidth > w ? w : tileWidth;
tileHeight = tileHeight > h ? h : tileHeight;
message = new StringBuilder("Original range is ").append(range.toString());
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 0);
message = new StringBuilder("New matrix dimension is (cols,rows)==(")
.append(tileWidth).append(",").append(tileHeight).append(")");
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 0);
// //
//
// read the coverage
//
// //
GridCoverage2D gc;
try {
gc = (GridCoverage2D) inReader.read(null);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
fireException(e);
return;
}
// ///////////////////////////////////////////////////////////////////
//
// MAIN LOOP
//
//
// ///////////////////////////////////////////////////////////////////
final int numTileX = (int) (w / (tileWidth * 1.0) + 1);
final int numTileY = (int) (h / (tileHeight * 1.0) + 1);
for (int i = 0; i < numTileX; i++)
for (int j = 0; j < numTileY; j++) {
// //
//
// computing the bbox for this tile
//
// //
final Rectangle sourceRegion = new Rectangle(i * tileWidth, j* tileHeight, tileWidth, tileHeight);
message = new StringBuilder("Writing region ").append(sourceRegion);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), (i + j)
/ (numTileX * numTileY * 1.0));
// //
//
// building gridgeometry for the read operation with the actual
// envelope
//
// //
final File fileOut = new File(outputLocation, new StringBuilder(
"mosaic").append("_").append(
Integer.toString(i * tileWidth + j)).append(".")
.append("tiff").toString());
// remove an old output file if it exists
if (fileOut.exists())
fileOut.delete();
message = new StringBuilder(
"Preparing to write tile (col,row)==(").append(j)
.append(",").append(i).append(") to file ").append(
fileOut);
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), (i + j)
/ (numTileX * numTileY * 1.0));
// //
//
// Write this coverage out as a geotiff
//
// //
final AbstractGridFormat outFormat = new GeoTiffFormat();
try {
final GeoTiffWriteParams wp = new GeoTiffWriteParams();
wp.setTilingMode(GeoToolsWriteParams.MODE_EXPLICIT);
wp.setTiling(internalTileWidth, internalTileHeight);
wp.setSourceRegion(sourceRegion);
if (this.compressionScheme != null&& !Double.isNaN(compressionRatio)) {
wp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
wp.setCompressionType(compressionScheme);
wp.setCompressionQuality((float) this.compressionRatio);
}
final ParameterValueGroup params = outFormat.getWriteParameters();
params.parameter(AbstractGridFormat.GEOTOOLS_WRITE_PARAMS.getName().toString()).setValue(wp);
final GeoTiffWriter writerWI = new GeoTiffWriter(fileOut);
writerWI.write(gc, (GeneralParameterValue[]) params.values().toArray(new GeneralParameterValue[1]));
writerWI.dispose();
} catch (IOException e) {
fireException(e);
return;
}
}
message = new StringBuilder("Done...");
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(message.toString());
fireEvent(message.toString(), 100);
}
public boolean parseArgs(String[] args) {
if (!super.parseArgs(args))
return false;
// ////////////////////////////////////////////////////////////////
//
// Parsing command line parameters and setting up
// Mosaic Index Builder options
//
// ////////////////////////////////////////////////////////////////
inputLocation = new File((String) getOptionValue(inputLocationOpt));
// output files' directory
if (hasOption(outputLocationOpt))
outputLocation = new File(
(String) getOptionValue(outputLocationOpt));
else
outputLocation = new File(inputLocation.getParentFile(), "tiled");
// //
//
// tile dim
//
// //
final String tileDim = (String) getOptionValue(tileDimOpt);
String[] pairs = tileDim.split(",");
tileWidth = Integer.parseInt(pairs[0]);
tileHeight = Integer.parseInt(pairs[1]);
// //
//
// Internal Tile dim
//
// //
final String internalTileDim = (String) getOptionValue(internalTileDimOpt);
if (internalTileDim != null && internalTileDim.length() > 0) {
pairs = internalTileDim.split(",");
internalTileWidth = Integer.parseInt(pairs[0]);
internalTileHeight = Integer.parseInt(pairs[1]);
}
// //
//
// Compression params
//
// //
// index name
if (hasOption(compressionTypeOpt)) {
compressionScheme = (String) getOptionValue(compressionTypeOpt);
if (compressionScheme == "")
compressionScheme = null;
}
if (hasOption(compressionRatioOpt)) {
try {
compressionRatio = Double
.parseDouble((String) getOptionValue(compressionRatioOpt));
} catch (Exception e) {
compressionRatio = Double.NaN;
}
}
return true;
}
public File getInputLocation() {
return inputLocation;
}
public void setInputLocation(File inputLocation) {
this.inputLocation = inputLocation;
}
public int getTileWidth() {
return tileWidth;
}
public void setTileWidth(int numTileX) {
this.tileWidth = numTileX;
}
public int getTileHeight() {
return tileHeight;
}
public void setTileHeight(int numTileY) {
this.tileHeight = numTileY;
}
public File getOutputLocation() {
return outputLocation;
}
public void setOutputLocation(File outputLocation) {
this.outputLocation = outputLocation;
}
public final double getCompressionRatio() {
return compressionRatio;
}
public final void setCompressionRatio(double compressionRatio) {
this.compressionRatio = compressionRatio;
}
public final String getCompressionScheme() {
return compressionScheme;
}
public final void setCompressionScheme(String compressionScheme) {
this.compressionScheme = compressionScheme;
}
public int getInternalTileHeight() {
return internalTileHeight;
}
public void setInternalTileHeight(int internalTileHeight) {
this.internalTileHeight = internalTileHeight;
}
public int getInternalTileWidth() {
return internalTileWidth;
}
public void setInternalTileWidth(int internalTileWidth) {
this.internalTileWidth = internalTileWidth;
}
}