/* * 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.imagepyramid; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.cli2.Option; import org.apache.commons.cli2.option.DefaultOption; import org.apache.commons.cli2.validation.InvalidArgumentException; import org.apache.commons.cli2.validation.Validator; import org.apache.commons.io.FileUtils; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.coverage.grid.io.AbstractGridFormat; import org.geotools.coverage.grid.io.GridFormatFinder; import org.geotools.geometry.GeneralEnvelope; import org.geotools.utils.CoverageToolsConstants; import org.geotools.utils.coveragetiler.CoverageTiler; import org.geotools.utils.imagemosaic.MosaicIndexBuilder; import org.geotools.utils.progress.BaseArgumentsManager; import org.geotools.utils.progress.ExceptionEvent; import org.geotools.utils.progress.ProcessingEvent; import org.geotools.utils.progress.ProcessingEventListener; /** * Given an original image, builds an image pyramid out of it by combining the * various tiler, mosaic and pyramid layer builder tools. * * <pre> * Example of use: * PyramidBuilder -s "/usr/home/data/home.tif" -f 2 -n 4 -t "25,25" -w * </pre> * * @author Andrea Aime * @author Simone Giannecchini, GeoSolutions. * @since 2.3.x * * * @source $URL$ */ public class PyramidBuilder extends BaseArgumentsManager implements Runnable, ProcessingEventListener { /** Static immutable set for scaling algorithms. */ private static final Set<String> scalingAlgorithms; static { scalingAlgorithms = new HashSet<String>(); scalingAlgorithms.add("nn"); scalingAlgorithms.add("bil"); scalingAlgorithms.add("bic"); scalingAlgorithms.add("avg"); scalingAlgorithms.add("filt"); } /** Program Version */ private final static String VERSION = "0.3"; private final static String NAME = "PyramidBuilder"; /** Commons-cli option for the input location. */ private DefaultOption locationOpt; /** Commons-cli option for the output location. */ private DefaultOption outputLocationOpt; /** Output folder, defaults to the "pyramid" subfolder */ private File outputLocation; /** Commons-cli option for the tile dimension. */ private Option tileDimOpt; /** Commons-cli option for the scale algorithm. */ private Option scaleAlgorithmOpt; /** Commons-cli option for the tile numbe of subsample step to use. */ private Option numStepsOpt; /** Commons-cli option for the scale factor to use. */ private Option scaleFactorOpt; /** * Commons-cli options for overwriting the output layer dirs if already * available */ private Option overwriteOpt; /** Tile width. */ private int tileW = -1; /** Tile height. */ private int tileH = -1; /** Scale algorithm. */ private String scaleAlgorithm; /** Logger for this class. */ private final static Logger LOGGER = org.geotools.util.logging.Logging .getLogger(PyramidBuilder.class.toString()); /** Downsampling step. */ private int scaleFactor; /** * The source path. */ private File inputLocation; /** * The name of the output pyramid, will simply be "pyramid" if not set */ private String name; /** * Commons-cli option for the pyramid name */ private Option nameOpt; private int numSteps; private boolean exceptionOccurred = false; private boolean overwriteOutputDirs = false; private double currStep = 0; private double totalSteps = 0; /** * Re-launches slave tools progress with the appropriate percentage * corrections */ private ProcessingEventListener slaveToolsListener = new ProcessingEventListener() { public void getNotification(ProcessingEvent event) { fireEvent(event.getMessage(), (currStep / totalSteps) * 100 + event.getPercentage() / totalSteps); } public void exceptionOccurred(ExceptionEvent event) { fireException(event.getMessage(), event.getPercentage(), event .getException()); exceptionOccurred = true; } }; private GeneralEnvelope envelope; private double[][] resolutions; private Option compressionRatioOpt; private Option compressionTypeOpt; private Option internalTileDimOpt; 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; /** * Simple constructor for a pyramid generator. Use the input string in order * to read an image. * * */ public PyramidBuilder() { super(NAME, VERSION); // ///////////////////////////////////////////////////////////////////// // Options for the command line // ///////////////////////////////////////////////////////////////////// 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(); locationOpt = optionBuilder .withShortName("s") .withLongName("source") .withArgument( argumentBuilder.withName("source").withMinimum(1) .withMaximum(1).withValidator(new Validator() { public void validate(List args) throws InvalidArgumentException { final int size = args.size(); if (size > 1) throw new InvalidArgumentException( "Source can be a single file or directory "); final File source = new File( (String) args.get(0)); if (!source.exists()) throw new InvalidArgumentException( new StringBuffer( "The provided source is invalid! ") .toString()); } }).create()).withDescription( "path where files are located").withRequired(true) .create(); nameOpt = optionBuilder.withShortName("name").withLongName( "pyramid_name").withArgument( argumentBuilder.withName("name").withMinimum(0).withMaximum(1) .create()).withDescription( "name for the pyramid property file").withRequired(false) .create(); tileDimOpt = optionBuilder.withShortName("t").withLongName( "tiled_dimension").withArgument( argumentBuilder.withName("t").withMinimum(0).withMaximum(1) .create()).withDescription( "tile dimensions as a couple width,height in pixels") .withRequired(true).create(); scaleFactorOpt = optionBuilder .withShortName("f") .withLongName("scale_factor") .withArgument( argumentBuilder.withName("f").withMinimum(1) .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"); int factor = Integer .parseInt((String) args.get(0)); if (factor <= 0) throw new InvalidArgumentException( new StringBuffer( "The provided scale factor is negative! ") .toString()); if (factor == 1) { LOGGER .warning("The scale factor is 1, program will exit!"); System.exit(0); } } }).create()).withDescription( "integer scale factor") .withRequired(true).create(); numStepsOpt = optionBuilder.withShortName("n") .withLongName("num_steps").withArgument( argumentBuilder.withName("n").withMinimum(1) .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"); int steps = Integer .parseInt((String) args.get(0)); if (steps <= 0) throw new InvalidArgumentException( new StringBuffer( "The provided scale factor is negative! ") .toString()); } }).create()).withDescription( "integer scale factor").withRequired(true).create(); scaleAlgorithmOpt = optionBuilder .withShortName("a") .withLongName("scaling_algorithm") .withArgument( argumentBuilder.withName("a").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"); if (!scalingAlgorithms.contains(args .get(0))) throw new InvalidArgumentException( new StringBuffer( "The output format ") .append(args.get(0)) .append( " is not permitted") .toString()); } }).create()) .withDescription( "name of the scaling algorithm, eeither one of average (a), filtered (f), bilinear (bil), nearest neigbhor (nn)") .withRequired(false).create(); overwriteOpt = optionBuilder.withShortName("w").withLongName( "overwrite").withDescription( "completely wipe out existing layer dirs before proceeding.") .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(compressionTypeOpt); addOption(compressionRatioOpt); addOption(locationOpt); addOption(tileDimOpt); addOption(scaleFactorOpt); addOption(scaleAlgorithmOpt); addOption(numStepsOpt); addOption(overwriteOpt); addOption(internalTileDimOpt); finishInitialization(); } public void run() { // ///////////////////////////////////////////////////////////////////// // // Gather reader to compute tile x and y from tile size // // ///////////////////////////////////////////////////////////////////// final AbstractGridFormat format = (AbstractGridFormat) GridFormatFinder .findFormat(inputLocation); if (format == null) { String message = "Could not find a format for this coverage"; fireException(message, 0, new IOException(message)); return; } final AbstractGridCoverage2DReader inReader = (AbstractGridCoverage2DReader) format .getReader(inputLocation); if (inReader == null) { String message = "Unable to instantiate a reader for this coverage"; fireException(message, 0, new IOException(message)); return; } envelope = inReader.getOriginalEnvelope(); inReader.dispose(); // ///////////////////////////////////////////////////////////////////// // // Create output directory // // ///////////////////////////////////////////////////////////////////// if (!outputLocation.exists()) if (!outputLocation.mkdir()) { String message = "Could not create output directory: " + outputLocation; fireException(message, 0, new IOException(message)); return; } // ///////////////////////////////////////////////////////////////////// // // Compute total steps and set current one so that slave tools progress // event percentages can be corrected to represent the global progress // // ////////////////////////////////////////////////////////////////////// totalSteps = (numSteps + 1) * 2; currStep = 1; // ///////////////////////////////////////////////////////////////////// // // Set up initial level using the coverage tiler // // ///////////////////////////////////////////////////////////////////// File outputDir = new File(outputLocation, "0"); if (!checkLayerDir(outputDir)) return; // create first tiled set resolutions = new double[2][numSteps + 1]; // tileInput(numTileX, numtileY, outputDir); tileInput(outputDir); if (exceptionOccurred) return; currStep++; // mosaic it double[] resolution = mosaicLevel(0); resolutions[0][0] = resolution[0]; resolutions[1][0] = resolution[1]; if (exceptionOccurred) return; currStep++; // ///////////////////////////////////////////////////////////////////// // // Now do create a new level, and mosaic it, up to the final level // // ///////////////////////////////////////////////////////////////////// int currLevel = scaleFactor; int prevLevel = 0; for (int step = 0; step < numSteps; step++) { // check output dir final File prevLevelDirectory = new File(outputLocation, String .valueOf(prevLevel)); final File currLevelDirectory = new File(outputLocation, String .valueOf(currLevel)); if (!checkLayerDir(currLevelDirectory)) return; // create next tiled set buildNewLayer(prevLevelDirectory, currLevelDirectory); if (exceptionOccurred) return; currStep++; // mosaic it resolution = mosaicLevel(currLevel); resolutions[0][step + 1] = resolution[0]; resolutions[1][step + 1] = resolution[1]; if (exceptionOccurred) return; currStep++; // switch to next resolution level prevLevel = currLevel; currLevel *= scaleFactor; } // ///////////////////////////////////////////////////////////////////// // // Finally, build the property file // // ///////////////////////////////////////////////////////////////////// fireEvent("Creating final properties file ", 99.9); createPropertiesFiles(); if (!exceptionOccurred) fireEvent("Done!!!", 100); } private boolean checkLayerDir(File outputDir) { if (!outputDir.exists()) return true; if (!overwriteOutputDirs) { fireException(new IOException("Layer directory " + outputDir + " already exist. Use -w to force its deletion")); return false; } try { FileUtils.deleteDirectory(outputDir); } catch (IOException e) { fireException(e); return false; } return true; } // private void tileInput(final int numTileX, final int numTileY, private void tileInput(File outputDir) { CoverageTiler tiler = new CoverageTiler(); tiler.addProcessingEventListener(slaveToolsListener); tiler.setInputLocation(inputLocation); tiler.setOutputLocation(outputDir); tiler.setTileWidth(tileW); tiler.setTileHeight(tileH); tiler.setInternalTileHeight(internalTileHeight); tiler.setInternalTileWidth(internalTileWidth); tiler.setCompressionRatio(this.compressionRatio); tiler.setCompressionScheme(this.compressionScheme); tiler.run(); tiler.removeAllProcessingEventListeners(); } private void buildNewLayer(File prevLevelDirectory, File currLevelDirectory) { PyramidLayerBuilder layerBuilder = new PyramidLayerBuilder(); layerBuilder.addProcessingEventListener(slaveToolsListener); layerBuilder.setInputLocation(new File(prevLevelDirectory, name + ".shp")); layerBuilder.setOutputLocation(currLevelDirectory); layerBuilder.setScaleAlgorithm(scaleAlgorithm); layerBuilder.setScaleFactor(scaleFactor); layerBuilder.setTileHeight(tileH); layerBuilder.setTileWidth(tileW); layerBuilder.setCompressionRatio(this.compressionRatio); layerBuilder.setCompressionScheme(this.compressionScheme); layerBuilder.run(); layerBuilder.removeAllProcessingEventListeners(); } private double[] mosaicLevel(int level) { MosaicIndexBuilder builder = new MosaicIndexBuilder(); builder.addProcessingEventListener(slaveToolsListener); builder.setLocationPath(new File(outputLocation, String.valueOf(level)) .getAbsolutePath()); builder.setIndexName(name); builder.run(); builder.removeAllProcessingEventListeners(); return new double[] { builder.getResolutionX(), builder.getResolutionY() }; } /** * @param envelope * @param doneSomething */ private void createPropertiesFiles() { // envelope final Properties properties = new Properties(); properties.setProperty("Envelope2D", new StringBuffer(Double .toString(envelope.getMinimum(0))).append(",").append( Double.toString(envelope.getMinimum(1))).append(" ").append( Double.toString(envelope.getMaximum(0))).append(",").append( Double.toString(envelope.getMaximum(1))).toString()); properties.setProperty("LevelsNum", Integer.toString(numSteps + 1)); final StringBuffer levels = new StringBuffer(); final StringBuffer levelDirs = new StringBuffer(); for (int i = 0; i < numSteps + 1; i++) { levels.append(Double.toString(resolutions[0][i])).append(",") .append(Double.toString(resolutions[1][i])); levelDirs.append(i == 0 ? "0" : Integer.toString((int) Math.pow( scaleFactor, i))); if (i < numSteps) { levels.append(" "); levelDirs.append(" "); } } properties.setProperty("Levels", levels.toString()); properties.setProperty("LevelsDirs", levelDirs.toString()); properties.setProperty("Name", name); try { properties.store(new BufferedOutputStream(new FileOutputStream( new File(outputLocation, name + ".properties"))), ""); // // // Creating PRJ file // // final File prjFile = new File(outputLocation, name + ".prj"); BufferedWriter out = new BufferedWriter(new FileWriter(prjFile)); out.write(envelope.getCoordinateReferenceSystem().toWKT()); out.close(); } catch (FileNotFoundException e) { fireException(e); } catch (IOException e) { fireException(e); } } public void getNotification(ProcessingEvent event) { LOGGER.info(new StringBuffer("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()); } public boolean parseArgs(String[] args) { if (!super.parseArgs(args)) return false; // //////////////////////////////////////////////////////////////// // // parsing command line parameters and setting up // Pyramid Builder options // // //////////////////////////////////////////////////////////////// inputLocation = new File((String) getOptionValue(locationOpt)); // output files' directory if (hasOption(outputLocationOpt)) outputLocation = new File( (String) getOptionValue(outputLocationOpt)); else outputLocation = new File(inputLocation.getParentFile(), "pyramid"); // output file name if (hasOption(nameOpt)) name = (String) getOptionValue(nameOpt); else name = "pyramid"; // shall we overwrite the output dirs? overwriteOutputDirs = hasOption(overwriteOpt); // tile dim final String tileDim = (String) getOptionValue(tileDimOpt); String[] pairs = tileDim.split(","); tileW = Integer.parseInt(pairs[0]); tileH = Integer.parseInt(pairs[1]); // // // // scale factor // // // final String scaleF = (String) getOptionValue(scaleFactorOpt); scaleFactor = Integer.parseInt(scaleF); // // // // scaling algorithm (default to nearest neighbour) // // // scaleAlgorithm = (String) getOptionValue(scaleAlgorithmOpt); if (scaleAlgorithm == null) scaleAlgorithm = "nn"; // // // // number of steps // // // numSteps = Integer.parseInt((String) getOptionValue(numStepsOpt)); // // // // 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; } } // // // // 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]); } else { internalTileWidth = tileW; internalTileHeight = tileH; } return true; } public double getCompressionRatio() { return compressionRatio; } public String getCompressionScheme() { return compressionScheme; } public void setCompressionRatio(double compressionRatio) { this.compressionRatio = compressionRatio; } public void setCompressionScheme(String compressionScheme) { this.compressionScheme = compressionScheme; } public int getInternalTileHeight() { return internalTileHeight; } public int getInternalTileWidth() { return internalTileWidth; } public void setInternalTileHeight(int internalTileHeight) { this.internalTileHeight = internalTileHeight; } public void setInternalTileWidth(int internalTileWidth) { this.internalTileWidth = internalTileWidth; } /** * This tool is designed to be used by the command line using this main * class but it can also be used from an GUI by using the setters and * getters. * * @param args * @throws IOException * @throws IllegalArgumentException * @throws InterruptedException */ public static void main(String[] args) throws IllegalArgumentException, IOException, InterruptedException { // creating an overviews embedder final PyramidBuilder builder = new PyramidBuilder(); // adding the embedder itself as a listener builder.addProcessingEventListener(builder); // parsing input argumentBuilder if (builder.parseArgs(args)) { // creating a thread to execute the request process, with the // provided priority final Thread t = new Thread(builder, "PyramidBuilder"); t.setPriority(builder.getPriority()); t.start(); try { t.join(); } catch (InterruptedException e) { LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e); } } else if (LOGGER.isLoggable(Level.FINE)) LOGGER .fine("Unable to parse command line argumentBuilder, exiting..."); } }