/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006 - 2016, Open Source Geospatial Foundation (OSGeo)5
*
* 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.gce.imagemosaic;
import java.awt.Rectangle;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.spi.ImageReaderSpi;
import javax.media.jai.ImageLayout;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.DefaultHarvestedSource;
import org.geotools.coverage.grid.io.DimensionDescriptor;
import org.geotools.coverage.grid.io.GranuleSource;
import org.geotools.coverage.grid.io.HarvestedSource;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.coverage.grid.io.StructuredGridCoverage2DReader;
import org.geotools.coverage.grid.io.footprint.MultiLevelROIProvider;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultFileServiceInfo;
import org.geotools.data.FileGroupProvider.FileGroup;
import org.geotools.data.ResourceInfo;
import org.geotools.data.ServiceInfo;
import org.geotools.factory.Hints;
import org.geotools.gce.imagemosaic.ImageMosaicEventHandlers.ExceptionEvent;
import org.geotools.gce.imagemosaic.ImageMosaicEventHandlers.FileProcessingEvent;
import org.geotools.gce.imagemosaic.ImageMosaicEventHandlers.ProcessingEvent;
import org.geotools.gce.imagemosaic.OverviewsController.OverviewLevel;
import org.geotools.gce.imagemosaic.Utils.Prop;
import org.geotools.gce.imagemosaic.catalog.CatalogConfigurationBean;
import org.geotools.gce.imagemosaic.catalog.GranuleCatalog;
import org.geotools.gce.imagemosaic.catalog.GranuleCatalogFactory;
import org.geotools.gce.imagemosaic.catalog.MultiLevelROIProviderMosaicFactory;
import org.geotools.gce.imagemosaic.catalogbuilder.CatalogBuilderConfiguration;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.util.Utilities;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.geometry.BoundingBox;
import org.opengis.metadata.Identifier;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValue;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
/**
* Reader responsible for providing access to mosaic of georeferenced images. Citing JAI documentation:
*
* The "Mosaic" operation creates a mosaic of two or more source images. This operation could be used for example to assemble a set of overlapping
* geospatially rectified images into a contiguous image. It could also be used to create a montage of photographs such as a panorama.
*
* All source images are assumed to have been geometrically mapped into a common coordinate space. The origin (minX, minY) of each image is therefore
* taken to represent the location of the respective image in the common coordinate system of the source images. This coordinate space will also be
* that of the destination image.
*
* All source images must have the same data type and sample size for all bands and have the same number of bands as color components. The destination
* will have the same data type, sample size, and number of bands and color components as the sources.
*
*
* @author Simone Giannecchini, GeoSolutions S.A.S
* @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs
* @since 2.3
*
*
*
* @source $URL$
*/
@SuppressWarnings("rawtypes")
public class ImageMosaicReader extends AbstractGridCoverage2DReader
implements StructuredGridCoverage2DReader {
Set<String> names = new HashSet<String>();
String defaultName = null;
public static final String UNSPECIFIED = "_UN$PECIFIED_";
Map<String, RasterManager> rasterManagers = new ConcurrentHashMap<String, RasterManager>();
public RasterManager getRasterManager(String name) {
if (name != null && rasterManagers.containsKey(name)) {
return rasterManagers.get(name);
}
return null;
}
@Override
public String[] getGridCoverageNames() {
return names.toArray(new String[] {});
}
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(ImageMosaicReader.class);
/**
* The source {@link URL} pointing to the index shapefile for this {@link ImageMosaicReader}.
*/
URL sourceURL;
File parentDirectory;
boolean expandMe;
PathType pathType;
ExecutorService multiThreadedLoader;
String locationAttributeName = Utils.DEFAULT_LOCATION_ATTRIBUTE;
int maxAllowedTiles = ImageMosaicFormat.MAX_ALLOWED_TILES.getDefaultValue();
/** The suggested SPI to avoid SPI lookup */
ImageReaderSpi suggestedSPI;
GranuleCatalog granuleCatalog;
boolean cachingIndex;
boolean imposedBBox;
boolean heterogeneousGranules;
boolean checkAuxiliaryMetadata = false;
String typeName;
/**
* Enumeration object used for defining 3 different behaviours for the Harvesting, each of them associated to one of these 3 objects:
* <ul>
* <li>File</li>
* <li>Directory</li>
* <li>List of Files</li>
* </ul>
*
* @author Nicola Lagomarsini, GeoSolutions S.A.S.
*
*/
public enum HarvestedResource {
FILE {
@Override
public void harvest(String defaultCoverage, Object source, Hints hints,
final List<HarvestedSource> result, ImageMosaicReader reader) {
File file;
if (source instanceof Collection<?>) {
file = (File) ((Collection<?>) source).iterator().next();
} else {
file = (File) source;
}
// Directory associated to the input File
File directory = file.getParentFile();
// File Filter associated to the input File
IOFileFilter filter = FileFilterUtils.nameFileFilter(file.getName());
// Harvesting file
harvestCalculation(defaultCoverage, result, reader, directory, filter);
}
},
DIRECTORY {
@Override
public void harvest(String defaultCoverage, Object source, Hints hints,
final List<HarvestedSource> result, ImageMosaicReader reader) {
File directory;
if (source instanceof Collection<?>) {
directory = (File) ((Collection<?>) source).iterator().next();
} else {
directory = (File) source;
}
// Harvesting directory
harvestCalculation(defaultCoverage, result, reader, directory, null);
}
},
FILE_COLLECTION {
@Override
public void harvest(String defaultCoverage, Object source, Hints hints,
final List<HarvestedSource> result, final ImageMosaicReader reader) {
// I have already checked that it is a Collection of File objects
Collection<File> files = (Collection<File>) source;
// Harvesting
// prepare the walker configuration
CatalogBuilderConfiguration configuration = new CatalogBuilderConfiguration();
configuration.setParameter(Prop.ABSOLUTE_PATH, Boolean.toString(true));
// Setting of the HARVEST_DIRECTORY property for passing the checks even if it is not used
// Selection of the first file
Iterator<File> it = files.iterator();
String indexingPath = it.next().getAbsolutePath();
configuration.setParameter(Prop.HARVEST_DIRECTORY, indexingPath);
if (defaultCoverage == null) {
String[] coverageNames = reader.getGridCoverageNames();
defaultCoverage = (coverageNames != null && coverageNames.length > 0)
? coverageNames[0] : Utils.DEFAULT_INDEX_NAME;
}
configuration.setParameter(Prop.INDEX_NAME, defaultCoverage);
configuration.setHints(new Hints(Utils.MOSAIC_READER, reader));
File mosaicSource = DataUtilities.urlToFile(reader.sourceURL);
if (!mosaicSource.isDirectory()) {
mosaicSource = mosaicSource.getParentFile();
}
configuration.setParameter(Prop.ROOT_MOSAIC_DIR, mosaicSource.getAbsolutePath());
// run the walker and collect information
ImageMosaicEventHandlers eventHandler = new ImageMosaicEventHandlers();
final ImageMosaicConfigHandler catalogHandler = new ImageMosaicConfigHandler(
configuration, eventHandler) {
protected GranuleCatalog buildCatalog() throws IOException {
return reader.granuleCatalog;
};
};
// Creation of the Walker for the File List
ImageMosaicFileCollectionWalker walker = new ImageMosaicFileCollectionWalker(
catalogHandler, eventHandler, files);
eventHandler.addProcessingEventListener(
new ImageMosaicEventHandlers.ProcessingEventListener() {
@Override
public void getNotification(ProcessingEvent event) {
if (event instanceof FileProcessingEvent) {
FileProcessingEvent fileEvent = (FileProcessingEvent) event;
result.add(new DefaultHarvestedSource(fileEvent.getFile(),
fileEvent.isIngested(), fileEvent.getMessage()));
}
}
@Override
public void exceptionOccurred(ExceptionEvent event) {
// nothing to do
}
});
// Wait the Walker ends its operations
walker.run();
}
};
HarvestedResource() {
}
/**
* Harvesting of the input resource. The result will be strored inside the {@link List} object.
*
* @param defaultCoverage
* @param source
* @param hints
* @param result
* @param reader
*/
public abstract void harvest(String defaultCoverage, Object source, Hints hints,
final List<HarvestedSource> result, ImageMosaicReader reader);
/**
* Returns the HarvestedResource associated to the input Object
*
* @param source
* @return
*/
public static HarvestedResource getResourceFromObject(Object source) {
// Check if the resource is a File or a Directory
if (source instanceof File) {
return getResourceFromFile((File) source);
}
// For a String instance, it is converted to String
if (source instanceof String) {
File file = new File((String) source);
return getResourceFromFile(file);
}
// Check if the input Object is a File Collection
if (source instanceof Collection<?>) {
Collection<File> files = null;
try {
files = (Collection<File>) source;
} catch (ClassCastException e) {
// Log the exception
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
}
// If the files are present
if (files != null) {
// No File is saved
int fileSize = files.size();
// Check on the File Size
if (fileSize < 1) {
return null;
} else if (fileSize == 1) {
// If the Collection size is 1 then the object can be only a file or a directory
return getResourceFromFile(files.iterator().next());
} else {
return FILE_COLLECTION;
}
}
}
return null;
}
/**
* Check if the File Object is a DIRECTORY or not and return the associated {@link HarvestedResource}
*
* @param file
* @return
*/
private static HarvestedResource getResourceFromFile(File file) {
if (file != null) {
if (file.isDirectory()) {
return DIRECTORY;
} else {
return FILE;
}
}
return null;
}
/**
* Method for harvesting on a directory
*
* @param defaultCoverage
* @param result
* @param reader
* @param directory
* @param filter
*/
private static void harvestCalculation(String defaultCoverage,
final List<HarvestedSource> result, final ImageMosaicReader reader, File directory,
IOFileFilter filter) {
// prepare the walker configuration
CatalogBuilderConfiguration configuration = new CatalogBuilderConfiguration();
configuration.setParameter(Prop.ABSOLUTE_PATH,
Boolean.toString(Utils.DEFAULT_PATH_BEHAVIOR));
String indexingPath = directory.getAbsolutePath();
configuration.setParameter(Prop.HARVEST_DIRECTORY, indexingPath);
if (defaultCoverage == null) {
String[] coverageNames = reader.getGridCoverageNames();
defaultCoverage = (coverageNames != null && coverageNames.length > 0)
? coverageNames[0] : Utils.DEFAULT_INDEX_NAME;
}
configuration.setParameter(Prop.INDEX_NAME, defaultCoverage);
configuration.setHints(new Hints(Utils.MOSAIC_READER, reader));
File mosaicSource = DataUtilities.urlToFile(reader.sourceURL);
if (!mosaicSource.isDirectory()) {
mosaicSource = mosaicSource.getParentFile();
}
configuration.setParameter(Prop.ROOT_MOSAIC_DIR, mosaicSource.getAbsolutePath());
// run the walker and collect information
ImageMosaicEventHandlers eventHandler = new ImageMosaicEventHandlers();
final ImageMosaicConfigHandler catalogHandler = new ImageMosaicConfigHandler(
configuration, eventHandler) {
@Override
protected GranuleCatalog buildCatalog() throws IOException {
return reader.granuleCatalog;
}
};
// build the index
ImageMosaicDirectoryWalker walker = new ImageMosaicDirectoryWalker(catalogHandler,
eventHandler, filter);
eventHandler.addProcessingEventListener(
new ImageMosaicEventHandlers.ProcessingEventListener() {
@Override
public void getNotification(ProcessingEvent event) {
if (event instanceof FileProcessingEvent) {
FileProcessingEvent fileEvent = (FileProcessingEvent) event;
result.add(new DefaultHarvestedSource(fileEvent.getFile(),
fileEvent.isIngested(), fileEvent.getMessage()));
}
}
@Override
public void exceptionOccurred(ExceptionEvent event) {
// nothing to do
}
});
walker.run();
}
}
/**
* Constructor.
*
* @param source The source object.
* @throws IOException
* @throws UnsupportedEncodingException
*
*/
public ImageMosaicReader(Object source, Hints uHints) throws IOException {
super(source, uHints);
//
// try to extract a multithreaded loader if available
//
if (this.hints.containsKey(Hints.EXECUTOR_SERVICE)) {
final Object executor = uHints.get(Hints.EXECUTOR_SERVICE);
if (executor != null && executor instanceof ExecutorService) {
multiThreadedLoader = (ExecutorService) executor;
if (LOGGER.isLoggable(Level.FINE)) {
if (multiThreadedLoader instanceof ThreadPoolExecutor) {
final ThreadPoolExecutor tpe = (ThreadPoolExecutor) multiThreadedLoader;
LOGGER.fine("Using ThreadPoolExecutor with the following settings: "
+ "core pool size = " + tpe.getCorePoolSize() + "\nmax pool size = "
+ tpe.getMaximumPoolSize() + "\nkeep alive time "
+ tpe.getKeepAliveTime(TimeUnit.MILLISECONDS));
}
}
}
}
// max allowed tiles for a single request
if (this.hints.containsKey(Hints.MAX_ALLOWED_TILES))
this.maxAllowedTiles = ((Integer) this.hints.get(Hints.MAX_ALLOWED_TILES));
//
// Check source
//
if (source instanceof ImageMosaicDescriptor) {
initReaderFromDescriptor((ImageMosaicDescriptor) source, uHints);
} else {
try {
// Cloning the hints
Hints localHints = new Hints(uHints);
if (localHints != null) {
localHints.add(new Hints(Utils.MOSAIC_READER, this));
}
initReaderFromURL(source, localHints);
} catch (Exception e) {
throw new DataSourceException(e);
}
}
}
/**
* Init this {@link ImageMosaicReader} using the provided {@link ImageMosaicDescriptor} as source.
*
* @param source
* @param uHints
* @throws DataSourceException
*/
private void initReaderFromDescriptor(final ImageMosaicDescriptor source, final Hints uHints)
throws IOException {
Utilities.ensureNonNull("source", source);
final MosaicConfigurationBean configuration = source.getConfiguration();
if (configuration == null) {
throw new DataSourceException(
"Unable to create reader for this mosaic since we could not parse the configuration.");
}
extractProperties(configuration);
GranuleCatalog catalog = source.getCatalog();
if (catalog == null) {
throw new DataSourceException(
"Unable to create reader for this mosaic since the inner catalog is null.");
}
final SimpleFeatureType schema = catalog
.getType(configuration.getCatalogConfigurationBean().getTypeName());
if (schema == null) {
throw new DataSourceException(
"Unable to create reader for this mosaic since the inner catalog schema is null.");
}
granuleCatalog = catalog;
// grid geometry
setGridGeometry(typeName);
// raster manager
addRasterManager(configuration, true);
}
/**
* Init this {@link ImageMosaicReader} using the provided object as a source referring to an {@link URL}.
*
* @param source
* @param uHints
* @throws DataSourceException
*/
private void initReaderFromURL(final Object source, final Hints hints) throws Exception {
this.sourceURL = Utils.checkSource(source, hints);
// Preliminary check on source
if (this.sourceURL == null) {
throw new DataSourceException(
"This plugin accepts File, URL or String. The string may describe a File or an URL");
}
// Load properties file
MosaicConfigurationBean configuration = null;
try {
if (sourceURL.getProtocol().equals("file")) {
final File sourceFile = DataUtilities.urlToFile(sourceURL);
if (!sourceFile.exists()) {
throw new DataSourceException(
"The specified sourceURL doesn't refer to an existing file");
}
}
if (sourceURL != null) {
parentDirectory = DataUtilities.urlToFile(sourceURL);
if (!parentDirectory.isDirectory()) {
parentDirectory = parentDirectory.getParentFile();
}
}
configuration = Utils.loadMosaicProperties(sourceURL);
if (configuration == null) {
//
// do we have a datastore properties file? It will preempt on the shapefile
//
final File parent = DataUtilities.urlToFile(sourceURL).getParentFile();
// this can be used to look for properties files that do NOT define a datastore
final File[] properties = parent
.listFiles((FilenameFilter) FileFilterUtils.and(
FileFilterUtils.notFileFilter(
FileFilterUtils.nameFileFilter("indexer.properties")),
FileFilterUtils.and(
FileFilterUtils.notFileFilter(FileFilterUtils
.nameFileFilter("datastore.properties")),
FileFilterUtils.makeFileOnly(
FileFilterUtils.suffixFileFilter(".properties")))));
// do we have a valid datastore + mosaic properties pair?
final File datastoreProperties = new File(parent, "datastore.properties");
// Scan for MosaicConfigurationBeans from properties files
List<MosaicConfigurationBean> beans = new ArrayList<>();
for (File propFile : properties) {
if (Utils.checkFileReadable(propFile) && Utils
.loadMosaicProperties(DataUtilities.fileToURL(propFile)) != null) {
configuration = Utils
.loadMosaicProperties(DataUtilities.fileToURL(propFile));
if (configuration != null) {
beans.add(configuration);
}
}
}
// In case we didn't find any configuration bean and datastore properties, we can't do anything
if (beans.isEmpty() && !datastoreProperties.exists()) {
throw new DataSourceException(
"No mosaic properties file or datastore properties file have been found");
}
// Catalog initialization from datastore
GranuleCatalog catalog = null;
final Properties params = ImageMosaicConfigHandler
.createGranuleCatalogProperties(datastoreProperties);
// Since we are dealing with a catalog from an existing store, make sure to scan for all the typeNames on initialization
final Object typeNames = params.get(Utils.SCAN_FOR_TYPENAMES);
if (typeNames != null) {
params.put(Utils.SCAN_FOR_TYPENAMES, Boolean.valueOf(typeNames.toString()));
} else {
params.put(Utils.SCAN_FOR_TYPENAMES, Boolean.TRUE);
}
if (beans.size() > 0) {
catalog = GranuleCatalogFactory.createGranuleCatalog(sourceURL,
beans.get(0).getCatalogConfigurationBean(), params, getHints());
} else {
catalog = ImageMosaicConfigHandler.createGranuleCatalogFromDatastore(parent,
datastoreProperties, true, getHints());
}
MultiLevelROIProvider rois = MultiLevelROIProviderMosaicFactory
.createFootprintProvider(parent);
catalog.setMultiScaleROIProvider(rois);
if (granuleCatalog != null) {
granuleCatalog.dispose();
}
granuleCatalog = catalog;
if (granuleCatalog == null) {
throw new DataSourceException(
"Unable to create index for this URL " + sourceURL);
}
// Creating a RasterManager for each mosaic configuration found on disk
for (MosaicConfigurationBean bean : beans) {
// Add a RasterManager on top of this Mosaic configuration bean and initialize it
addRasterManager(bean, true);
}
} else {
// Old style code: we have a single MosaicConfigurationBean. Use that to create the catalog
granuleCatalog = ImageMosaicConfigHandler.createCatalog(sourceURL, configuration,
this.hints);
File parent = DataUtilities.urlToFile(sourceURL).getParentFile();
MultiLevelROIProvider rois = MultiLevelROIProviderMosaicFactory
.createFootprintProvider(parent);
granuleCatalog.setMultiScaleROIProvider(rois);
addRasterManager(configuration, true);
}
} catch (Throwable e) {
// Dispose catalog
try {
if (granuleCatalog != null) {
granuleCatalog.dispose();
}
} catch (Throwable e1) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, e1.getLocalizedMessage(), e1);
}
} finally {
granuleCatalog = null;
}
// dispose raster managers as well
try {
disposeManagers();
} catch (Throwable e1) {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, e1.getLocalizedMessage(), e1);
}
} finally {
rasterManagers = null;
}
// rethrow
throw new DataSourceException(e);
}
}
private void setGridGeometry(final ReferencedEnvelope envelope, final GranuleCatalog catalog,
String typeName) {
Utilities.ensureNonNull("index", catalog);
//
// save the bbox and prepare other info
//
final BoundingBox bounds = catalog.getBounds(typeName);
// we might have an imposed bbox
this.crs = bounds.getCoordinateReferenceSystem();
if (envelope == null)
this.originalEnvelope = new GeneralEnvelope(bounds);
else {
this.originalEnvelope = new GeneralEnvelope(envelope);
this.originalEnvelope.setCoordinateReferenceSystem(crs);
}
// original gridrange (estimated). I am using the floor here in order to make sure
// we always stays inside the real area that we have for the granule
originalGridRange = new GridEnvelope2D(
new Rectangle((int) (originalEnvelope.getSpan(0) / highestRes[0]),
(int) (originalEnvelope.getSpan(1) / highestRes[1])));
raster2Model = new AffineTransform2D(highestRes[0], 0, 0, -highestRes[1],
originalEnvelope.getLowerCorner().getOrdinate(0) + 0.5 * highestRes[0],
originalEnvelope.getUpperCorner().getOrdinate(1) - 0.5 * highestRes[1]);
}
private void setGridGeometry(final String typeName) {
setGridGeometry(null, granuleCatalog, typeName);
}
private void extractProperties(final MosaicConfigurationBean configuration) throws IOException {
// resolutions levels
numOverviews = configuration.getLevelsNum() - 1;
final double[][] resolutions = configuration.getLevels();
overViewResolutions = numOverviews >= 1 ? new double[numOverviews][2] : null;
highestRes = new double[2];
highestRes[0] = resolutions[0][0];
highestRes[1] = resolutions[0][1];
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(new StringBuilder("Highest res ").append(highestRes[0]).append(" ")
.append(highestRes[1]).toString());
if (numOverviews > 0) {
for (int i = 0; i < numOverviews; i++) {
overViewResolutions[i][0] = resolutions[i + 1][0];
overViewResolutions[i][1] = resolutions[i + 1][1];
}
}
// name
coverageName = configuration.getName();
// need a color expansion?
// this is a newly added property we have to be ready to the case where
// we do not find it.
expandMe = configuration.isExpandToRGB();
checkAuxiliaryMetadata = configuration.isCheckAuxiliaryMetadata();
CatalogConfigurationBean catalogConfigurationBean = configuration
.getCatalogConfigurationBean();
// do we have heterogenous granules
heterogeneousGranules = catalogConfigurationBean.isHeterogeneous();
// absolute or relative path
pathType = catalogConfigurationBean.isAbsolutePath() ? PathType.ABSOLUTE
: PathType.RELATIVE;
//
// location attribute
//
locationAttributeName = catalogConfigurationBean.getLocationAttribute();
// suggested SPI
final String suggestedSPIClass = catalogConfigurationBean.getSuggestedSPI();
if (suggestedSPIClass != null) {
try {
final Class<?> clazz = Class.forName(suggestedSPIClass);
if (clazz.newInstance() instanceof ImageReaderSpi)
suggestedSPI = (ImageReaderSpi) clazz.newInstance();
else
suggestedSPI = null;
} catch (Exception e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
suggestedSPI = null;
}
}
// caching for the index
cachingIndex = catalogConfigurationBean.isCaching();
// imposed BBOX
if (configuration.getEnvelope() != null) {
this.imposedBBox = true;
// we set the BBOX later to retain also the CRS
} else {
this.imposedBBox = false;
}
// typeName to be used for reading the mosaic
this.typeName = catalogConfigurationBean.getTypeName();
}
/**
* Constructor.
*
* @param source The source object.
* @throws IOException
* @throws UnsupportedEncodingException
*
*/
public ImageMosaicReader(Object source) throws IOException {
this(source, null);
}
/**
*
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new ImageMosaicFormat();
}
public GridCoverage2D read(GeneralParameterValue[] params) throws IOException {
return read(UNSPECIFIED, params);
}
/**
*
* @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
* @Override
*/
public GridCoverage2D read(String coverageName, GeneralParameterValue[] params)
throws IOException {
// check if we were disposed already
if (rasterManagers == null) {
throw new IOException(
"Looks like this reader has been already disposed or it has not been properly initialized.");
}
if (LOGGER.isLoggable(Level.FINE)) {
if (sourceURL != null) {
LOGGER.fine("Reading mosaic from " + sourceURL.toString());
} else {
LOGGER.fine("Reading mosaic");
}
final double[][] levels = getResolutionLevels(coverageName);
if (levels != null) {
final double[] highRes = levels[0];
LOGGER.fine("Highest res " + highRes[0] + " " + highRes[1]);
}
}
//
// add max allowed tiles if missing
//
if (this.maxAllowedTiles != ImageMosaicFormat.MAX_ALLOWED_TILES.getDefaultValue()) {
if (params != null) {
// first thing let's see if we have it already, in which case we do nothing since a read parameter override a Hint
boolean found = false;
for (GeneralParameterValue pv : params) {
if (pv.getDescriptor().getName()
.equals(ImageMosaicFormat.MAX_ALLOWED_TILES.getName())) {
found = true;
break;
}
}
// ok, we did not find it, let's add it back
if (!found) {
final GeneralParameterValue[] temp = new GeneralParameterValue[params.length
+ 1];
System.arraycopy(params, 0, temp, 0, params.length);
ParameterValue<Integer> tempVal = ImageMosaicFormat.MAX_ALLOWED_TILES
.createValue();
tempVal.setValue(this.maxAllowedTiles);
temp[params.length] = tempVal;
}
} else {
// we do not have nay read params, we have to create the array for them
ParameterValue<Integer> tempVal = ImageMosaicFormat.MAX_ALLOWED_TILES.createValue();
tempVal.setValue(this.maxAllowedTiles);
params = new GeneralParameterValue[] { tempVal };
}
}
//
// Loading tiles trying to optimize as much as possible
//
final Collection<GridCoverage2D> response = read(params, coverageName);
if (response.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("The response is empty. ==> returning a null GridCoverage");
}
return null;
} else {
GridCoverage2D coverage = response.iterator().next();
return coverage;
}
}
/**
* Look for the parameter containing the coverage name and check its validity. Then delegate the proper RasterManager to do the read operation.
*
* @param params
* @return
* @throws IOException
*/
private Collection<GridCoverage2D> read(GeneralParameterValue[] params, String coverageName)
throws IOException {
coverageName = checkUnspecifiedCoverage(coverageName);
return getRasterManager(coverageName).read(params);
}
/**
* Package private accessor for {@link Hints}.
*
* @return this {@link Hints} used by this reader.
*/
Hints getHints() {
return super.hints;
}
/**
* Package private accessor for the highest resolution values.
*
* @return the highest resolution values.
*/
double[] getHighestRes() {
return super.highestRes;
}
/**
*
* @return
*/
double[][] getOverviewsResolution() {
return super.overViewResolutions;
}
int getNumberOfOvervies() {
return super.numOverviews;
}
/** Package scope grid to world transformation accessor */
MathTransform getRaster2Model() {
return raster2Model;
}
/**
* Let us retrieve the {@link GridCoverageFactory} that we want to use.
*
* @return retrieves the {@link GridCoverageFactory} that we want to use.
*/
GridCoverageFactory getGridCoverageFactory() {
return coverageFactory;
}
/**
* Number of coverages for this reader is 1
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return names.size();
}
/**
* Releases resources held by this reader.
*
*/
@Override
public synchronized void dispose() {
super.dispose();
synchronized (this) {
try {
if (granuleCatalog != null)
this.granuleCatalog.dispose();
disposeManagers();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
}
}
}
/**
* Dispose raster managers
*/
private void disposeManagers() {
if (rasterManagers != null) {
Set<String> keys = rasterManagers.keySet();
for (String key : keys) {
rasterManagers.get(key).dispose();
}
rasterManagers.clear();
rasterManagers = null;
}
}
@Override
public String[] getMetadataNames() {
return getMetadataNames(UNSPECIFIED);
}
/**
* Populate the metadata names array for the specified coverageName
*
* @param coverageName
* @return
*/
@Override
public String[] getMetadataNames(String coverageName) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager != null ? manager.getMetadataNames() : null;
}
@Override
public String getMetadataValue(final String name) {
return getMetadataValue(UNSPECIFIED, name);
}
@Override
public String getMetadataValue(String coverageName, final String name) {
coverageName = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(coverageName);
return manager.getMetadataValue(name);
}
@Override
public Set<ParameterDescriptor<List>> getDynamicParameters() {
return getDynamicParameters(UNSPECIFIED);
}
@Override
public Set<ParameterDescriptor<List>> getDynamicParameters(String coverageName) {
coverageName = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(coverageName);
return (Set<ParameterDescriptor<List>>) (manager.domainsManager != null
? manager.domainsManager.getDynamicParameters() : Collections.emptySet());
}
public boolean isParameterSupported(Identifier name) {
return isParameterSupported(UNSPECIFIED, name);
}
@Override
public int getNumOverviews(String coverageName) {
coverageName = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(coverageName);
return manager.overviewsController.getNumberOfOverviews();
}
@Override
public int getNumOverviews() {
return getNumOverviews(UNSPECIFIED);
}
@Override
public double[] getReadingResolutions(OverviewPolicy policy, double[] requestedResolution) {
return getReadingResolutions(UNSPECIFIED, policy, requestedResolution);
}
@Override
public double[] getReadingResolutions(String coverageName, OverviewPolicy policy,
double[] requestedResolution) {
coverageName = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(coverageName);
final int numOverviews = getNumOverviews(coverageName);
OverviewsController overviewsController = manager.overviewsController;
OverviewLevel level = null;
if (numOverviews > 0) {
int imageIdx = overviewsController.pickOverviewLevel(policy, requestedResolution);
level = overviewsController.getLevel(imageIdx);
} else {
level = overviewsController.getLevel(0);
}
return new double[] { level.resolutionX, level.resolutionY };
}
/**
* Check whether the specified parameter is supported for the specified coverage.
*
* @param coverageName
* @param parameterName
* @return
*/
public boolean isParameterSupported(String coverageName, Identifier parameterName) {
coverageName = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(coverageName);
return manager.domainsManager != null
? manager.domainsManager.isParameterSupported(parameterName) : false;
}
/**
* Checker whether the specified coverageName is supported. In case the name is Unspecified and the manager only has 1 coverage, then it returns
* the only available coverage name (using default to speed up the response without need to access the set through an iterator). In case of
* multiple coverages, throws an Exceptions if the coverage name is unspecified.
*
* @param coverageName
*/
private String checkUnspecifiedCoverage(String coverageName) {
if (coverageName.equalsIgnoreCase(UNSPECIFIED)) {
if (getGridCoverageCount() > 1) {
throw new IllegalArgumentException(
"Need to specify the coverageName for a reader related to multiple coverages");
} else {
return defaultName;
}
} else {
if (!names.contains(coverageName)) {
throw new IllegalArgumentException("The specified coverageName is unavailable");
} else {
return coverageName;
}
}
}
/**
* Create a RasterManager on top of this {@link MosaicConfigurationBean}
*
* @param configuration the {@link MosaicConfigurationBean} to be used to create the {@link RasterManager}
* @param init {@code true} if the Manager should be initialized.
* @return
* @throws IOException
*/
protected RasterManager addRasterManager(final MosaicConfigurationBean configuration,
final boolean init) throws IOException {
Utilities.ensureNonNull("MosaicConfigurationBean", configuration);
String name = configuration.getName();
RasterManager rasterManager = new RasterManager(this, configuration);
rasterManagers.put(name, rasterManager);
names.add(name);
if (defaultName == null) {
defaultName = name;
}
if (init) {
rasterManager.initialize(false);
}
return rasterManager;
}
@Override
public GranuleSource getGranules(String coverageName, final boolean readOnly)
throws IOException, UnsupportedOperationException {
if (coverageName == null) {
coverageName = defaultName;
}
RasterManager manager = getRasterManager(coverageName);
if (manager == null) {
// Consider creating a new GranuleStore
} else {
return manager.getGranuleSource(readOnly, getHints());
}
return null;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public void createCoverage(String coverageName, SimpleFeatureType indexSchema)
throws IOException, UnsupportedOperationException {
RasterManager manager = getRasterManager(coverageName);
if (manager != null) {
manager.createStore(indexSchema);
} else {
throw new IOException(
"This implementation requires to create a RasterManager for a coverage before creating the store. "
+ coverageName);
}
}
@Override
public boolean removeCoverage(String coverageName, final boolean delete) throws IOException {
return removeCoverage(coverageName, delete, true);
}
/**
*
* @param coverageName
* @param forceDelete
* @param checkForReferences {@code true} true in case, when deleting, we need to check whether the file is being referred by some other coverage
* or not. In the latter case, we can safely delete it
*
* @return
* @throws IOException
*/
private boolean removeCoverage(String coverageName, final boolean forceDelete,
final boolean checkForReferences) throws IOException {
RasterManager manager = getRasterManager(coverageName);
if (manager != null) {
manager.removeStore(coverageName, forceDelete, checkForReferences);
// Should I preserve managers for future re-harvesting or it's ok
// to remove them
rasterManagers.remove(coverageName);
names.remove(coverageName);
if (defaultName == coverageName) {
Iterator<String> iterator = names.iterator();
if (iterator.hasNext()) {
defaultName = iterator.next();
} else {
defaultName = null;
}
}
return true;
} else {
throw new IOException(
"No Raster manager have been found for the specified coverageName. "
+ coverageName);
}
}
@Override
public GeneralEnvelope getOriginalEnvelope() {
return getOriginalEnvelope(UNSPECIFIED);
}
@Override
public GeneralEnvelope getOriginalEnvelope(String coverageName) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.spatialDomainManager.coverageEnvelope;
}
@Override
public GridEnvelope getOriginalGridRange() {
return getOriginalGridRange(UNSPECIFIED);
}
@Override
public GridEnvelope getOriginalGridRange(String coverageName) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.spatialDomainManager.gridEnvelope;
}
@Override
public MathTransform getOriginalGridToWorld(PixelInCell pixInCell) {
return getOriginalGridToWorld(UNSPECIFIED, pixInCell);
}
@Override
public MathTransform getOriginalGridToWorld(String coverageName, PixelInCell pixInCell) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.spatialDomainManager.getOriginalGridToWorld(pixInCell);
}
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return getCoordinateReferenceSystem(UNSPECIFIED);
}
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem(String coverageName) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.spatialDomainManager.coverageCRS;
}
@Override
public ImageLayout getImageLayout() throws IOException {
return getImageLayout(UNSPECIFIED);
}
@Override
public ImageLayout getImageLayout(String coverageName) throws IOException {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.defaultImageLayout;
}
@Override
public double[][] getResolutionLevels() throws IOException {
return getResolutionLevels(UNSPECIFIED);
}
@Override
public double[][] getResolutionLevels(String coverageName) throws IOException {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
return manager.levels;
}
@Override
public List<HarvestedSource> harvest(String defaultCoverage, Object source, Hints hints)
throws IOException, UnsupportedOperationException {
// Get the HarvestedResource object associated to the source. This object defines the harvseting behaviour.
HarvestedResource resource = HarvestedResource.getResourceFromObject(source);
// Check if the source object can be accepted
final List<HarvestedSource> result = new ArrayList<HarvestedSource>();
if (resource == null) {
result.add(new DefaultHarvestedSource(source, false, "Unrecognized source type"));
return result;
} else if (source instanceof File && !((File) source).exists() || singleFileList(source)) {
result.add(new DefaultHarvestedSource(source, false,
"Specified file path does not exist"));
return result;
}
// Harvesting of the input source
resource.harvest(defaultCoverage, source, hints, result, this);
String coverage = defaultCoverage != null ? defaultCoverage : this.defaultName;
// rebuild the raster manager for this coverage, as the spatial and dimensional domains
// have probably changed
RasterManager rasterManager = rasterManagers.get(coverage);
rasterManager.initialize(true);
return result;
}
/**
* Simple method used for checking if the list contains a single object and it is a file
*
* @param source
* @return
*/
private boolean singleFileList(Object source) {
if (source instanceof Collection<?>) {
Collection<?> collection = ((Collection<?>) source);
if (collection.size() == 1) {
// Selection of the single file
File file = (File) collection.iterator().next();
// Check if it exists
if (!file.exists()) {
return true;
}
}
}
return false;
}
@Override
public List<DimensionDescriptor> getDimensionDescriptors(String coverageName)
throws IOException {
RasterManager manager = getRasterManager(coverageName);
return manager.getDimensionDescriptors();
}
@Override
public synchronized void delete(boolean deleteData) throws IOException {
String[] coverageNames = getGridCoverageNames();
for (String coverageName : coverageNames) {
removeCoverage(coverageName, deleteData, true);
}
// Dispose before deleting to make sure any lock on files or resources is released
dispose();
// drop the DB
granuleCatalog.drop();
// try to delete data
if (deleteData) {
// quick way: delete everything
final File[] list = parentDirectory.listFiles();
for (File file : list) {
FileUtils.deleteQuietly(file);
}
} else {
finalizeCleanup();
}
}
/**
* Finalize the clean up by removing any file returned by the cleanup filter. Note that some H2 .db files change their name during life cycle. So
* they won't be stored inside the fileset manager
*/
private void finalizeCleanup() {
IOFileFilter filesFilter = Utils.getCleanupFilter();
Collection<File> files = FileUtils.listFiles(parentDirectory, filesFilter, null);
for (File file : files) {
FileUtils.deleteQuietly(file);
}
}
@Override
public boolean removeCoverage(String coverageName)
throws IOException, UnsupportedOperationException {
return removeCoverage(coverageName, false);
}
/**
* This subclass of the {@link ImageMosaicWalker} cycles around a List of files and for each one calls the superclass handleFile() method. For
* each file is done a check if it really exists, it can be read and it is not a directory.
*
* @author Nicola Lagomarsini, GeoSolutions S.A.S.
*
*/
private static class ImageMosaicFileCollectionWalker extends ImageMosaicWalker {
/** Input File list to walk on */
private Collection<File> files;
public ImageMosaicFileCollectionWalker(ImageMosaicConfigHandler configHandler,
ImageMosaicEventHandlers eventHandler, Collection<File> files) {
super(configHandler, eventHandler);
this.files = files;
}
@Override
public void run() {
try {
// Initialization steps
configHandler.indexingPreamble();
startTransaction();
// Setting of the Collection size
setNumFiles(files.size());
// Creation of an Iterator on the input files
Iterator<File> it = files.iterator();
// Cycle on all the input files
while (it.hasNext()) {
File file = it.next();
// Stop the Harvesting if requested
if (getStop()) {
break;
}
// Check if the File has an absolute path
if (checkFile(file)) {
handleFile(file);
} else {
// SKIP and log
skipFile(file.getAbsolutePath());
}
}
// close transaction
if (getStop()) {
rollbackTransaction();
} else {
commitTransaction();
}
} catch (IOException e) {
// Exception Logged
LOGGER.log(Level.WARNING, e.getMessage(), e);
try {
// Rollback of the Transaction
rollbackTransaction();
} catch (IOException e1) {
throw new IllegalStateException(e);
}
} finally {
// close transaction
try {
closeTransaction();
} catch (Exception e) {
final String message = "Unable to close transaction" + e.getLocalizedMessage();
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, message, e);
}
// notify listeners
eventHandler.fireException(e);
}
// close indexing
try {
configHandler.indexingPostamble(!getStop());
} catch (Exception e) {
final String message = "Unable to close indexing" + e.getLocalizedMessage();
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, message, e);
}
// notify listeners
eventHandler.fireException(e);
}
}
}
}
@Override
public ResourceInfo getInfo(String coverageName) {
String name = checkUnspecifiedCoverage(coverageName);
RasterManager manager = getRasterManager(name);
String parentLocation = DataUtilities.fileToURL(parentDirectory).toString();
return new ImageMosaicFileResourceInfo(manager, parentLocation, this.locationAttributeName);
}
@Override
public ServiceInfo getInfo() {
IOFileFilter filesFilter = Utils.MOSAIC_SUPPORT_FILES_FILTER;
Collection<File> files = FileUtils.listFiles(parentDirectory, filesFilter, null);
List<FileGroup> fileGroups = new ArrayList<FileGroup>();
for (File file : files) {
fileGroups.add(new FileGroup(file, null, null));
}
return new DefaultFileServiceInfo(fileGroups);
}
public ExecutorService getMultiThreadedLoader() {
return multiThreadedLoader;
}
}