/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2011-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2011-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.coverage.io;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.opengis.coverage.grid.GridGeometry;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.geotoolkit.image.io.mosaic.TileManager;
import org.geotoolkit.image.io.mosaic.TileManagerFactory;
import org.geotoolkit.image.io.mosaic.MosaicBuilder;
import org.geotoolkit.image.io.mosaic.TileWritingPolicy;
import org.geotoolkit.image.io.mosaic.MosaicImageWriteParam;
import org.geotoolkit.internal.image.io.SupportFiles;
import org.geotoolkit.coverage.grid.GridGeometry2D;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.io.wkt.PrjFiles;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.lang.Debug;
import static org.apache.sis.util.ArgumentChecks.*;
/**
* An image reader specialized for image mosaics. The {@linkplain #getInput() input} shall be
* instance of {@link TileManager}. In addition, this coverage reader requires a CRS determined
* at construction time.
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @version 3.18
*
* @since 3.18
* @module
*/
final class MosaicCoverageReader extends ImageCoverageReader {
/**
* The extension to give to the directory which will contain the cached tiles.
*/
static final String CACHE_EXTENSION = ".tiles";
/**
* {@code true} if this reader has created a new mosaic.
* This is used only for debugging and testing purpose.
*/
@Debug
boolean saved;
/**
* The coordinate reference system of the mosaic.
*/
private final CoordinateReferenceSystem crs;
/**
* The grid geometry, computed when first needed.
*/
private transient GridGeometry2D gridGeometry;
/**
* Creates a new reader for the given tiles and CRS.
*
* @param tiles The tiles.
* @param crs The coordinate reference system.
* @throws CoverageStoreException If the reader can not be created for the given input.
*/
public MosaicCoverageReader(final Object tiles, final CoordinateReferenceSystem crs)
throws CoverageStoreException
{
this.crs = crs;
setInput(tiles);
}
/**
* Creates a mosaic reader using a cache of tiles at different resolutions. Tiles will be
* created the first time this constructor is invoked for a given input. The tiles will be
* created in a sub-directory having the same name than the given input, with an additional
* {@value #CACHE_EXTENSION} extension.
* <p>
* This method will fetch the {@linkplain CoordinateReferenceSystem Coordinate Reference System}
* from a file having the same name than the given {@code input} file but with the {@code ".prj"}
* suffix.
*
* @param input The input to read.
* @param generate If {@code true}, this constructor is allowed to generate its own mosaic
* from the given input.
* @throws CoverageStoreException If the reader can not be created for the given file.
* @deprecated use {@link #MosaicCoverageReader(Path, boolean)} instead
*/
@Deprecated
public MosaicCoverageReader(final File input, final boolean generate) throws CoverageStoreException {
this(input.toPath(), generate);
}
/**
* Creates a mosaic reader using a cache of tiles at different resolutions. Tiles will be
* created the first time this constructor is invoked for a given input. The tiles will be
* created in a sub-directory having the same name than the given input, with an additional
* {@value #CACHE_EXTENSION} extension.
* <p>
* This method will fetch the {@linkplain CoordinateReferenceSystem Coordinate Reference System}
* from a file having the same name than the given {@code input} file but with the {@code ".prj"}
* suffix.
*
* @param input The input to read.
* @param generate If {@code true}, this constructor is allowed to generate its own mosaic
* from the given input.
* @throws CoverageStoreException If the reader can not be created for the given file.
*/
public MosaicCoverageReader(final Path input, final boolean generate) throws CoverageStoreException {
Path directory = input.getParent(); // May be null.
if (directory != null && !Files.isDirectory(directory)) {
throw new CoverageStoreException(Errors.format(Errors.Keys.NotADirectory_1, directory));
}
if (directory == null) {
throw new CoverageStoreException(Errors.format(Errors.Keys.FileDoesNotExist_1, directory));
}
/*
* Before to perform the more costly operations, try to load the ".prj" file.
*/
try {
crs = PrjFiles.read((Path) SupportFiles.changeExtension(input, "prj"));
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
/*
* If a serialized TileManager exists, reuse it.
*/
final String inputName = input.getFileName().toString();
directory = directory.resolve(inputName + CACHE_EXTENSION);
final Path serialized = directory.resolve(TileManager.SERIALIZED_FILENAME);
if (Files.exists(serialized)) {
TileManager[] managers = null;
try {
managers = TileManagerFactory.DEFAULT.create(serialized);
} catch (Exception e) { // Catch IOException and various RuntimeExceptions.
// Ignore, we will try to rebuild the manager using the code below.
// Declare the public CoverageIO.createMosaicReader(...) method in the log record.
Logging.recoverableException(GridCoverageStore.LOGGER, CoverageIO.class, "createMosaicReader", e);
}
if (managers != null && managers.length == 1) {
setInput(managers[0]);
return;
}
}
/*
* If we are not allowed to generate a new mosaic, create
* a TileManager from the content of the given directory.
*/
if (!generate) {
setInput(input);
return;
}
/*
* Creates (if it does not already exist) the directory which will contain the tiles.
* If the directory already exists, we will let it untouched and assume that it has
* been created by a previous execution of this constructor.
*/
final TileWritingPolicy policy;
if (Files.exists(directory)) {
if (!Files.isDirectory(directory)) {
throw new CoverageStoreException(Errors.format(Errors.Keys.NotADirectory_1, directory));
}
policy = TileWritingPolicy.NO_WRITE;
} else {
try {
Files.createDirectory(directory);
} catch (IOException e) {
throw new CoverageStoreException(Errors.format(Errors.Keys.CantCreateDirectory_1, directory));
}
policy = TileWritingPolicy.OVERWRITE;
}
final MosaicBuilder builder = new MosaicBuilder();
builder.setTileDirectory(directory);
/*
* Process to the tile generation and serialize the tile manager for faster access
* next time the mosaic will be required.
*/
final MosaicImageWriteParam params = new MosaicImageWriteParam();
params.setTileWritingPolicy(policy);
final TileManager manager;
try {
manager = builder.writeFromInput(input, params);
if (policy == TileWritingPolicy.OVERWRITE) {
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serialized))) {
out.writeObject(manager);
out.writeObject(crs);
}
}
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
setInput(manager);
saved = true;
}
/**
* Sets the input, which shall be an instance of {@link TileManager}. Other types
* will be processed by {@link TileManagerFactory#createFromObject(Object)}.
*
* @param input The {@code TileManager} to use as input, or {@code null}.
* @throws IllegalArgumentException if input is not a {@code TileManager} instance.
* @throws CoverageStoreException if the input can not be set.
*/
@Override
public void setInput(Object input) throws CoverageStoreException {
if (input != null && !(input instanceof TileManager)) {
// Attempt a TileManager creation.
final TileManager[] managers;
try {
managers = TileManagerFactory.DEFAULT.createFromObject(input);
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
if (managers != null && managers.length == 1) {
input = managers[0];
}
}
ensureCanCast("input", TileManager.class, input);
super.setInput(input);
}
/**
* Returns the input, or {@code null} if none.
*/
@Override
public TileManager getInput() throws CoverageStoreException {
return (TileManager) super.getInput();
}
/**
* Returns the grid geometry computed from the tile manager.
*/
@Override
public GridGeometry2D getGridGeometry(final int index) throws CoverageStoreException {
/*
* There is typically only one coverage. If the user asks for an other coverage,
* delegates to the super-class (which will typically thrown an exception).
*/
if (index != 0) {
return super.getGridGeometry(index);
}
if (gridGeometry == null) {
final TileManager input = getInput();
if (input == null) {
throw new IllegalStateException(formatErrorMessage(Errors.Keys.NoImageInput));
}
final GridGeometry gg;
try {
gg = input.getGridGeometry();
} catch (IOException e) {
throw new CoverageStoreException(formatErrorMessage(e), e);
}
gridGeometry = (gg == null) ? super.getGridGeometry(index) :
new GridGeometry2D(gg.getExtent(), PixelInCell.CELL_CORNER, gg.getGridToCRS(), crs, null);
}
return gridGeometry;
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
return "MosaicCoverageReader[saved=" + saved + ']';
}
}