/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.gce.imagepyramid;
import it.geosolutions.imageio.maskband.DatasetLayout;
import java.awt.Rectangle;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.channels.Channels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.media.jai.ImageLayout;
import org.apache.commons.io.IOUtils;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.AbstractGridFormat;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.data.PrjFileReader;
import org.geotools.factory.Hints;
import org.geotools.gce.imagemosaic.ImageMosaicReader;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
/**
* This reader is responsible for providing access to a pyramid of mosaics of georeferenced
* coverages that are read directly through imageio readers, like tiff, pngs, etc...
*
* <p>
* Specifically this plugin relies on the image mosaic plugin to handle each single level of
* resolutions available, hence all the magic is done inside the mosaic plugin.
*
*
* <p>
* For information on how to build a mosaic, please refer to the {@link ImageMosaicReader}
* documentation.
*
* <p>
* If you are looking for information on how to create a pyramid, here you go.
*
* The pyramid itself does no magic. All the magic is performed by the single mosaic readers that
* are polled depending on the requeste resolution levels. Therefore the <b>first step</b> is having
* a mosaic of images like geotiff, tiff, jpeg, or png which is going to be the base for the
* pyramid.
*
* <p>
* The <b>second step</b> is to build the next (lower resolution) levels for the pyramid. <br>
* If you look inside the spike dire of the geotools project you will find a (growing) set of tools
* that can be used for doing processing on coverages. <br>
* Specifically there is one tool called PyramidBuilder that can be used to build the pyramid level
* by level.
*
* <p>
* <b>Last step</b> is providing a prj file with the projection of the pyramid (btw all the levels
* has to be in the same projection) as well as a properties file with this structure:
*
* <pre>
* #
* #Mon Aug 21 22:23:27 CEST 2006
* #name of the coverage
* Name=ikonos
* #different resolution levels available
* Levels=1.2218682749859724E-5,9.220132503102996E-6 2.4428817977683634E-5,1.844026500620314E-5 4.8840552865873626E-5,3.686350299024973E-5 9.781791400307775E-5,7.372700598049946E-5 1.956358280061555E-4,1.4786360643866836E-4 3.901787184256844E-4,2.9572721287731037E-4
* #where all the levels reside
* LevelsDirs=0 2 4 8 16 32
* #number of levels availaible
* LevelsNum=6
* #envelope for this pyramid
* Envelope2D=13.398228477973406,43.591366397808976 13.537912459169803,43.67121274528585
* </pre>
*
* <p>
* Starting with 16.x ImagePyramid can now support ImageMosaics with inner overviews.
* See {@link ImageLevelsMapper} for additional details of the Levels entry of
* a pyramid of mosaics with inner overviews.
*
* @author Simone Giannecchini
* @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for
* jar:file:foo.jar/bar.properties like URLs
* @since 2.3
*
*
*
* @source $URL$
*/
public final class ImagePyramidReader extends AbstractGridCoverage2DReader implements
GridCoverageReader {
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(ImagePyramidReader.class.toString());
/**
* The input properties file to read the pyramid information from.
*/
private URL sourceURL;
private String[] coverageNames;
private int count = 1;
private ImageLevelsMapper imageLevelsMapper;
/**
* Constructor for an {@link ImagePyramidReader}.
*
* @param source The source object.
* @param uHints {@link Hints} to control the behaviour of this reader.
* @throws IOException
* @throws UnsupportedEncodingException
*
*/
public ImagePyramidReader(Object source, Hints uHints) throws IOException {
// //
//
// managing hints
//
// //
if (uHints == null) {
this.hints = new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,Boolean.TRUE);
} else {
this.hints = uHints;
}
this.coverageFactory = CoverageFactoryFinder.getGridCoverageFactory(this.hints);
// Check source
if (source == null) {
throw new DataSourceException(
"ImagePyramidReader:null source set to read this coverage.");
}
this.source = source;
this.sourceURL = Utils.checkSource(source, uHints);
if (sourceURL == null) {
throw new DataSourceException(
"This plugin accepts only a URL, a File or a String pointing to a directory with a structure similar to the one of gdal_retile!");
}
// get the crs if able to
final URL prjURL = DataUtilities.changeUrlExt(sourceURL, "prj");
PrjFileReader crsReader = null;
try {
crsReader = new PrjFileReader(Channels.newChannel(prjURL.openStream()));
} catch (FactoryException e) {
throw new DataSourceException(e);
} finally {
try {
crsReader.close();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
}
}
final Object tempCRS = hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs = (CoordinateReferenceSystem) tempCRS;
LOGGER.log(Level.WARNING, "Using forced coordinate reference system " + crs.toWKT());
} else {
final CoordinateReferenceSystem tempcrs = crsReader.getCoordinateReferenceSystem();
if (tempcrs == null) {
// use the default crs
crs = AbstractGridFormat.getDefaultCRS();
LOGGER.log(
Level.WARNING,
"Unable to find a CRS for this coverage, using a default one: "
+ crs.toWKT());
} else
crs = tempcrs;
}
// Load properties file with information about levels and envelope
parseMainFile(sourceURL);
}
/**
* Parses the main properties file loading the information regarding geographic extent and
* overviews.
*
* @param sourceFile
* @throws IOException
* @throws FileNotFoundException
*/
private void parseMainFile(final URL sourceURL) throws IOException {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Parsing pyramid properties file at:" + sourceURL.toExternalForm());
}
BufferedInputStream propertyStream = null;
InputStream openStream = null;
try {
openStream = sourceURL.openStream();
propertyStream = new BufferedInputStream(openStream);
final Properties properties = new Properties();
properties.load(propertyStream);
// load the envelope
final String envelope = properties.getProperty("Envelope2D");
String[] pairs = envelope.split(" ");
final double cornersV[][] = new double[2][2];
String pair[];
for (int i = 0; i < 2; i++) {
pair = pairs[i].split(",");
cornersV[i][0] = Double.parseDouble(pair[0]);
cornersV[i][1] = Double.parseDouble(pair[1]);
}
this.originalEnvelope = new GeneralEnvelope(cornersV[0], cornersV[1]);
this.originalEnvelope.setCoordinateReferenceSystem(crs);
imageLevelsMapper = new ImageLevelsMapper(properties);
numOverviews = imageLevelsMapper.getNumOverviews();
overViewResolutions = imageLevelsMapper.getOverViewResolutions();
highestRes = imageLevelsMapper.getHighestResolution();
// name
coverageName = properties.getProperty("Name");
if (coverageName != null) {
if (coverageName.contains(",")) {
coverageNames = coverageName.split(",");
coverageName = coverageNames[0];
} else {
coverageNames = new String[] { coverageName };
}
count = coverageNames.length;
}
// original gridrange (estimated)
originalGridRange = new GridEnvelope2D(new Rectangle((int) Math.round(originalEnvelope
.getSpan(0) / highestRes[0]), (int) Math.round(originalEnvelope.getSpan(1)
/ highestRes[1])));
final GridToEnvelopeMapper geMapper = new GridToEnvelopeMapper(originalGridRange,
originalEnvelope);
geMapper.setPixelAnchor(PixelInCell.CELL_CORNER);
raster2Model = geMapper.createTransform();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Parsed pyramid properties file at:" + sourceURL.toExternalForm());
}
} finally {
// close input stream
if (propertyStream != null)
IOUtils.closeQuietly(propertyStream);
if (openStream != null)
IOUtils.closeQuietly(openStream);
}
}
/**
* Constructor for an {@link ImagePyramidReader}.
*
* @param source The source object.
* @throws IOException
* @throws UnsupportedEncodingException
*
*/
public ImagePyramidReader(Object source) throws IOException {
this(source, null);
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new ImagePyramidFormat();
}
@Override
public GridCoverage2D read(GeneralParameterValue[] params) throws IOException {
return read(coverageName, params);
}
@Override
public GridEnvelope getOriginalGridRange(String coverageName) {
return getFirstLevelReader(coverageName, false).getOriginalGridRange(
getReaderCoverageName(coverageName));
}
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem(String coverageName) {
return getFirstLevelReader(coverageName, false).getCoordinateReferenceSystem(
getReaderCoverageName(coverageName));
}
@Override
public GeneralEnvelope getOriginalEnvelope(String coverageName) {
return getFirstLevelReader(coverageName, false).getOriginalEnvelope(
getReaderCoverageName(coverageName));
}
@Override
public MathTransform getOriginalGridToWorld(String coverageName, PixelInCell pixInCell) {
return getFirstLevelReader(coverageName, false).getOriginalGridToWorld(
getReaderCoverageName(coverageName), pixInCell);
}
@Override
public Set<ParameterDescriptor<List>> getDynamicParameters(String coverageName) {
return getFirstLevelReader(coverageName, false).getDynamicParameters(
getReaderCoverageName(coverageName));
}
@Override
public int getNumOverviews(String coverageName) {
return getFirstLevelReader(coverageName, false).getNumOverviews(
getReaderCoverageName(coverageName));
}
@Override
public DatasetLayout getDatasetLayout(String coverageName) {
return getFirstLevelReader(coverageName, false).getDatasetLayout(
getReaderCoverageName(coverageName));
}
@Override
public ImageLayout getImageLayout(String coverageName) throws IOException {
return getFirstLevelReader(coverageName, false).getImageLayout(
getReaderCoverageName(coverageName));
}
@Override
public double[][] getResolutionLevels(String coverageName) throws IOException {
return getFirstLevelReader(coverageName, false).getResolutionLevels(
getReaderCoverageName(coverageName));
}
@Override
public GridCoverage2D read(final String coverageName, GeneralParameterValue[] params)
throws IOException {
GeneralEnvelope requestedEnvelope = null;
Rectangle dim = null;
OverviewPolicy overviewPolicy = null;
if (params != null) {
// /////////////////////////////////////////////////////////////////////
//
// Checking params
//
// /////////////////////////////////////////////////////////////////////
if (params != null) {
for (int i = 0; i < params.length; i++) {
@SuppressWarnings("rawtypes")
final ParameterValue param = (ParameterValue) params[i];
if (param == null) {
continue;
}
final String name = param.getDescriptor().getName().getCode();
if (name.equals(AbstractGridFormat.READ_GRIDGEOMETRY2D.getName().toString())) {
final GridGeometry2D gg = (GridGeometry2D) param.getValue();
requestedEnvelope = new GeneralEnvelope((Envelope) gg.getEnvelope2D());
dim = gg.getGridRange2D().getBounds();
continue;
}
if (name.equals(AbstractGridFormat.OVERVIEW_POLICY.getName().toString())) {
overviewPolicy = (OverviewPolicy) param.getValue();
continue;
}
}
}
}
//
// Loading tiles
//
return loadRequestedTiles(coverageName, requestedEnvelope, dim, params, overviewPolicy);
}
/**
* This method loads the tiles which overlap the requested envelope using the provided values
* for alpha and input ROI.
*
* @param coverageName
* @param requestedEnvelope
* @param dim
* @param params
* @param overviewPolicy
* @return A {@link GridCoverage}, well actually a {@link GridCoverage2D}.
* @throws TransformException
* @throws IOException
*/
private GridCoverage2D loadRequestedTiles(final String coverageName,
GeneralEnvelope requestedEnvelope, Rectangle dim, GeneralParameterValue[] params,
OverviewPolicy overviewPolicy) throws IOException {
// if we get here we have something to load
// compute the requested resolution
final ImageReadParam readP = new ImageReadParam();
Integer imageChoice = 0;
if (dim != null) {
try {
imageChoice = setReadParams(overviewPolicy, readP, requestedEnvelope, dim);
} catch (TransformException e) {
throw new DataSourceException(e);
}
}
// Check to have the needed reader in memory
// light check to see if this reader had been disposed, not synch-ing for performance.
if (!imageLevelsMapper.hasReaders()) {
throw new IllegalStateException("This ImagePyramidReader has already been disposed");
}
ImageMosaicReader reader = getImageMosaicReaderForLevel(coverageName, imageChoice);
//
// Abusing of the created ImageMosaicreader for getting a
// gridcoverage2d, then rename it
//
GridCoverage2D mosaicCoverage = reader.read(getReaderCoverageName(coverageName), params);
if (mosaicCoverage != null) {
return new GridCoverage2D(coverageName, mosaicCoverage);
} else {
// the mosaic can still return null in corner cases, handle that gracefully
return null;
}
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#dispose()
*/
@Override
public synchronized void dispose() {
super.dispose();
imageLevelsMapper.dispose();
}
@Override
public String[] getGridCoverageNames() {
return coverageNames;
}
/**
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return count;
}
/**
* Retrieve meta data value from requested coverage and for requested metadata
*
* @param coverageName
* @param name
* @return
*/
@Override
public String getMetadataValue(final String coverageName, final String name) {
return getImageMosaicMetadataValue(coverageName, name);
}
/**
* Retrieve data value for requested metadata
*/
public String getMetadataValue(final String name) {
return getImageMosaicMetadataValue(coverageName, name);
}
private ImageMosaicReader getFirstLevelReader(String coverageName) {
return getFirstLevelReader(coverageName, false);
}
private ImageMosaicReader getFirstLevelReader(String coverageName, boolean canBeNull) {
ImageMosaicReader reader = null;
try {
reader = getImageMosaicReaderForLevel(coverageName, 0);
} catch (IOException e) {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, "Could not get reader for datasource.", e);
}
if (reader == null && !canBeNull){
throw new IllegalArgumentException(
"Could not get reader for the specified coverageName: " + coverageName, e);
}
}
return reader;
}
private String getImageMosaicMetadataValue(final String coverageName, final String name) {
ImageMosaicReader firstLevelReader = getFirstLevelReader(coverageName);
if (firstLevelReader == null) {
return null;
}
if (name.equalsIgnoreCase(HAS_TIME_DOMAIN)) {
return String.valueOf(this.hasTimeDomain(coverageName, firstLevelReader));
}
if (TIME_DOMAIN.equalsIgnoreCase(name) || TIME_DOMAIN_MAXIMUM.equalsIgnoreCase(name)
|| TIME_DOMAIN_MINIMUM.equalsIgnoreCase(name)) {
if (this.hasTimeDomain(coverageName, firstLevelReader)) {
return this.getTimeDomain(coverageName, firstLevelReader, name);
}
}
return firstLevelReader.getMetadataValue(getReaderCoverageName(coverageName), name);
}
@Override
public String[] getMetadataNames(final String coverageName) {
// Looks like the metadaNames are fixed on pyramid
return getMetadataNames();
}
public String[] getMetadataNames() {
final String[] parentNames = super.getMetadataNames();
final List<String> metadataNames = new ArrayList<String>();
metadataNames.add(TIME_DOMAIN);
metadataNames.add(HAS_TIME_DOMAIN);
metadataNames.add(TIME_DOMAIN_MINIMUM);
metadataNames.add(TIME_DOMAIN_MAXIMUM);
metadataNames.add(TIME_DOMAIN_RESOLUTION);
if (parentNames != null)
metadataNames.addAll(Arrays.asList(parentNames));
return metadataNames.toArray(new String[metadataNames.size()]);
}
/**
* Retrieve time domains metadata values for the requested ImageMosaicReader
*
* @param reader
* @param name
* @return
*/
private String getTimeDomain(final String coverageName, ImageMosaicReader reader,
final String name) {
if (hasTimeDomain(coverageName, reader) && reader != null) {
return reader.getMetadataValue(getReaderCoverageName(coverageName), name);
}
return null;
}
/**
* Verify if the requested Mosaic has a time domain configuration
*
* @param reader
* @return True if has time domain configuration
*/
private boolean hasTimeDomain(final String coverageName, ImageMosaicReader reader) {
if (reader != null) {
String strHasTimeDomain = reader.getMetadataValue(getReaderCoverageName(coverageName),
HAS_TIME_DOMAIN);
return Boolean.parseBoolean(strHasTimeDomain);
}
return false;
}
/**
* Retrieve the ImageMosaicReader for the requested Level and load if necessary
*
* @return ImageMosaicReader for level
* */
public ImageMosaicReader getImageMosaicReaderForLevel(Integer imageChoice)
throws IOException {
return getImageMosaicReaderForLevel(coverageName, imageChoice);
}
/**
* Retrieve the ImageMosaicReader for the requested Level and load if necessary
*
* @return ImageMosaicReader for level
* */
public ImageMosaicReader getImageMosaicReaderForLevel(String coverageName, Integer imageChoice)
throws IOException {
return imageLevelsMapper.getReader(imageChoice, coverageName, sourceURL, hints);
}
private String getReaderCoverageName(String coverageName) {
// When dealing with a single coverage, ImagePyramid exposes a coverageName
// whilst the underlying ImageMosaic may have a different one
return count > 1 ? coverageName : ImageMosaicReader.UNSPECIFIED;
}
}