/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-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.
*/
/*
* NOTICE OF RELEASE TO THE PUBLIC DOMAIN
*
* This work was created by employees of the USDA Forest Service's
* Fire Science Lab for internal use. It is therefore ineligible for
* copyright under title 17, section 105 of the United States Code. You
* may treat it as you would treat any public domain work: it may be used,
* changed, copied, or redistributed, with or without permission of the
* authors, for free or for compensation. You may not claim exclusive
* ownership of this code because it is already owned by everyone. Use this
* software entirely at your own risk. No warranty of any kind is given.
*
* A copy of 17-USC-105 should have accompanied this distribution in the file
* 17USC105.html. If not, you may access the law via the US Government's
* public websites:
* - http://www.copyright.gov/title17/92chap1.html#105
* - http://www.gpoaccess.gov/uscode/ (enter "17USC105" in the search box.)
*/
package org.geotools.gce.geotiff;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.PlanarImage;
import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.TypeMap;
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.AbstractGridFormat;
import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffIIOMetadataDecoder;
import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffMetadata2CRSAdapter;
import org.geotools.data.DataSourceException;
import org.geotools.data.DataUtilities;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.NumberRange;
import org.opengis.coverage.ColorInterpretation;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridCoverageReader;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
/**
* this class is responsible for exposing the data and the Georeferencing
* metadata available to the Geotools library. This reader is heavily based on
* the capabilities provided by the ImageIO tools and JAI libraries.
*
*
* @author Bryce Nordgren, USDA Forest Service
* @author Simone Giannecchini
* @since 2.1
*
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/unsupported/geotiff_new/src/main/java/org/geotools/gce/geotiff/GeoTiffReader.java $
*/
public class GeoTiffReader extends AbstractGridCoverage2DReader implements
GridCoverageReader {
/** Logger for the {@link GeoTiffReader} class. */
private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(GeoTiffReader.class.toString());
/**
* Number of coverages for this reader is
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return 1;
}
/** Adapter for the GeoTiff crs. */
private GeoTiffMetadata2CRSAdapter gtcs;
private double noData = Double.NaN;
private RasterManager rasterManager;
URL sourceURL;
boolean expandMe;
RasterLayout[] overViewLayouts;
RasterLayout hrLayout;
ImageTypeSpecifier baseImageType;
File ovrSource;
ImageInputStreamSpi ovrInStreamSPI = null;
int extOvrImgChoice = -1;
@Override
public void dispose() {
super.dispose();
rasterManager.dispose();
}
/**
* 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;
}
/**
* Creates a new instance of GeoTiffReader
*
* @param input
* the GeoTiff file
* @throws DataSourceException
*/
public GeoTiffReader(Object input) throws DataSourceException {
this(input, new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE));
}
/**
* Creates a new instance of GeoTiffReader
*
* @param input
* the GeoTiff file
* @param uHints
* user-supplied hints TODO currently are unused
* @throws DataSourceException
*/
public GeoTiffReader(Object input, Hints uHints) throws DataSourceException {
super(input, uHints);
//
// Forcing longitude first since the geotiff specification seems to
// assume that we have first longitude the latitude.
//
if (uHints != null) {
// prevent the use from reordering axes
this.hints.remove(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER);
this.hints.add(new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.TRUE));
}
//
// Set the source being careful in case it is an URL pointing to a file
//
try {
// setting source
if (input instanceof URL) {
final URL sourceURL = (URL) input;
source = DataUtilities.urlToFile(sourceURL);
}
closeMe = true;
// /////////////////////////////////////////////////////////////////////
//
// Get a stream in order to read from it for getting the basic
// information for this coverage
//
// /////////////////////////////////////////////////////////////////////
if ((source instanceof InputStream) || (source instanceof ImageInputStream)) {
closeMe = false;
}
if (source instanceof ImageInputStream) {
inStream = (ImageInputStream) source;
} else {
inStream = ImageIO.createImageInputStream(source);
}
if (inStream == null) {
throw new IllegalArgumentException("No input stream for the provided source");
}
this.sourceURL = Utils.checkSource(source);
// /////////////////////////////////////////////////////////////////////
//
// Informations about multiple levels and such
//
// /////////////////////////////////////////////////////////////////////
checkForExternalOverviews();
getHRInfo(this.hints);
// /////////////////////////////////////////////////////////////////////
//
// Coverage name
//
// /////////////////////////////////////////////////////////////////////
coverageName = source instanceof File ? ((File) source).getName() : "geotiff_coverage";
final int dotIndex = coverageName.lastIndexOf('.');
if (dotIndex != -1 && dotIndex != coverageName.length()) {
coverageName = coverageName.substring(0, dotIndex);
}
// /////////////////////////////////////////////////////////////////////
//
// Freeing streams
//
// /////////////////////////////////////////////////////////////////////
if (closeMe) {
inStream.close();
}
} catch (IOException e) {
throw new DataSourceException(e);
} catch (TransformException e) {
throw new DataSourceException(e);
} catch (FactoryException e) {
throw new DataSourceException(e);
}
rasterManager = new RasterManager(this);
}
private void checkForExternalOverviews() {
if (!(source instanceof File)) {
return;
}
File src = (File) source;
ovrSource = new File(src.getParent(), src.getName() + ".ovr");
if (!ovrSource.exists()) {
return;
}
ovrInStreamSPI = ImageIOExt.getImageInputStreamSPI(ovrSource);
}
/**
*
* @param hints
* @throws IOException
* @throws FactoryException
* @throws TransformException
* @throws MismatchedDimensionException
* @throws DataSourceException
*/
private void getHRInfo(Hints hints) throws IOException, FactoryException, TransformException,
MismatchedDimensionException, DataSourceException {
// //
//
// Get a reader for this format
//
// //
final ImageReader reader = Utils.TIFFREADERFACTORY.createReaderInstance();
ImageReader ovrReader = null;
ImageInputStream ovrStream = null;
try {
// //
//
// get the METADATA
//
// //
reader.setInput(inStream);
final IIOMetadata iioMetadata = reader.getImageMetadata(0);
GeoTiffIIOMetadataDecoder metadata = null;
metadata = new GeoTiffIIOMetadataDecoder(iioMetadata);
gtcs = new GeoTiffMetadata2CRSAdapter(hints);
// //
//
// get the CRS INFO
//
// //
final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs = (CoordinateReferenceSystem) tempCRS;
if (LOGGER.isLoggable(Level.FINE))
LOGGER.log(Level.FINE, "Using forced coordinate reference system");
} else {
// check metadata first
if (metadata.hasGeoKey()&& gtcs != null)
crs = gtcs.createCoordinateSystem(metadata);
if (crs == null)
crs = Utils.getCRS(source);
}
if (crs == null){
if(LOGGER.isLoggable(Level.WARNING))
LOGGER.warning("Coordinate Reference System is not available");
crs = AbstractGridFormat.getDefaultCRS();
}
if (metadata.hasNoData())
noData = metadata.getNoData();
// //
//
// get the dimension of the hr image and build the model as well as
// computing the resolution
// //
numOverviews = reader.getNumImages(true) - 1;
final int hrWidth = reader.getWidth(0);
final int hrHeight = reader.getHeight(0);
final int hrTileW = reader.getTileWidth(0);
final int hrTileH = reader.getTileHeight(0);
hrLayout = new RasterLayout(0, 0, hrWidth, hrHeight, 0, 0, hrTileW, hrTileH);
final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight);
originalGridRange = new GridEnvelope2D(actualDim);
if (gtcs != null&& metadata!=null&& (metadata.hasModelTrasformation()||(metadata.hasPixelScales()&&metadata.hasTiePoints()))) {
this.raster2Model = GeoTiffMetadata2CRSAdapter.getRasterToModel(metadata);
} else {
this.raster2Model = Utils.parseWorldFile(source);
}
if (this.raster2Model == null) {
throw new DataSourceException("Raster to Model Transformation is not available");
}
final AffineTransform tempTransform = new AffineTransform((AffineTransform) raster2Model);
tempTransform.translate(-0.5, -0.5);
originalEnvelope = CRS.transform(ProjectiveTransform
.create(tempTransform), new GeneralEnvelope(actualDim));
originalEnvelope.setCoordinateReferenceSystem(crs);
// ///
//
// setting the higher resolution available for this coverage
//
// ///
highestRes = new double[2];
highestRes[0] = XAffineTransform.getScaleX0(tempTransform);
highestRes[1] = XAffineTransform.getScaleY0(tempTransform);
if (ovrInStreamSPI != null) {
ovrReader = Utils.TIFFREADERFACTORY.createReaderInstance();
ovrStream = ovrInStreamSPI.createInputStreamInstance(ovrSource,
ImageIO.getUseCache(), ImageIO.getCacheDirectory());
ovrReader.setInput(ovrStream);
// this includes the real image as this is a image index, we need to add one.
extOvrImgChoice = numOverviews + 1;
numOverviews = numOverviews + ovrReader.getNumImages(true);
if (numOverviews < extOvrImgChoice)
extOvrImgChoice = -1;
}
// //
//
// get information for the successive images
//
// //
if (numOverviews >= 1) {
overViewResolutions = new double[numOverviews][2];
overViewLayouts = new RasterLayout[numOverviews];
// Internal overviews start at 1, so lastInternalOverview matches numOverviews if no
// external.
int firstExternalOverview = extOvrImgChoice == -1 ? numOverviews : extOvrImgChoice - 1;
double spanRes0 = highestRes[0] * this.originalGridRange.getSpan(0);
double spanRes1 = highestRes[1] * this.originalGridRange.getSpan(1);
for (int i = 0; i < firstExternalOverview; i++) {
final int w = reader.getWidth(i + 1);
final int h = reader.getHeight(i + 1);
final int tw = reader.getTileWidth(i + 1);
final int th = reader.getTileHeight(i + 1);
overViewResolutions[i][0] = spanRes0 / w;
overViewResolutions[i][1] = spanRes1 / h;
overViewLayouts[i] = new RasterLayout(0, 0, w, h, 0, 0, tw, th);
}
for (int i = firstExternalOverview; i < numOverviews; i++) {
final int w = ovrReader.getWidth(i - firstExternalOverview);
final int h = ovrReader.getHeight(i - firstExternalOverview);
final int tw = ovrReader.getTileWidth(i - firstExternalOverview);
final int th = ovrReader.getTileHeight(i - firstExternalOverview);
overViewResolutions[i][0] = spanRes0 / w;
overViewResolutions[i][1] = spanRes1 / h;
overViewLayouts[i] = new RasterLayout(0, 0, w, h, 0, 0, tw, th);
}
} else {
overViewResolutions = null;
}
// get sample image
final ImageReadParam readParam = reader.getDefaultReadParam();
readParam.setSourceRegion(new Rectangle(0, 0, 4, 4));
final BufferedImage sampleImage = reader.read(0, readParam);
baseImageType = new ImageTypeSpecifier(sampleImage);
} catch (Throwable e) {
throw new DataSourceException(e);
} finally {
if (reader != null)
try {
reader.dispose();
} catch (Throwable t) {
}
if (ovrReader != null)
try {
ovrReader.dispose();
} catch (Throwable t) {
}
if (ovrStream != null)
try {
ovrStream.close();
} catch (Throwable t) {
}
if (inStream != null)
try {
inStream.reset();
} catch (Throwable t) {
}
}
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new GeoTiffFormat();
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
*/
@Override
public GridCoverage2D read(GeneralParameterValue[] params)
throws IOException {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Reading image from " + sourceURL.toString() + "\n"
+ "Highest res " + highestRes[0] + " " + highestRes[1]);
}
final Collection<GridCoverage2D> response = rasterManager.read(params);
if (response.isEmpty()) {
if (LOGGER.isLoggable(Level.FINE)){
LOGGER.fine("The response is empty. ==> returning a null GridCoverage");
}
return null;
} else {
return response.iterator().next();
}
}
/**
* Returns the geotiff metadata for this geotiff file.
*
* @return the metadata
*/
public GeoTiffIIOMetadataDecoder getMetadata() {
GeoTiffIIOMetadataDecoder metadata = null;
ImageReader reader = null;
boolean closeMe = true;
ImageInputStream stream = null;
try {
if ((source instanceof InputStream)|| (source instanceof ImageInputStream)){
closeMe = false;
}
if (source instanceof ImageInputStream ) {
stream =(ImageInputStream) source;
} else {
inStreamSPI = ImageIOExt.getImageInputStreamSPI(source);
if (inStreamSPI == null) {
throw new IllegalArgumentException("No input stream for the provided source");
}
stream = inStreamSPI.createInputStreamInstance(source, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
}
if (stream == null) {
throw new IllegalArgumentException("No input stream for the provided source");
}
stream.mark();
reader = Utils.TIFFREADERFACTORY.createReaderInstance();
reader.setInput(stream);
final IIOMetadata iioMetadata = reader.getImageMetadata(0);
metadata = new GeoTiffIIOMetadataDecoder(iioMetadata);
} catch (IOException e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
} finally {
if (reader != null)
try {
reader.dispose();
} catch (Throwable t) {
}
if (stream != null){
try {
stream.reset();
} catch (Throwable t) {
}
if (closeMe){
try {
stream.close();
} catch (Throwable t) {
}
}
}
}
return metadata;
}
/**
* Creates a {@link GridCoverage} for the provided {@link PlanarImage} using
* the {@link #raster2Model} that was provided for this coverage.
*
* <p>
* This method is vital when working with coverages that have a raster to
* model transformation that is not a simple scale and translate.
*
* @param image
* contains the data for the coverage to create.
* @param raster2Model
* is the {@link MathTransform} that maps from the raster space
* to the model space.
* @return a {@link GridCoverage}
* @throws IOException
*/
protected final GridCoverage2D createCoverage(PlanarImage image, MathTransform raster2Model) throws IOException {
// creating bands
final SampleModel sm = image.getSampleModel();
final ColorModel cm = image.getColorModel();
final int numBands = sm.getNumBands();
final GridSampleDimension[] bands = new GridSampleDimension[numBands];
// setting bands names.
Category noDataCategory = null;
final Map<String, Double> properties = new HashMap<String, Double>();
if (!Double.isNaN(noData)) {
noDataCategory = new Category(
Vocabulary.formatInternational(VocabularyKeys.NODATA),
new Color[] { new Color(0, 0, 0, 0) }, NumberRange.create(
noData, noData), NumberRange.create(noData, noData));
properties.put("GC_NODATA", new Double(noData));
}
Set<String> bandNames = new HashSet<String>();
for (int i = 0; i < numBands; i++) {
final ColorInterpretation colorInterpretation = TypeMap.getColorInterpretation(cm, i);
if (colorInterpretation == null) {
throw new IOException("Unrecognized sample dimension type");
}
Category[] categories = null;
if (noDataCategory != null) {
categories = new Category[] { noDataCategory };
}
String bandName = colorInterpretation.name();
// make sure we create no duplicate band names
if(colorInterpretation == ColorInterpretation.UNDEFINED || bandNames.contains(bandName)) {
bandName = "Band" + (i + 1);
}
bands[i] = new GridSampleDimension(bandName,categories,null).geophysics(true);
}
// creating coverage
if (raster2Model != null) {
return coverageFactory.create(coverageName, image, crs, raster2Model,
bands, null, null);
}
return coverageFactory.create(coverageName, image, new GeneralEnvelope(
originalEnvelope), bands, null, null);
}
/**
* 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 getNumberOfOverviews() {
return super.numOverviews;
}
String getName() {
return super.coverageName;
}
}