/* * 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.image.io.mosaic; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.logging.Level; import javax.imageio.ImageReader; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.ImageGeometry; import org.geotools.geometry.GeneralEnvelope; import org.geotools.math.Fraction; import org.geotools.math.XMath; import org.geotools.referencing.operation.builder.GridToEnvelopeMapper; import org.geotools.resources.XArray; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import org.opengis.geometry.Envelope; import org.opengis.referencing.datum.PixelInCell; /** * A convenience class for building tiles using the same {@linkplain ImageReader image reader} * and organized according some common {@linkplain TileLayout tile layout}. Optionally, this * builder can also write the tiles to disk from an initially untiled image. * * @since 2.5 * * @source $URL$ * @version $Id$ * @author Cédric Briançon * @author Martin Desruisseaux */ public class MosaicBuilder { /** * The default tile size in pixels. */ private static final int DEFAULT_TILE_SIZE = 512; /** * Minimum tile size when using {@link TileLayout#CONSTANT_GEOGRAPHIC_AREA} without * explicit subsamplings provided by user. */ private static final int MIN_TILE_SIZE = 64; /** * The factory to use for creating {@link TileManager} instances. */ protected final TileManagerFactory factory; /** * The desired layout. */ private TileLayout layout; /** * The tile directory, or {@code null} for current directory. * It may be either a relative or absolute path. */ private File directory; /** * The image reader provider. The initial value is {@code null}. * This value must be set before {@link Tile} objects are created. */ private ImageReaderSpi tileReaderSpi; /** * The envelope for the mosaic as a whole, or {@code null} if none. This is optional, but * if specified this builder uses it for assigning values to {@link Tile#getGridToCRS}. */ private GeneralEnvelope mosaicEnvelope; /** * The raster bounding box in pixel coordinates. The initial value is {@code null}. * This value must be set before {@link Tile} objects are created. */ private Rectangle untiledBounds; /** * The desired tile size. The initial value is {@code null}. * This value must be set before {@link Tile} objects are created. */ private Dimension tileSize; /** * The subsamplings to use when creating a new overview. Values at even index are * <var>x</var> subsamplings and values at odd index are <var>y</var> subsamplings. * If {@code null}, subsampling will be computed automatically from the image and * tile size in order to get only entire tiles. */ private int[] subsamplings; /** * The filename formatter. */ private final FilenameFormatter formatter; /** * The logging level for tiling information during reads and writes. */ private Level logLevel = Level.FINE; /** * Generates tiles using the default factory. */ public MosaicBuilder() { this(null); } /** * Generates tiles using the specified factory. * * @param factory The factory to use, or {@code null} for the * {@linkplain TileManagerFactory#DEFAULT default} one. */ public MosaicBuilder(final TileManagerFactory factory) { this.factory = (factory != null) ? factory : TileManagerFactory.DEFAULT; layout = TileLayout.CONSTANT_TILE_SIZE; formatter = new FilenameFormatter(); } /** * Returns the logging level for tile information during read and write operations. * * @return The current logging level. */ public Level getLogLevel() { return logLevel; } /** * Sets the logging level for tile information during read and write operations. * The default value is {@link Level#FINE}. A {@code null} value restore the default. * * @param level The new logging level. */ public void setLogLevel(Level level) { if (level == null) { level = Level.FINE; } logLevel = level; } /** * Returns the tile layout. The default value is * {@link TileLayout#CONSTANT_TILE_SIZE CONSTANT_TILE_SIZE}, which is the most efficient * layout available in {@code org.geotools.image.io.mosaic} implementation. * * @return An identification of current tile layout. */ public TileLayout getTileLayout() { return layout; } /** * Sets the tile layout to the specified value. Valid values are * {@link TileLayout#CONSTANT_TILE_SIZE CONSTANT_TILE_SIZE} and * {@link TileLayout#CONSTANT_GEOGRAPHIC_AREA CONSTANT_GEOGRAPHIC_AREA}. * * @param layout An identification of new tile layout. */ public void setTileLayout(final TileLayout layout) { if (layout != null) { switch (layout) { case CONSTANT_TILE_SIZE: case CONSTANT_GEOGRAPHIC_AREA: { this.layout = layout; return; } } } throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "layout", layout)); } /** * Returns the tile directory, or {@code null} for current directory. The directory * may be either relative or absolute. The default value is {@code null}. * * @return The current tiles directory. */ public File getTileDirectory() { return directory; } /** * Sets the directory where tiles will be read or written. May be a relative or absolute * path, or {@code null} (the default) for current directory. * * @param directory The new tiles directory. */ public void setTileDirectory(final File directory) { this.directory = directory; } /** * Returns the {@linkplain ImageReader image reader} provider to use for reading tiles. * The initial value is {@code null}, which means that the provider should be the same * than the one detected by {@link #writeFromUntiledImage writeFromUntiledImage}. * * @return The current image reader provider for tiles. */ public ImageReaderSpi getTileReaderSpi() { return tileReaderSpi; } /** * Sets the {@linkplain ImageReader image reader} provider for each tiles to be read. * A {@code null} value means that the provider should be automatically detected by * {@link #writeFromUntiledImage writeFromUntiledImage}. * * @param provider The new image reader provider for tiles. */ public void setTileReaderSpi(final ImageReaderSpi provider) { this.tileReaderSpi = provider; } /** * Sets the {@linkplain ImageReader image reader} provider by name. This convenience method * searchs a provider for the given name in the default {@link IIORegistry} and delegates to * {@link #setTileReaderSpi(ImageReaderSpi)}. * * @param format The image format name for tiles. * @throws IllegalArgumentException if no provider was found for the given name. */ public void setTileReaderSpi(String format) throws IllegalArgumentException { ImageReaderSpi spi = null; if (format != null) { format = format.trim(); final IIORegistry registry = IIORegistry.getDefaultInstance(); final Iterator<ImageReaderSpi> it=registry.getServiceProviders(ImageReaderSpi.class, true); do { if (!it.hasNext()) { throw new IllegalArgumentException(Errors.format( ErrorKeys.UNKNOW_IMAGE_FORMAT_$1, format)); } spi = it.next(); } while (!XArray.contains(spi.getFormatNames(), format)); } setTileReaderSpi(spi); } /** * Returns the envelope for the mosaic as a whole, or {@code null} if none. This is optional, * but if specified this builder uses it for assigning values to {@link Tile#getGridToCRS}. * * @return The current envelope, or {@code null} if none. */ public Envelope getMosaicEnvelope() { return (mosaicEnvelope != null) ? mosaicEnvelope.clone() : null; } /** * Sets the envelope for the mosaic as a whole, or {@code null} if none. This is optional, * but if specified this builder uses it for assigning values to {@link Tile#getGridToCRS}. * <p> * This is merely a convenient way to invoke {@link TileManager#setGridToCRS} with a transform * computed from the envelope and the {@linkplain #getUntiledImageBounds untiled image bounds}, * where the later may be known only at reading time. As always, creating "grid to CRS" from an * envelope is ambiguous, since we don't know if axis need to be interchanged, <var>y</var> axis * flipped, <cite>etc.</cite> Subclasses can gain more control by overriding the * {@link #createGridToEnvelopeMapper createGridToEnvelopeMapper} method. The default behavior * fits most typical cases however. * * @param envelope The new envelope, or {@code null} if none. * * @see #createGridToEnvelopeMapper */ public void setMosaicEnvelope(final Envelope envelope) { mosaicEnvelope = (envelope != null) ? new GeneralEnvelope(envelope) : null; } /** * Returns the bounds of the untiled image, or {@code null} if not set. In the later case, the * bounds will be inferred from the input image when {@link #writeFromUntiledImage} is invoked. * * @return The current untiled image bounds. */ public Rectangle getUntiledImageBounds() { return (untiledBounds != null) ? (Rectangle) untiledBounds.clone() : null; } /** * Sets the bounds of the untiled image to the specified value. * A {@code null} value discarts any value previously set. * * @param bounds The new untiled image bounds. */ public void setUntiledImageBounds(final Rectangle bounds) { untiledBounds = (bounds != null) ? new Rectangle(bounds) : null; } /** * Returns the tile size. If no tile size has been explicitly set, then a default tile size * will be computed from the {@linkplain #getUntiledImageBounds untiled image bounds}. If no * size can be computed, then this method returns {@code null}. * * @return The current tile size. * * @see #suggestedTileSize */ public Dimension getTileSize() { if (tileSize == null) { final Rectangle untiledBounds = getUntiledImageBounds(); if (untiledBounds == null) { return null; } int width = untiledBounds.width; int height = untiledBounds.height; width = suggestedTileSize(width); height = (height == untiledBounds.width) ? width : suggestedTileSize(height); tileSize = new Dimension(width, height); } return (Dimension) tileSize.clone(); } /** * Sets the tile size. A {@code null} value discarts any value previously set. * * @param size The new tile size. */ public void setTileSize(final Dimension size) { if (size == null) { tileSize = null; } else { if (size.width < 2 || size.height < 2) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$1, "size")); } tileSize = new Dimension(size); } } /** * Returns the suggested tile size using default values. */ private static int suggestedTileSize(final int imageSize) { return suggestedTileSize(imageSize, DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE - DEFAULT_TILE_SIZE/4, DEFAULT_TILE_SIZE + DEFAULT_TILE_SIZE/4); } /** * Returns a suggested tile size ({@linkplain Dimension#width width} or * {@linkplain Dimension#height height}) for the given image size. This * method searchs for a value <var>x</var> inside the {@code [minSize...maxSize]} * range where {@code imageSize}/<var>x</var> has the largest amount of * {@linkplain XMath#divisors divisors}. If more than one value have the same amount * of divisors, then the one which is the closest to {@code tileSize} is returned. * * @param imageSize The image size. * @param tileSize The preferred tile size. Must be inside the {@code [minSize...maxSize]} range. * @param minSize The minimum size, inclusive. Must be greater than 0. * @param maxSize The maximum size, inclusive. Must be equals or greater that {@code minSize}. * @return The suggested tile size. Inside the {@code [minSize...maxSize]} range except * if {@code imageSize} was smaller than {@link minSize}. * @throws IllegalArgumentException if any argument doesn't meet the above-cited conditions. */ public static int suggestedTileSize(final int imageSize, final int tileSize, final int minSize, final int maxSize) throws IllegalArgumentException { if (minSize <= 1 || minSize > maxSize) { throw new IllegalArgumentException(Errors.format( ErrorKeys.BAD_RANGE_$2, minSize, maxSize)); } if (tileSize < minSize || tileSize > maxSize) { throw new IllegalArgumentException(Errors.format( ErrorKeys.VALUE_OUT_OF_BOUNDS_$3, tileSize, minSize, maxSize)); } if (imageSize <= minSize) { return imageSize; } int numDivisors = 0; int best = tileSize; for (int i=minSize; i<=maxSize; i++) { if (imageSize % i != 0) { continue; } // Note: Fraction rounding mode must be the same than in getSubsamplings(). final int n = XMath.divisors(Fraction.round(imageSize, i)).length; if (n < numDivisors) { continue; } if (n == numDivisors) { if (Math.abs(i - tileSize) >= Math.abs(best - tileSize)) { continue; } } best = i; numDivisors = n; } return best; } /** * Returns a suggested set of divisors of the number of tiles that can fit in an image. * More specifically, this method executes the following pseudo code twice, once for * {@linkplain Dimension#width width} and once for {@linkplain Dimension#height height}, * resulting in two arrays of type {@code int[]}: * * <blockquote><code> * {@linkplain XMath#divisors divisors}({@linkplain Fraction#round round}(imageBounds / tileSize)) * </code></blockquote> * * The two arrays are then decimated using the following procedures: * <p> * <ul> * <li>If {@code multiples} is {@code true}, then the arrays are decimated in such a way * that each value {@code divisors[i]} is a multiple of {@code divisors[i-1]}. This * is useful for getting only tiles that can fit entirely in bigger tiles.</li> * <li>The largest of the two arrays is trimmed in order to get arrays of the same * length.</li> * <p> * If the number of divisors is lower than the {@code preferredCount}, then this method will * try again for smaller tiles until the preferred count or some other (currently undocumented) * stop condition is reached. * <p> * Let {@code r} be the return value. The following contract should hold in all cases: * <p> * <ul> * <li>{@code r} is never {@code null}</li> * <li>{@code r.length} is always 2</li> * <li>{@code r[0].length} == {@code r[1].length} and this length is greater than 0</li> * <li>{@code r[0][0]} == {@code r[1][0]} == 1 (i.e. the first divisor is always 1)</li> * </ul> * * @param imageBounds The image size. * @param tileSize The tile size. * @param preferredCount The preferred minimal amount of divisors, or 0 if there is no * minimal count. * @param multiples If {@code true}, then the returned numbers are restricted to multiples. * @return Two arrays of the same length, which are respectively the divisors of * image {@linkplain Rectangle#width width} and the divisors of image * {@linkplain Rectangle#height height}. */ public static int[][] suggestedNumTiles(final Rectangle imageBounds, final Dimension tileSize, final int preferredCount, final boolean multiples) { final int width = tileSize.width; final int height = tileSize.height; final int[][] divisors = new int[2][]; final int xmax = imageBounds.width / width; final int ymax = imageBounds.height / height; final int maxScale = Math.min(tileSize.width, tileSize.height) / MIN_TILE_SIZE; int scale = 1; boolean oldTileDivideImage = false; do { final int[] oldX = divisors[0]; final int[] oldY = divisors[1]; final long dx = scale * (long) imageBounds.width; final long dy = scale * (long) imageBounds.height; final int nx = (int) Fraction.round(dx, width); final int ny = (int) Fraction.round(dy, height); final boolean tileDivideImage = (nx * width == dx) && (ny * height == dy); if (oldTileDivideImage && !tileDivideImage) { continue; // Doesn't worth to continue for this scale. } int[] sx = XMath.divisors(nx); int[] sy; if (nx == ny) { sx = XArray.resize(sx, decimate(sx, Math.min(xmax, ymax), multiples)); divisors[0] = divisors[1] = sy = sx; } else { sy = XMath.divisors(ny); divisors[0] = sx = XArray.resize(sx, decimate(sx, xmax, multiples)); divisors[1] = sy = XArray.resize(sy, decimate(sy, ymax, multiples)); reduceLargest(divisors); } final int length = divisors[0].length; if (tileDivideImage && length >= preferredCount) { // In addition of the preferred count, we also favorise the results // computed from a tile size which is a divisor of the image bounds. break; } if (oldX != null) { if (tileDivideImage && !oldTileDivideImage) { oldTileDivideImage = true; continue; // Keep the new divisors. } if (length <= oldX.length) { divisors[0] = oldX; divisors[1] = oldY; } } } while (++scale <= maxScale); return divisors; } /** * Decimates the given array in-place so that each value {@code divisors[i]} is a multiple * of {@code divisors[i-1]}. Value greater than {@code maximum} are trimmed. * * @param divisors The array to decimate in-place. * @param maximum The maximal value to keep, inclusive. * @param multiples If {@code false}, disables the restriction to multiples. * @return The number of valid values in the given array. */ private static int decimate(final int[] divisors, final int maximum, final boolean multiples) { assert XArray.isStrictlySorted(divisors); int n; if (!multiples) { n = Arrays.binarySearch(divisors, maximum); if (n < 0) { n = (~n) - 1; } } else { n = 0; for (int i=1; i<divisors.length; i++) { final int x = divisors[i]; if (x > maximum) { break; } if ((x % divisors[n]) == 0) { divisors[++n] = x; } } } return ++n; } /** * Given two arrays which may be of different length, remove some elements from the largest * array in order to get the same length than the shortest array. Arrays most be sorted in * strictly increasing order. * * @return The divisors of image {@linkplain Rectangle#width width} and the divisors of image * {@linkplain Rectangle#height height}. The largest array will be replaced by a new * one. */ private static void reduceLargest(final int[][] divisors) { int[] large = divisors[0]; int[] small = divisors[1]; assert XArray.isStrictlySorted(large) && XArray.isStrictlySorted(small); if (large.length == small.length) { return; } final int target; if (large.length >= small.length) { target = 0; } else { target = 1; final int[] tmp = large; large = small; small = tmp; } final int[] reduced = new int[small.length]; for (int i=0; i<small.length; i++) { int value = small[i]; int k = Arrays.binarySearch(large, value); if (k < 0) { k = ~k; // Really tilde, not minus operator. if (k != 0 && (k == large.length || (large[k] - value) >= (value - large[k-1]))) { k--; } value = large[k]; } reduced[i] = value; } divisors[target] = reduced; } /** * Returns the subsampling for overview computations. If no subsamplings were {@linkplain * #setSubsamplings(Dimension[]) explicitly set}, then this method computes automatically * some subsamplings from the {@linkplain #getUntiledImageBounds untiled image bounds} and * {@linkplain #getTileSize tile size}, with the following properties (note that those * properties are not garanteed if the subsampling was explicitly specified rather than * computed): * <p> * <ul> * <li>The first element in the returned array is (1,1).</li> * <li>Elements are sorted by increasing subsampling values.</li> * <li>At most one subsampling (the last one) results in an image big enough for holding * the whole mosaic.</li> * </ul> * <p> * If no subsampling can be computed, then this method returns {@code null}. * * @return The current subsamplings for each overview levels. */ public Dimension[] getSubsamplings() { if (subsamplings == null) { final Rectangle untiledBounds = getUntiledImageBounds(); if (untiledBounds == null) { return null; } final Dimension tileSize = getTileSize(); if (tileSize == null) { return null; } /* * If the tile layout is CONSTANT_GEOGRAPHIC_AREA, increasing the subsampling will have * the effect of reducing the tile size by the same amount, so we are better to choose * subsamplings that are divisors of the tile size. * * If the tile layout is CONSTANT_TILE_SIZE, increasing the subsampling will have the * effect of reducing the number of tiles required for covering the whole image. So we * are better to choose subsamplings that are divisors of the number of tiles. If the * number of tiles are not integers, we round towards nearest integers in the hope that * we get a number closer to user's intend. * * If the tile layout is unknown, we don't really know what to choose. We fallback on * some values that seem reasonable, but our fallback may change in future version. * It doesn't hurt any code in this module - the only consequence is that tiling may * be suboptimal. */ final boolean constantArea = TileLayout.CONSTANT_GEOGRAPHIC_AREA.equals(layout); int nx = tileSize.width; int ny = tileSize.height; if (!constantArea) { // Must performs the division in the same way than in suggestedTileSize(...). nx = Fraction.round(untiledBounds.width, nx); ny = Fraction.round(untiledBounds.height, ny); } int[] xSubsamplings = XMath.divisors(nx); if (nx != ny) { /* * Subsamplings are different along x and y axis. We need at least arrays of the * same length. While not strictly required, it is better that xSubsampling and * ySubsampling are equal, assuming that pixels are square (otherwise we could * multiply by a height/width ratio; it may be done in a future version). Current * implementation keep the union of divisors. * * TODO: move the code that computes the union in XArray.union(int[],int[]). */ final int[] ySubsamplings = XMath.divisors(ny); final int[] union = new int[xSubsamplings.length + ySubsamplings.length]; int nu=0; for (int ix=0, iy=0;;) { if (ix == xSubsamplings.length) { final int no = ySubsamplings.length - iy; System.arraycopy(ySubsamplings, iy, union, nu, no); nu += no; break; } if (iy == ySubsamplings.length) { final int no = xSubsamplings.length - ix; System.arraycopy(xSubsamplings, ix, union, nu, no); nu += no; break; } final int sx = xSubsamplings[ix]; final int sy = ySubsamplings[iy]; final int s; if (sx <= sy) { s = sx; ix++; if (sx == sy) { iy++; } } else { s = sy; iy++; } union[nu++] = s; } xSubsamplings = XArray.resize(union, nu); } /* * Trims the subsamplings which would produce tiles smaller than the minimum size * (for CONSTANT_GEOGRAPHIC_AREA layout) or which would produce more than one tile * enclosing the whole image (for CONSTANT_TILE_SIZE layout). First, we calculate * as (nx,ny) the maximum subsamplings expected (inclusive). Then we search those * maximum in the actual subsampling and assign to (nx,ny) the new array length. */ if (constantArea) { nx = tileSize.width / MIN_TILE_SIZE; ny = tileSize.height / MIN_TILE_SIZE; } else { nx = (untiledBounds.width - 1) / tileSize.width + 1; ny = (untiledBounds.height - 1) / tileSize.height + 1; } // Increments (++) below are inconditional (outside the "if" block). nx = Arrays.binarySearch(xSubsamplings, nx); if (nx < 0) nx = ~nx; nx++; ny = Arrays.binarySearch(xSubsamplings, ny); if (ny < 0) ny = ~ny; ny++; final int length = Math.min(Math.max(nx, ny), xSubsamplings.length); subsamplings = new int[length * 2]; int source = 0; for (int i=0; i<length; i++) { subsamplings[source++] = xSubsamplings[i]; subsamplings[source++] = xSubsamplings[i]; } } final Dimension[] dimensions = new Dimension[subsamplings.length / 2]; int source = 0; for (int i=0; i<dimensions.length; i++) { dimensions[i] = new Dimension(subsamplings[source++], subsamplings[source++]); } return dimensions; } /** * Sets the subsamplings for overview computations. The number of overview levels created * by this {@code MosaicBuilder} will be equals to the {@code subsamplings} array length. * <p> * Subsamplings most be explicitly provided for {@link TileLayout#CONSTANT_GEOGRAPHIC_AREA}, * but is optional for {@link TileLayout#CONSTANT_TILE_SIZE}. In the later case subsamplings * may be {@code null} (the default), in which case they will be automatically computed from * the {@linkplain #getUntiledImageBounds untiled image bounds} and {@linkplain #getTileSize * tile size} in order to have only entire tiles (i.e. tiles in last columns and last rows * don't need to be cropped). * * @param subsamplings The new subsamplings for each overview levels. */ public void setSubsamplings(final Dimension[] subsamplings) { final int[] newSubsamplings; if (subsamplings == null) { newSubsamplings = null; } else { int target = 0; newSubsamplings = new int[subsamplings.length * 2]; for (int i=0; i<subsamplings.length; i++) { final Dimension subsampling = subsamplings[i]; final int xSubsampling = subsampling.width; final int ySubsampling = subsampling.height; if (xSubsampling < 1 || ySubsampling < 1) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$1, "subsamplings[" + i + ']')); } newSubsamplings[target++] = xSubsampling; newSubsamplings[target++] = ySubsampling; } } this.subsamplings = newSubsamplings; } /** * Sets uniform subsamplings for overview computations. This convenience method delegates to * {@link #setSubsamplings(Dimension[])} with the same value affected to both * {@linkplain Dimension#width width} and {@linkplain Dimension#height height}. * * @param subsamplings The new subsamplings for each overview levels. */ public void setSubsamplings(final int[] subsamplings) { final Dimension[] newSubsamplings; if (subsamplings == null) { newSubsamplings = null; } else { newSubsamplings = new Dimension[subsamplings.length]; for (int i=0; i<subsamplings.length; i++) { final int subsampling = subsamplings[i]; newSubsamplings[i] = new Dimension(subsampling, subsampling); } } // Delegates to setSubsamplings(Dimension[]) instead of performing the same work in-place // (which would have been more efficient) because the user may have overriden the former. setSubsamplings(newSubsamplings); } /** * Creates a tile manager from the informations supplied in above setters. * The following methods must be invoked prior this one: * <p> * <ul> * <li>{@link #setUntiledImageBounds}</li> * <li>{@link #setTileReaderSpi}</li> * </ul> * <p> * The other setter methods are optional. * * @return The tile manager created from the information returned by getter methods. * @throws IOException if an I/O operation was required and failed. The default implementation * does not perform any I/O, but subclasses are allowed to do so. */ public TileManager createTileManager() throws IOException { return createFromInput(null); } /** * Implementation of {@link #createTileManager()} with a given input. This method is not * public because it expects an argument controlling the behavior of tile writting, while * this method actually does not write anything to disk. The policy is used in order to * determine whatever this method should skip empty tiles or not. Skipping empty tiles are * usually performed when reading the original untiled image, because we know only at that * time which tiles are going to contain non-zero pixels. However it is possible to skip the * tiles that do not intersect any input tile. This is incomplete since some of the remaining * tiles may need to be skipped as well (we will do that later, during the write process), * but doing this early pre-filtering here can improve a lot the performance and memory usage. * * @param input * The tile manager for the input tiles, or {@code null} if none. If non-null, this is * used only in order to filter the output tiles to the ones that intersect the input * tiles. This value should be {@code null} if no such filtering should be applied. * @return The tile manager created from the information returned by getter methods. * @throws IOException if an I/O operation was required and failed. */ @SuppressWarnings("fallthrough") private TileManager createFromInput(final TileManager input) throws IOException { tileReaderSpi = getTileReaderSpi(); if (tileReaderSpi == null) { // TODO: We may try to detect automatically the Spi in a future version. throw new IllegalStateException(Errors.format(ErrorKeys.NO_IMAGE_READER)); } untiledBounds = getUntiledImageBounds(); // Forces computation, if any. if (untiledBounds == null) { throw new IllegalStateException(Errors.format(ErrorKeys.UNSPECIFIED_IMAGE_SIZE)); } tileSize = getTileSize(); // Forces computation if (tileSize == null) { tileSize = ImageUtilities.toTileSize(untiledBounds.getSize()); } formatter.initialize(tileReaderSpi); final TileManager output; /* * Delegates to a method using an algorithm appropriate for the requested layout. */ boolean constantArea = false; switch (layout) { case CONSTANT_GEOGRAPHIC_AREA: { constantArea = true; // Fall through } case CONSTANT_TILE_SIZE: { output = createFromInput(constantArea, canUsePattern(), input); break; } default: { throw new IllegalStateException(layout.toString()); } } /* * After TileManager creation, computes the "grid to CRS" transform * if an envelope was given to this builder. */ if (mosaicEnvelope != null && !mosaicEnvelope.isNull()) { final GridToEnvelopeMapper mapper = createGridToEnvelopeMapper(output); mapper.setGridRange(new GridEnvelope2D(untiledBounds)); mapper.setEnvelope(mosaicEnvelope); output.setGridToCRS((AffineTransform) mapper.createTransform()); } return output; } /** * Creates tiles for the following cases: * <ul> * <li>covering a constant geographic region. The tile size will reduce as we progress into * overviews levels. The {@link #minimumTileSize} value is the stop condition - no smaller * tiles will be created.</li> * <li>tiles of constant size in pixels. The stop condition is when a single tile cover * the whole image.</li> * </ul> * * @param constantArea * {@code true} for constant area layout, or {@code false} for constant * tile size layout. * @param usePattern * {@code true} for creating tiles using a pattern instead of creating * individual instance of every tiles. * @param input * The tile manager for the input tiles, or {@code null} if none. If non-null, this is * used only in order to filter the output tiles to the ones that intersect the input * tiles. This value should be {@code null} if no such filtering should be applied. * @return The tile manager. * @throws IOException if an I/O operation was requested and failed. */ private TileManager createFromInput(final boolean constantArea, final boolean usePattern, final TileManager input) throws IOException { final Dimension tileSize = this.tileSize; // Paranoiac compile-time safety against final Rectangle untiledBounds = this.untiledBounds; // unwanted reference assignments. final Rectangle imageBounds = new Rectangle(untiledBounds); final Rectangle tileBounds = new Rectangle(tileSize); Dimension[] subsamplings = getSubsamplings(); if (subsamplings == null) { final int n; if (constantArea) { n = Math.max(tileBounds.width, tileBounds.height) / MIN_TILE_SIZE; } else { n = Math.max(imageBounds.width / tileBounds.width, imageBounds.height / tileBounds.height); } subsamplings = new Dimension[n]; for (int i=1; i<=n; i++) { subsamplings[i-1] = new Dimension(i,i); } } final List<Tile> tiles; final OverviewLevel[] levels; if (usePattern) { tiles = null; levels = new OverviewLevel[subsamplings.length]; } else { tiles = new ArrayList<Tile>(); levels = null; } final Rectangle absoluteBounds = new Rectangle(); /* * For each overview level, computes the size of tiles and the size of the mosaic as * a whole. The 'tileBounds' and 'imageBounds' rectangles are overwritten during each * iteration. The filename formatter is configured according the expected number of * tiles computed from the bounds. */ formatter.computeLevelFieldSize(subsamplings.length); for (int level=0; level<subsamplings.length; level++) { final Dimension subsampling = subsamplings[level]; final int xSubsampling = subsampling.width; final int ySubsampling = subsampling.height; imageBounds.setBounds(untiledBounds.x / xSubsampling, untiledBounds.y / ySubsampling, untiledBounds.width / xSubsampling, untiledBounds.height / ySubsampling); tileBounds.setBounds(imageBounds); tileBounds.setSize(tileSize); if (constantArea) { tileBounds.width /= xSubsampling; tileBounds.height /= ySubsampling; } else { if (tileBounds.width > imageBounds.width) tileBounds.width = imageBounds.width; if (tileBounds.height > imageBounds.height) tileBounds.height = imageBounds.height; } formatter.computeFieldSizes(imageBounds, tileBounds); /* * If we are allowed to use a pattern, create directly the pattern string. * Example of pattern: "File:directory/L{level:1}_{column:2}{row:2}.png". * It will take much less memory than creating every individual tiles, but * is possible only if the user didn't customized too much the tiles creation. */ if (usePattern) { String pattern = formatter.toString(); pattern = new File(directory, pattern).getPath(); pattern = "File:" + pattern; final Tile tile = new Tile(tileReaderSpi, pattern, 0, tileBounds, subsampling); final OverviewLevel ol = new OverviewLevel(tile, imageBounds); ol.createLinkedList(level, (level != 0) ? levels[level - 1] : null); if (input != null) { final int nx = ol.getNumXTiles(); final int ny = ol.getNumYTiles(); absoluteBounds.width = xSubsampling * tileBounds.width; absoluteBounds.height = ySubsampling * tileBounds.height; absoluteBounds.y = ySubsampling * tileBounds.y; for (int y=0; y<ny; y++) { absoluteBounds.x = xSubsampling * tileBounds.x; for (int x=0; x<nx; x++) { if (!input.intersects(absoluteBounds, subsampling)) { ol.removeTile(x, y); } absoluteBounds.x += absoluteBounds.width; } absoluteBounds.y += absoluteBounds.height; } } levels[level] = ol; } else { /* * If we are not allowed to use a pattern, enumerate every tiles individually. * We will let TileManagerFactory tries to figure out a layout from them. Note * that the factory may create a GridTileManager instance anyway, but the later * will typically be more customized than the one created in the 'usePattern' case. */ final int xmin = imageBounds.x; final int ymin = imageBounds.y; final int xmax = imageBounds.width + xmin; final int ymax = imageBounds.height + ymin; final int dx = tileBounds.width; final int dy = tileBounds.height; absoluteBounds.width = xSubsampling * dx; absoluteBounds.height = ySubsampling * dy; int y = 0; for (tileBounds.y = ymin; tileBounds.y < ymax; tileBounds.y += dy, y++) { int x = 0; absoluteBounds.y = ySubsampling * tileBounds.y; for (tileBounds.x = xmin; tileBounds.x < xmax; tileBounds.x += dx, x++) { if (input != null) { absoluteBounds.x = xSubsampling * tileBounds.x; if (!input.intersects(absoluteBounds, subsampling)) { continue; } } Rectangle clippedBounds = tileBounds.intersection(imageBounds); File file = new File(directory, generateFilename(level, x, y)); Tile tile = new Tile(tileReaderSpi, file, 0, clippedBounds, subsampling); tiles.add(tile); } } } } /* * Creates the tile manager. If assertions are enabled, the manager created using * patterns will be compared to the manager created by enumerating every tiles. */ final TileManager manager; if (usePattern) { manager = new GridTileManager(levels[levels.length - 1]); /* * Following assertion creates a new TileManager by enumerating every tiles * (instead than using the pattern) and makes sure that we get the same set * of tiles. The later comparaison is trigged by the call to getTiles(). */ assert !(new ComparedTileManager(manager, createFromInput(constantArea, false, input)).getTiles().isEmpty()); } else { final TileManager[] managers = factory.create(tiles); manager = managers[0]; } return manager; } /** * The mosaic image writer to be used by {@link MosaicBuilder#writeFromUntiledImage}. */ private final class Writer extends MosaicImageWriter { /** * The tile writing policy. Should be identical to the value given to * {@link MosaicImageWriteParam#setTileWritingPolicy}. */ private final TileWritingPolicy policy; /** * Index of the untiled image to read. */ private final int inputIndex; /** * The input tile managers, or {@code null} if none. */ TileManager[] inputTiles; /** * The tiles created by {@link MosaicBuilder#createTileManager}. * Will be set by {@link #filter} and read by {@link MosaicBuilder}. */ TileManager outputTiles; /** * Creates a writer for an untiled image to be read at the given index. */ Writer(final int inputIndex, final TileWritingPolicy policy) { this.inputIndex = inputIndex; this.policy = policy; } /** * Creates the tiles for the specified untiled images. */ @Override protected boolean filter(ImageReader reader) throws IOException { final Rectangle bounds = new Rectangle(); bounds.width = reader.getWidth (inputIndex); bounds.height = reader.getHeight(inputIndex); TileManager input = null; // Sets only after successful reading of image size. if (reader instanceof MosaicImageReader) { final MosaicImageReader mosaic = (MosaicImageReader) reader; inputTiles = mosaic.getInput(); // Should not be null as of filter(...) contract. if (inputTiles.length > inputIndex && (policy != null && !policy.includeEmpty)) { input = inputTiles[inputIndex]; } reader = mosaic.getTileReader(); } if (reader != null) { // May be null as a result of above line. final ImageReaderSpi spi = reader.getOriginatingProvider(); if (spi != null && getTileReaderSpi() == null) { setTileReaderSpi(spi); } } setUntiledImageBounds(bounds); outputTiles = createFromInput(input); try { setOutput(outputTiles); } catch (IllegalArgumentException exception) { final Throwable cause = exception.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } throw exception; } return true; } /** * Invoked when a tile is about to be written. Delegates to a method that users can * override. */ @Override protected void onTileWrite(Tile tile, ImageWriteParam parameters) throws IOException { MosaicBuilder.this.onTileWrite(tile, parameters); } } /** * Creates a tile manager from an untiled image. The {@linkplain #getUntiledImageBounds * untiled image bounds} and {@linkplain #getTileReaderSpi tile reader SPI} are inferred * from the input, unless they were explicitly specified. * <p> * This method does not write any tile to disk. * * @param input The image input, typically as a {@link File} or an other {@link TileManager}. * @return The tiles, or {@code null} if the process has been aborted. * @throws IOException if an error occured while reading the untiled image. */ public TileManager createTileManager(final Object input) throws IOException { return createTileManager(input, 0, TileWritingPolicy.NO_WRITE); } /** * Creates a tile manager from an untiled image. The {@linkplain #getUntiledImageBounds * untiled image bounds} and {@linkplain #getTileReaderSpi tile reader SPI} are inferred * from the input, unless they were explicitly specified. * <p> * Optionnaly if the tile writing policy is anything else than * {@link TileWritingPolicy#NO_WRITE NO_WRITE}, then pixel values are read from the untiled * images, organized in tiles as specified by the {@link TileManager} to be returned and saved * to disk. This work is done using a default {@link MosaicImageWriter}. * * @param input The image input, typically as a {@link File} or an other {@link TileManager}. * @param inputIndex Index of image to read, typically 0. * @param policy Sets whatever tiles are created and saved to disk. * @return The tiles, or {@code null} if the process has been aborted while writing tiles. * @throws IOException if an error occured while reading the untiled image or (only if * {@code writeTiles} is {@code true}) while writting the tiles to disk. */ public TileManager createTileManager(final Object input, final int inputIndex, final TileWritingPolicy policy) throws IOException { formatter.ensurePrefixSet(input); final Writer writer = new Writer(inputIndex, policy); writer.setLogLevel(getLogLevel()); final MosaicImageWriteParam param = writer.getDefaultWriteParam(); param.setTileWritingPolicy(policy); try { if (!writer.writeFromInput(input, inputIndex, param)) { return null; } } finally { writer.dispose(); } TileManager tiles = writer.outputTiles; /* * Before to return the tile manager, if no geometry has been inferred from the target * tiles (typically because no setEnvelope(...) has not been invoked), then inherit the * geometry from the source tile, if there is any. This operation is conservative and * performed only on a "best effort" basis. */ if (tiles.geometry == null) { if (writer.inputTiles != null) { for (final TileManager candidate : writer.inputTiles) { final ImageGeometry geometry = candidate.getGridGeometry(); if (geometry != null) { tiles.setGridToCRS(geometry.getGridToCRS()); break; } } } } return tiles; } /** * Returns {@code true} if we can create {@link TileManager} using a regular pattern instead * than enumerating every tiles. This method returns {@code true} if {@link #generateFilename} * has not be overriden, otherwise we can't guess at this stage the pattern that the user is * applying. */ private boolean canUsePattern() { final Class<?>[] parameters = new Class[3]; Arrays.fill(parameters, Integer.TYPE); Class<?> classe = getClass(); Method method; do try { method = classe.getDeclaredMethod("generateFilename", parameters); return method.getDeclaringClass().equals(MosaicBuilder.class); } catch (NoSuchMethodException e) { classe = classe.getSuperclass(); } while (classe != null); // Would be a programming error. The method we are looking for is just below. throw new AssertionError(); } /** * Generates a filename for the current tile based on the position of this tile in the raster. * For example, a tile in the first overview level, which is localized on the 5th column and * 2nd row may have a name like "{@code L1_E2.png}". * <p> * Subclasses may override this method if they want more control on generated tile filenames. * * @param level The level of overview. First level is 0. * @param column The index of columns. First column is 0. * @param row The index of rows. First row is 0. * @return A filename based on the position of the tile in the whole raster. */ protected String generateFilename(final int level, final int column, final int row) { return formatter.generateFilename(level, column, row); } /** * Invoked automatically when a "<cite>grid to CRS</cite>" transform needs to be computed. The * default implementation returns a new {@link GridToEnvelopeMapper} instance in its default * configuration, except for the {@linkplain GridToEnvelopeMapper#setPixelAnchor pixel anchor} * which is set to {@link PixelInCell#CELL_CORNER CELL_CORNER} (OGC specification maps pixel * center, while Java I/O maps pixel upper-left corner). * <p> * Subclasses may override this method in order to configure the mapper in an other way. * * @param tiles The tiles for which a "<cite>grid to CRS</cite>" transform needs to be computed. * @return An "grid to envelope" mapper having the desired configuration. * * @see #setMosaicEnvelope */ protected GridToEnvelopeMapper createGridToEnvelopeMapper(final TileManager tiles) { final GridToEnvelopeMapper mapper = new GridToEnvelopeMapper(); mapper.setPixelAnchor(PixelInCell.CELL_CORNER); return mapper; } /** * Invoked automatically when a tile is about to be written. The default implementation does * nothing. Subclasses can override this method in order to set custom write parameters. The * {@linkplain ImageWriteParam#setSourceRegion source region} and * {@linkplain ImageWriteParam#setSourceSubsampling source subsampling} should not be set * since they will be inconditionnaly overwritten by the caller. * * @param tile The tile to be written. * @param parameters The parameters to be given to the {@linkplain ImageWriter image writer}. * @throws IOException if an I/O operation was required and failed. */ protected void onTileWrite(Tile tile, ImageWriteParam parameters) throws IOException { } }