/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.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.nio.file.Path;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.logging.Level;
import java.lang.reflect.Method;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.ImageWriteParam;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.math.Fraction;
import org.geotoolkit.lang.Builder;
import org.geotoolkit.util.logging.LogProducer;
import org.apache.sis.util.logging.PerformanceLevel;
import org.apache.sis.util.collection.BackingStoreException;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.coverage.grid.ImageGeometry;
import org.geotoolkit.image.io.plugin.WorldFileImageReader;
import org.geotoolkit.internal.image.io.Formats;
import static org.apache.sis.util.ArgumentChecks.ensureBetween;
import org.geotoolkit.image.internal.ImageUtilities;
import org.geotoolkit.image.palette.IIOListeners;
/**
* Creates {@link TileManager} from a set of images organized according a given
* {@linkplain TileLayout tile layout}. This class can work with pre-existing
* tile files (in which case it just build a {@code TileManager}), or can write
* the tiles to the disk if the files do not already exist.
*
* {@section Example creating tiles to disk}
* For example in order to create a mosaic for a set of tiles of size 256×256 pixels,
* with overviews having pixels 2, 3 and 4 times the width and height of original pixels and
* for writing the tiles in the {@code "output"} directory, use the following:
*
* {@preformat java
* Object originalMosaic = ...; // May be a File, URL, list of Tiles, etc.
* MosaicBuilder builder = new MosaicBuilder();
* builder.setTileDirectory(new File("output"));
* builder.setTileSize(new Dimension(256, 256));
* builder.setSubsamplings(1, 2, 3, 4);
* TileManager newMosaic = builder.writeFromInput(originalMosaic, null);
* }
*
* @author Martin Desruisseaux (Geomatys)
* @author Cédric Briançon (Geomatys)
* @version 3.17
*
* @see org.geotoolkit.gui.swing.image.MosaicBuilderEditor
*
* @since 2.5
* @module
*/
public class MosaicBuilder extends Builder<TileManager> implements LogProducer {
/**
* 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 Path 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;
/**
* An optional <cite>grid to CRS</cite> transform to be used for assigned value
* to {@link Tile#getGridToCRS()}.
*
* @since 3.16
*/
private AffineTransform gridToCRS;
/**
* The mosaic 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 image I/O listeners. Can contains both read and write listeners.
*/
private final IIOListeners listeners;
/**
* The filename formatter.
*/
private final FilenameFormatter formatter;
/**
* The logging level for tiling information during reads and write operations. If {@code null},
* then the level shall be selected by {@link PerformanceLevel#forDuration(long, TimeUnit)}.
*/
private Level logLevel;
/**
* Creates a new instance which will use the
* {@linkplain TileManagerFactory#DEFAULT default tile manager factory}.
*/
public MosaicBuilder() {
this(null);
}
/**
* Generates tiles using the specified tile manager 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();
listeners = new IIOListeners();
}
/**
* Returns the logging level for information about tiles being read and written.
* The default value is one of the {@link PerformanceLevel} constants, determined
* according the duration of the operation.
*
* @return The current logging level.
*/
@Override
public synchronized Level getLogLevel() {
final Level level = logLevel;
return (level != null) ? level : PerformanceLevel.PERFORMANCE;
}
/**
* Sets the logging level for information about tiles being read and written. A {@code null}
* value restores the default level documented in the {@link #getLogLevel()} method.
*
* @param level The new logging level.
*/
@Override
public synchronized void setLogLevel(final Level level) {
logLevel = level;
}
/**
* Returns the tile layout. This is an enumeration that specify how this {@code MosaicBuilder}
* will lay out the new tiles relative to each other. For example if the pixels in an image
* <cite>overview</cite> cover a geographic area 2 time larger (in width and height) than the
* pixels in an <cite>original</cite> image, then we have a choice:
* <p>
* <ul>
* <li>The overview image as a whole covers the same geographic area than the original image,
* in which case the overview has 2×2 less pixels than the original image
* ({@link TileLayout#CONSTANT_GEOGRAPHIC_AREA CONSTANT_GEOGRAPHIC_AREA}).</li>
* <li>The overview image has the same amount of pixels than the original image, in which
* case the image as a whole covers a geographic area 2×2 bigger than the original
* image ({@link TileLayout#CONSTANT_TILE_SIZE CONSTANT_TILE_SIZE}).</li>
* </ul>
* <p>
* The default value is {@link TileLayout#CONSTANT_TILE_SIZE CONSTANT_TILE_SIZE}.
*
* @return An identification of current tile layout.
*/
public synchronized 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 synchronized 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(
Errors.Keys.IllegalArgument_2, "layout", layout));
}
/**
* Returns the tile directory, or {@code null} for current directory. This is the directory
* where {@link #writeFromInput(Object, MosaicImageWriteParam) writeFromInput} methods will
* write the new tiles, if writing tiles is allowed. This is also the directory where the
* {@code TileManager} created by the above methods will read the tiles back.
* <p>
* The directory may be either relative or absolute. The default value is {@code null}.
*
* @return The current tiles directory.
*/
public synchronized Path 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.
* @deprecated use {@link #setTileDirectory(Path)}
*/
public synchronized void setTileDirectory(final File directory) {
this.directory = directory.toPath();
}
/**
* 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 synchronized void setTileDirectory(final Path 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 #createTileManager(Object)} from its input argument.
*
* @return The current image reader provider for tiles.
*/
public synchronized 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 #createTileManager(Object)}.
* <p>
* It is recommended to avoid {@link WorldFileImageReader} provider, in order to avoid
* unnecessary attempts to read the {@code ".tfw"} and {@code ".prj"} files. Callers
* can use the following code:
*
* {@preformat java
* setTileReaderSpi(WorldFileImageReader.Spi.unwrap(provider));
* }
*
* @param provider The new image reader provider for tiles.
*/
public synchronized void setTileReaderSpi(final ImageReaderSpi provider) {
this.tileReaderSpi = provider;
}
/**
* Sets the {@linkplain ImageReader image reader} provider by name. This convenience method
* searches 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(final String format) throws IllegalArgumentException {
// No need to synchronize.
setTileReaderSpi(Formats.getReaderByFormatName(format, WorldFileImageReader.Spi.class));
}
/**
* Returns the transform from mosaic pixel coordinates to mosaic geodetic coordinates,
* or {@code null} if none. This transform is optional. If specified, then the builder
* will forward this value to {@link TileManager#setGridToCRS(AffineTransform)}.
*
* @return The <cite>pixel to geodetic</cite> transform, or {@code null} if none.
*
* @since 3.16
*/
public synchronized AffineTransform getGridToCRS() {
return (gridToCRS != null) ? (AffineTransform) gridToCRS.clone() : null;
}
/**
* Sets the transform from mosaic pixel coordinates to mosaic geodetic coordinates.
* This transform is optional. If specified, then the builder will forward this value
* to {@link TileManager#setGridToCRS(AffineTransform)}.
*
* {@section Tips}
* <ul>
* <li>If the available information is rather a geodetic envelope,
* then the transform can be computed from the envelope using
* {@link org.geotoolkit.referencing.operation.builder.GridToEnvelopeMapper}.</li>
* <li>If the available information is rather a {@link org.opengis.coverage.grid.RectifiedGrid},
* then the transform can be computed from the rectified
* grid using {@link org.geotoolkit.image.io.metadata.MetadataHelper}.</li>
* </ul>
*
* @param tr The <cite>pixel to geodetic</cite> transform, or {@code null} if none.
*
* @since 3.16
*/
public synchronized void setGridToCRS(final AffineTransform tr) {
gridToCRS = (tr != null) ? new AffineTransform(tr) : null;
}
/**
* Returns the grid envelope (in pixels) of the mosaic as a whole, or {@code null}
* if not set. In the later case, the bounds will be inferred from the input image
* when {@link #createTileManager(Object)} is invoked.
*
* @return The current grid envelope of the mosaic, or {@code null}.
*/
public synchronized Rectangle getUntiledImageBounds() {
return (untiledBounds != null) ? (Rectangle) untiledBounds.clone() : null;
}
/**
* Sets the grid envelope (in pixels) of the mosaic as a whole.
* A {@code null} value discards any value previously set.
*
* @param bounds The new grid envelope of the mosaic, or {@code null}.
*/
public synchronized 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 synchronized 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 discards any value previously set.
*
* @param size The new tile size.
*/
public synchronized void setTileSize(final Dimension size) {
if (size == null) {
tileSize = null;
} else {
if (size.width < 2 || size.height < 2) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_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 searches for a value <var>x</var> inside the {@code [minSize...maxSize]}
* range where {@code imageSize}/<var>x</var> has the largest amount of
* {@linkplain MathFunctions#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. This value is inside the {@code [minSize...maxSize]}
* range except if {@code imageSize} was smaller than {@code 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(
Errors.Keys.IllegalRange_2, minSize, maxSize));
}
ensureBetween("tileSize", minSize, maxSize, tileSize);
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 = MathFunctions.divisors(new Fraction(imageSize, i).round()).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 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 guaranteed 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 synchronized Dimension[] getSubsamplings() {
if (subsamplings == null) {
final Rectangle untiledBounds = getUntiledImageBounds();
if (untiledBounds == null) {
return null;
}
final Dimension tileSize = getTileSize();
if (tileSize == null) {
return null;
}
/*
* 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). We calculate (nx,ny)
* which are the maximum subsamplings expected (inclusive).
*/
final int nx, ny;
if (layout == TileLayout.CONSTANT_GEOGRAPHIC_AREA) {
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;
}
final int[] sub = new int[Math.max(1, Integer.SIZE - Integer.numberOfLeadingZeros(Math.max(nx, ny) - 1))];
for (int i=0, s=1; i<sub.length; i++, s <<= 1) {
sub[i] = s;
}
setSubsamplings(sub);
}
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 equal 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 synchronized 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(
Errors.Keys.IllegalArgument_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) {
// No need to synchronize.
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 overridden the former.
setSubsamplings(newSubsamplings);
}
/**
* Creates a tile manager from the informations supplied in above setters.
* The default implementation delegates to {@link #createTileManager()},
* wrapping any potential {@link IOException} into a {@link BackingStoreException}.
*
* @return The tile manager created from the information returned by getter methods.
* @throws BackingStoreException if {@link #createTileManager()} threw {@link IOException}.
*
* @since 3.20
*/
@Override
public TileManager build() throws BackingStoreException {
try {
return createTileManager();
} catch (IOException e) {
throw new BackingStoreException(e);
}
}
/**
* 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(Rectangle)}</li>
* <li>{@link #setTileReaderSpi(ImageReaderSpi)}</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 synchronized 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 writing, 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(Errors.Keys.NoImageReader));
}
untiledBounds = getUntiledImageBounds(); // Forces computation, if any.
if (untiledBounds == null) {
throw new IllegalStateException(Errors.format(Errors.Keys.UnspecifiedImageSize));
}
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, if a transform was already
* specified in the input tiles, inherit that transform.
*/
if (gridToCRS == null && input != null) {
final ImageGeometry geometry = input.getGridGeometry();
if (geometry != null) {
gridToCRS = geometry.getGridToCRS();
}
}
if (gridToCRS != null) {
output.setGridToCRS(gridToCRS);
}
return output;
}
/**
* Creates tiles for the following cases:
* <p>
* <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 || subsamplings.length == 0) {
throw new IllegalStateException(Errors.format(Errors.Keys.NoParameterValue_1,
Vocabulary.format(Vocabulary.Keys.Subsampling)));
}
final List<Tile> tiles;
final OverviewLevel[] levels;
if (usePattern) {
tiles = null;
levels = new OverviewLevel[subsamplings.length];
} else {
tiles = new ArrayList<>();
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();
final String dirPath = directory.toString();
pattern = "Path:" + dirPath + File.separator + 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);
Path file = directory.resolve(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 comparison 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#createTileManager(Object)}.
* Compared to the parent {@code MosaicImageWriter} class, this writer adds the following
* capabilities:
* <p>
* <ul>
* <li>Sets the following properties with values inferred from the given reader:
* <ul>
* <li>{@link MosaicBuilder#setUntiledImageBounds(Rectangle)}</li>
* <li>{@link MosaicBuilder#setTileReaderSpi(ImageReaderSpi)}</li>
* </ul></li>
* <li>Remember the output {@link TileManager} produced by the builder.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.01
*
* @since 2.5
* @module
*/
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. This is inferred from the
* {@link ImageReader} given by the user - this is not computed by this class.
*/
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;
listeners.addListenersTo(this);
}
/**
* Returns {@code true} if the writer is allows to cache the tiles for performance.
* This method is overridden in order to disallow caching if the subsampling is greater
* than (1,1). Caching every pixels in this case would be more costly than needed since
* we going the use only a small fraction of them.
*/
@Override
protected boolean isCachingEnabled(ImageReader input, int inputIndex) throws IOException {
for (final Dimension subsampling : getSubsamplings()) {
if (subsampling.width == 1 && subsampling.height == 1) {
return super.isCachingEnabled(input, inputIndex);
}
}
return false;
}
/**
* Invoked after {@link MosaicImageWriter} has created a reader and set the input.
* This implementation sets the following properties with values inferred from the
* given reader:
* <p>
* <ul>
* <li>{@link MosaicBuilder#setUntiledImageBounds(Rectangle)}</li>
* <li>{@link MosaicBuilder#setTileReaderSpi(ImageReaderSpi)}</li>
* </ul>
* <p>
* In addition, the reader listeners are set to the values given to
* {@link MosaicBuilder#listeners()}.
*/
@Override
protected boolean filter(ImageReader reader) throws IOException {
final Rectangle bounds = new Rectangle();
bounds.width = reader.getWidth (inputIndex);
bounds.height = reader.getHeight(inputIndex);
/*
* At this point, 'bounds' is the value that we want to give to
* setUntiledImageBounds(Rectangle). But we will not do that now;
* we will wait for making sure that the image can be read.
*
* If the reader given to this method is actually a MosaicImageReader,
* then it will be replaced by the reader of the underlying tiles.
*/
TileManager input = null;
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.readers.getTileReader();
}
/*
* Now set the MosaicBuilder properties:
* - The SPI of tile to create
* - The bounds of the mosaic as a whole.
*/
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;
}
/*
* Sets the listeners and we are done. We use the listeners field instead than
* the method in order to avoid giving the ImageReader to client code (which
* could happen if addListenersTo(ImageReader) has been overridden).
*/
listeners.addListenersTo(reader);
return super.filter(reader);
}
/**
* Invoked when a tile is about to be written. Delegates to a method that users can
* override. This is only a hook for user-overriding - the default implementations
* of those methods does nothing.
*/
@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. The input may be a {@link File}
* if the mosaic should be created from a single input image, or may be a collection of
* {@link Tile}s or a {@link TileManager} if the new mosaic should be created from an
* existing one.
* <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 occurred while reading the untiled image.
*/
public synchronized TileManager createTileManager(final Object input) throws IOException {
final MosaicImageWriteParam param = new MosaicImageWriteParam();
param.setTileWritingPolicy(TileWritingPolicy.NO_WRITE);
return writeFromInput(input, 0, param, true); // Do not invoke the user-overrideable method.
}
/**
* Creates the tile manager and writes the tiles on disk. This is equivalent to
* <code>{@linkplain #writeFromInput(Object,int,MosaicImageWriteParam) writeFromInput}(input,
* <b>0</b>, policy)</code> except that this method ensures that the input contains only one
* image. If more than one image is found, then an exception is throw. This is often desireable
* when the input is a collection of {@link Tile}s, since having more than one "image" (where
* "image" in this context means different instances of {@code TileManager}) means that we
* failed to create a single mosaic from a set of source tiles.
*
* @param input The image input, typically as a {@link File} or an other {@link TileManager}.
* @param param The parameter to be given to {@link MosaicImageWriter}, or {@code null}
* for the default parameters.
* @return The tiles, or {@code null} if the process has been aborted while writing tiles.
* @throws IOException if an error occurred while reading the untiled image or while writing
* the tiles to disk.
*
* @since 3.01
*/
public synchronized TileManager writeFromInput(final Object input,
final MosaicImageWriteParam param) throws IOException
{
return writeFromInput(input, 0, param, true);
}
/**
* Creates the tile manager and writes the tiles on disk. The {@linkplain #getUntiledImageBounds
* untiled image bounds} and {@linkplain #getTileReaderSpi tile reader SPI} are inferred
* from the input, unless they were explicitly specified. The input may be a {@link File}
* if the mosaic should be created from a single input image, or may be a collection of
* {@link Tile}s or a {@link TileManager} if the new mosaic should be created from an
* existing one.
*
* @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 param The parameter to be given to {@link MosaicImageWriter}, or {@code null}
* for the default parameters.
* @return The tiles, or {@code null} if the process has been aborted while writing tiles.
* @throws IOException if an error occurred while reading the untiled image or while writing
* the tiles to disk.
*
* @since 3.01
*/
public synchronized TileManager writeFromInput(final Object input, final int inputIndex,
final MosaicImageWriteParam param) throws IOException
{
return writeFromInput(input, inputIndex, param, false);
}
/**
* Implements the public {@code writeFromInput} methods.
*
* @param onlyOneImage If {@code true}, then the operation fails if the input contains more than
* one image. This is often necessary if the input is a collection of {@link TileManager}s,
* since more than 1 image means that the manager failed to create a single mosaic from
* a set of source images.
*/
private TileManager writeFromInput(final Object input, final int inputIndex,
final MosaicImageWriteParam param, final boolean onlyOneImage) throws IOException
{
formatter.ensurePrefixSet(input);
final TileWritingPolicy policy;
if (param != null) {
policy = param.getTileWritingPolicy();
} else {
policy = TileWritingPolicy.DEFAULT;
}
final Writer writer = new Writer(inputIndex, policy);
writer.setLogLevel(logLevel); // Don't use getLogLevel() because we want the null value for the default.
try {
if (!writer.writeFromInput(input, inputIndex, param, onlyOneImage)) {
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 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.getGridGeometry() == 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 a modifiable collection of image I/O listeners. Methods like
* {@link IIOListeners#addIIOReadProgressListener addIIOReadProgressListener} and
* {@link IIOListeners#addIIOWriteProgressListener addIIOWriteProgressListener} can
* be invoked on the returned object. The read listeners are used when reading the
* input mosaic, while the write listeners are used when writing the output mosaic.
*
* @return The manager of image I/O listeners.
*
* @since 3.02
*/
public IIOListeners listeners() {
return listeners;
}
/**
* 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 overridden, 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() == 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 tile is about to be written. The default implementation does
* nothing. Subclasses can override this method in order to set custom write parameters.
* <p>
* The {@linkplain ImageWriteParam#setSourceRegion source region} and
* {@linkplain ImageWriteParam#setSourceSubsampling source subsampling} parameters can not be
* set through this method. Their setting will be overwritten by the caller because their
* values depend on the strategy chosen by {@code MosaicImageWriter} for reading images,
* which itself depends on the amount of available memory.
*
* @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 {
}
}