/*
* 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 it.geosolutions.imageioimpl.plugins.tiff.TIFFImageReaderSpi;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.channels.FileChannel;
import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.PlanarImage;
import org.geotools.coverage.Category;
import org.geotools.coverage.CoverageFactoryFinder;
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.imageio.geotiff.GeoTiffIIOMetadataDecoder;
import org.geotools.coverage.grid.io.imageio.geotiff.GeoTiffMetadata2CRSAdapter;
import org.geotools.data.DataSourceException;
import org.geotools.data.PrjFileReader;
import org.geotools.data.WorldFileReader;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
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/branches/2.7.x/build/maven/javadoc/../../../modules/unsupported/geotiff_new/src/main/java/org/geotools/gce/geotiff/GeoTiffReader.java $
*/
@SuppressWarnings("deprecation")
public final class GeoTiffReader extends AbstractGridCoverage2DReader implements
GridCoverageReader {
/**
* Number of coverages for this reader is 1
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return 1;
}
/** Logger for the {@link GeoTiffReader} class. */
private Logger LOGGER = org.geotools.util.logging.Logging.getLogger(GeoTiffReader.class.toString());
/** SPI for creating tiff readers in ImageIO tools */
private final static TIFFImageReaderSpi readerSPI = new TIFFImageReaderSpi();
/** Decoder for the GeoTiff metadata. */
private GeoTiffIIOMetadataDecoder metadata;
/** Adapter for the GeoTiff crs. */
private GeoTiffMetadata2CRSAdapter gtcs;
private double noData = Double.NaN;
private RasterManager rasterManager;
URL sourceURL;
boolean expandMe;
RasterLayout[] overViewLayouts;
RasterLayout hrLayout;
@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 {
// /////////////////////////////////////////////////////////////////////
//
// Forcing longitude first since the geotiff specification seems to
// assume that we have first longitude the latitude.
//
// /////////////////////////////////////////////////////////////////////
if (hints == null)
this.hints= new Hints();
if (uHints != null) {
// prevent the use from reordering axes
uHints.remove(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER);
this.hints.add(uHints);
this.hints.add(new Hints(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER,Boolean.TRUE));
}
this.coverageFactory= CoverageFactoryFinder.getGridCoverageFactory(this.hints);
coverageName = "geotiff_coverage";
//
// Seting input
//
if (input == null) {
final IOException ex = new IOException("GeoTiffReader:No source set to read this coverage.");
throw new DataSourceException(ex);
}
//
// Set the source being careful in case it is an URL pointing to a file
//
try {
this.source = input;
// setting source
if (input instanceof URL) {
final URL sourceURL = (URL) input;
if (sourceURL.getProtocol().equalsIgnoreCase("http")
|| sourceURL.getProtocol().equalsIgnoreCase("ftp")) {
try {
source = sourceURL.openStream();
} catch (IOException e) {
new RuntimeException(e);
}
} else if (sourceURL.getProtocol().equalsIgnoreCase("file"))
source = new File(URLDecoder.decode(sourceURL.getFile(),
"UTF-8"));
}
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
//
// /////////////////////////////////////////////////////////////////////
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);
}
/**
*
* @param hints
* @throws IOException
* @throws FactoryException
* @throws GeoTiffException
* @throws TransformException
* @throws MismatchedDimensionException
* @throws DataSourceException
*/
private void getHRInfo(Hints hints) throws IOException, FactoryException,
GeoTiffException, TransformException, MismatchedDimensionException,
DataSourceException {
// //
//
// Get a reader for this format
//
// //
final ImageReader reader = readerSPI.createReaderInstance();
// //
//
// get the METADATA
//
// //
reader.setInput(inStream);
final IIOMetadata iioMetadata = reader.getImageMetadata(0);
CoordinateReferenceSystem foundCrs = null;
boolean useWorldFile = false;
try{
metadata = new GeoTiffIIOMetadataDecoder(iioMetadata);
gtcs = (GeoTiffMetadata2CRSAdapter) GeoTiffMetadata2CRSAdapter.get(hints);
if (gtcs != null)
foundCrs = gtcs.createCoordinateSystem(metadata);
else
useWorldFile = true;
if (metadata.hasNoData())
noData = metadata.getNoData();
}catch (IllegalArgumentException iae){
useWorldFile = true;
}catch (UnsupportedOperationException uoe){
useWorldFile = true;
}
// //
//
// get the CRS INFO
//
// //
final Object tempCRS = this.hints
.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs = (CoordinateReferenceSystem) tempCRS;
LOGGER.log(Level.WARNING, new StringBuilder(
"Using forced coordinate reference system ").append(
crs.toWKT()).toString());
} else{
if (useWorldFile)
foundCrs = getCRS(source);
crs = foundCrs;
}
if (crs == null){
throw new DataSourceException ("Coordinate Reference System is not available");
}
// //
//
// 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 (!useWorldFile && gtcs != null) {
this.raster2Model = gtcs.getRasterToModel(metadata);
}
else {
this.raster2Model = 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);
// //
//
// get information for the successive images
//
// //
if (numOverviews >= 1) {
overViewResolutions = new double[numOverviews][2];
overViewLayouts= new RasterLayout[numOverviews];
for (int i = 0; i < numOverviews; i++) {
//getting raster layout
final int width=reader.getWidth(i+1);
final int height=reader.getHeight(i+1);
final int tileW=reader.getTileWidth(i+1);
final int tileH=reader.getTileHeight(i+1);
overViewLayouts[i]= new RasterLayout(0,0,width,height,0,0,tileW,tileH);
//computing resolutions
overViewResolutions[i][0] = (highestRes[0]*hrWidth)/width;
overViewResolutions[i][1] = (highestRes[1]*hrHeight)/height;
}
} else
overViewResolutions = null;
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new GeoTiffFormat();
}
// /**
// * This method reads in the TIFF image, constructs an appropriate CRS,
// * determines the math transform from raster to the CRS model, and
// * constructs a GridCoverage.
// *
// * @param params
// * currently ignored, potentially may be used for hints.
// *
// * @return grid coverage represented by the image
// *
// * @throws IOException
// * on any IO related troubles
// */
// public GridCoverage read(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++) {
// final ParameterValue param = (ParameterValue) params[i];
// 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;
// }
// }
// }
// }
// // /////////////////////////////////////////////////////////////////////
// //
// // set params
// //
// // /////////////////////////////////////////////////////////////////////
// Integer imageChoice = new Integer(0);
// final ImageReadParam readP = new ImageReadParam();
// try {
// imageChoice = setReadParams(overviewPolicy, readP,
// requestedEnvelope, dim);
// } catch (TransformException e) {
// new DataSourceException(e);
// }
//
// // /////////////////////////////////////////////////////////////////////
// //
// // IMAGE READ OPERATION
// //
// // /////////////////////////////////////////////////////////////////////
//// final ImageReader reader = readerSPI.createReaderInstance();
//// final ImageInputStream inStream = ImageIO
//// .createImageInputStream(source);
//// reader.setInput(inStream);
// final Hints newHints = (Hints) hints.clone();
//// if (!reader.isImageTiled(imageChoice.intValue())) {
//// final Dimension tileSize = ImageUtilities.toTileSize(new Dimension(
//// reader.getWidth(imageChoice.intValue()), reader
//// .getHeight(imageChoice.intValue())));
//// final ImageLayout layout = new ImageLayout();
//// layout.setTileGridXOffset(0);
//// layout.setTileGridYOffset(0);
//// layout.setTileHeight(tileSize.height);
//// layout.setTileWidth(tileSize.width);
//// newHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
//// }
//// inStream.close();
//// reader.reset();
// final ParameterBlock pbjRead = new ParameterBlock();
// pbjRead.add(ImageIO.createImageInputStream(source));
// pbjRead.add(imageChoice);
// pbjRead.add(Boolean.FALSE);
// pbjRead.add(Boolean.FALSE);
// pbjRead.add(Boolean.FALSE);
// pbjRead.add(null);
// pbjRead.add(null);
// pbjRead.add(readP);
// pbjRead.add( readerSPI.createReaderInstance());
// final RenderedOp coverageRaster=JAI.create("ImageRead", pbjRead,
// (RenderingHints) newHints);
//
// // /////////////////////////////////////////////////////////////////////
// //
// // BUILDING COVERAGE
// //
// // /////////////////////////////////////////////////////////////////////
// // I need to calculate a new transformation (raster2Model)
// // between the cropped image and the required
// // adjustedRequestEnvelope
// final int ssWidth = coverageRaster.getWidth();
// final int ssHeight = coverageRaster.getHeight();
// if (LOGGER.isLoggable(Level.FINE)) {
// LOGGER.log(Level.FINE, "Coverage read: width = " + ssWidth
// + " height = " + ssHeight);
// }
//
// // //
// //
// // setting new coefficients to define a new affineTransformation
// // to be applied to the grid to world transformation
// // -----------------------------------------------------------------------------------
// //
// // With respect to the original envelope, the obtained planarImage
// // needs to be rescaled. The scaling factors are computed as the
// // ratio between the cropped source region sizes and the read
// // image sizes.
// //
// // //
// final double scaleX = originalGridRange.getLength(0) / (1.0 * ssWidth);
// final double scaleY = originalGridRange.getLength(1) / (1.0 * ssHeight);
// final AffineTransform tempRaster2Model = new AffineTransform((AffineTransform) raster2Model);
// tempRaster2Model.concatenate(new AffineTransform(scaleX, 0, 0, scaleY, 0, 0));
// return createCoverage(coverageRaster, ProjectiveTransform.create((AffineTransform) tempRaster2Model));
//
//
// }
/**
* @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());
LOGGER.fine(new StringBuffer("Highest res ").append(highestRes[0])
.append(" ").append(highestRes[1]).toString());
}
final Collection<GridCoverage2D> response = rasterManager.read(params);
if(response.isEmpty())
throw new DataSourceException("Unable to create a coverage for this request ");
else
return response.iterator().next();
}
/**
* Returns the geotiff metadata for this geotiff file.
*
* @return the metadata
*/
public GeoTiffIIOMetadataDecoder getMetadata() {
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 GridCoverage 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;
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));
}
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};
bands[i] = new GridSampleDimension(colorInterpretation.name(),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);
}
private CoordinateReferenceSystem getCRS(Object source) {
CoordinateReferenceSystem crs = null;
if (source instanceof File
|| (source instanceof URL && (((URL) source).getProtocol() == "file"))) {
// getting name for the prj file
final String sourceAsString;
if (source instanceof File) {
sourceAsString = ((File) source).getAbsolutePath();
} else {
String auth = ((URL) source).getAuthority();
String path = ((URL) source).getPath();
if (auth != null && !auth.equals("")) {
sourceAsString = "//" + auth + path;
} else {
sourceAsString = path;
}
}
final int index = sourceAsString.lastIndexOf(".");
final StringBuilder base = new StringBuilder(sourceAsString
.substring(0, index)).append(".prj");
// does it exist?
final File prjFile = new File(base.toString());
if (prjFile.exists()) {
// it exists then we have top read it
PrjFileReader projReader = null;
try {
final FileChannel channel = new FileInputStream(prjFile)
.getChannel();
projReader = new PrjFileReader(channel);
crs = projReader.getCoordinateReferenceSystem();
} catch (FileNotFoundException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} catch (IOException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} catch (FactoryException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER.log(Level.INFO, e.getLocalizedMessage(), e);
} finally {
if (projReader != null)
try {
projReader.close();
} catch (IOException e) {
// warn about the error but proceed, it is not fatal
// we have at least the default crs to use
LOGGER
.log(Level.SEVERE, e.getLocalizedMessage(),
e);
}
}
}
}
return crs;
}
/**
* @throws IOException
*/
static MathTransform parseWorldFile(Object source) throws IOException {
MathTransform raster2Model = null;
// TODO: Add support for FileImageInputStreamExt
// TODO: Check for WorldFile on URL beside the actual connection.
if (source instanceof File) {
final File sourceFile = ((File) source);
String parentPath = sourceFile.getParent();
String filename = sourceFile.getName();
final int i = filename.lastIndexOf('.');
filename = (i == -1) ? filename : filename.substring(0, i);
// getting name and extension
final String base = (parentPath != null) ? new StringBuilder(
parentPath).append(File.separator).append(filename)
.toString() : filename;
// We can now construct the baseURL from this string.
File file2Parse = new File(new StringBuilder(base).append(".wld")
.toString());
if (file2Parse.exists()) {
final WorldFileReader reader = new WorldFileReader(file2Parse);
raster2Model = reader.getTransform();
} else {
// looking for another extension
file2Parse = new File(new StringBuilder(base).append(".tfw")
.toString());
if (file2Parse.exists()) {
// parse world file
final WorldFileReader reader = new WorldFileReader(
file2Parse);
raster2Model = reader.getTransform();
}
}
}
return raster2Model;
}
/**
* 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;
}
}