/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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.arcgrid;
import it.geosolutions.imageio.plugins.arcgrid.AsciiGridsImageMetadata;
import it.geosolutions.imageio.plugins.arcgrid.spi.AsciiGridsImageReaderSpi;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.FileCacheImageInputStream;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.measure.unit.Unit;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
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.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.GridCoverage2DReader;
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.geometry.GeneralEnvelope;
import org.geotools.image.io.ImageIOExt;
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.geometry.Envelope;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import com.vividsolutions.jts.io.InStream;
/**
* This class can read an arc grid data source (ArcGrid or GRASS ASCII) and
* create a {@link GridCoverage2D} from the data.
*
* @author Daniele Romagnoli, GeoSolutions
* @author Simone Giannecchini, GeoSolutions
* @since 2.3.x
*
*
* @source $URL$
*/
public final class ArcGridReader extends AbstractGridCoverage2DReader implements
GridCoverage2DReader {
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gce.arcgrid");
/** Caches and ImageReaderSpi for an AsciiGridsImageReader. */
private final static ImageReaderSpi readerSPI = new AsciiGridsImageReaderSpi();
/** No data value for this dataset. */
private double inNoData = Double.NaN;
/**
* Creates a new instance of an ArcGridReader basing the decision on whether
* the file is compressed or not. I assume nothing about file extension.
*
* @param input
* Source object for which we want to build an ArcGridReader.
* @throws DataSourceException
*/
public ArcGridReader(Object input) throws DataSourceException {
this(input, null);
}
/**
* Creates a new instance of an ArcGridReader basing the decision on whether
* the file is compressed or not. I assume nothing about file extension.
*
* @param input
* Source object for which we want to build an ArcGridReader.
* @param hints
* Hints to be used by this reader throughout his life.
* @throws DataSourceException
*/
public ArcGridReader(Object input, final Hints hints)
throws DataSourceException {
super(input,hints);
//
// Checking input
//
coverageName = "AsciiGrid";
try {
//
// Source management
//
checkSource(input,hints);
//
// CRS
//
final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs=(CoordinateReferenceSystem) tempCRS;
LOGGER.log(Level.WARNING,"Using default coordinate reference system ");
} else {
initCoordinateReferenceSystem();
}
//
// Reader and metadata
//
// //
//
// Getting a reader for this format
//
// //
final ImageReader reader = readerSPI.createReaderInstance();
reader.setInput(inStream);
//
// Envelope and other metadata
//
parseMetadata(reader);
//
// Informations about multiple levels and such
//
getResolutionInfo(reader);
// release the stream if we can.
finalStreamPreparation();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)){
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
throw new DataSourceException(e);
}
}
/**
* Close the {@link InStream} {@link ImageInputStream} if we open it up on
* purpose to read header info for this {@link AbstractGridCoverage2DReader}.
* If the stream cannot be closed, we just reset and mark it.
*
* @throws IOException
*/
private void finalStreamPreparation() throws IOException {
if (closeMe)
inStream.close();
else {
inStream.reset();
inStream.mark();
}
}
/**
* Checks the input provided to this {@link ArcGridReader} and sets all the
* other objects and flags accordingly.
*
* @param input
* provided to this {@link ArcGridReader}.
* @param hints
* Hints to be used by this reader throughout his life.
*
* @throws UnsupportedEncodingException
* @throws DataSourceException
* @throws IOException
* @throws FileNotFoundException
*/
private void checkSource(Object input, final Hints hints)
throws UnsupportedEncodingException, DataSourceException,
IOException, FileNotFoundException {
closeMe = true;
// //
//
// URL to File
//
// //
// if it is a URL pointing to a File I convert it to a file,
// otherwise, later on, I will try to get an inputstream out of it.
if (input instanceof URL) {
// URL that point to a file
final URL sourceURL = ((URL) input);
if (sourceURL.getProtocol().compareToIgnoreCase("file") == 0) {
this.source = input = DataUtilities.urlToFile(sourceURL);
}
}
// //
//
// File
//
// //
if (input instanceof File) {
final File sourceFile = (File) input;
if (!sourceFile.exists() || sourceFile.isDirectory()
|| !sourceFile.canRead())
throw new DataSourceException("Provided file does not exist or is a directory or is not readable!");
this.coverageName = sourceFile.getName();
final int dotIndex = coverageName.indexOf(".");
gzipped = coverageName.toLowerCase().endsWith("gz");
coverageName = (dotIndex == -1) ? coverageName : coverageName
.substring(0, dotIndex);
if(gzipped)
inStream = ImageIO.createImageInputStream(new GZIPInputStream(
new FileInputStream(sourceFile)));
else{
inStreamSPI= ImageIOExt.getImageInputStreamSPI(sourceFile);
if (inStreamSPI == null)
throw new DataSourceException(
"No input stream for the provided source");
inStream = inStreamSPI.createInputStreamInstance(sourceFile, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
}
} else
// //
//
// URL
//
// //
if (input instanceof URL) {
final URL tempURL = ((URL) input);
input = tempURL.openConnection().getInputStream();
GZIPInputStream gzInStream = null;
try {
gzInStream = new GZIPInputStream((InputStream) input);
gzipped = false;
} catch (Exception e) {
gzipped = false;
}
input = tempURL.openConnection().getInputStream();
inStream = gzipped ? ImageIO.createImageInputStream(gzInStream)
: ImageIO.createImageInputStream(tempURL.openConnection()
.getInputStream());
} else
// //
//
// InputStream
//
// //
if (input instanceof InputStream) {
closeMe = false;
if (ImageIO.getUseCache())
inStream = new FileCacheImageInputStream((InputStream) input,
null);
else
inStream = new MemoryCacheImageInputStream((InputStream) input);
// let's mark it
inStream.mark();
} else
// //
//
// ImageInputStream
//
// //
if (input instanceof ImageInputStream) {
closeMe = false;
inStream = (ImageInputStream) input;
inStream.mark();
} else
throw new IllegalArgumentException("Unsupported input type");
if (inStream == null)
throw new DataSourceException(
"No input stream for the provided source");
}
/**
* Gets resolution information about the coverage itself.
*
* @param reader
* an {@link ImageReader} to use for getting the resolution
* information.
* @throws IOException
* @throws TransformException
*/
private void getResolutionInfo(ImageReader reader) throws IOException,
TransformException {
// //
//
// get the dimension of the hr image and build the model as well as
// computing the resolution
// //
final Rectangle actualDim = new Rectangle(0, 0, reader.getWidth(0),reader.getHeight(0));
originalGridRange = new GridEnvelope2D(actualDim);
// ///
//
// setting the higher resolution avalaible for this coverage
//
// ///
highestRes = getResolution(originalEnvelope, actualDim, crs);
}
/**
* @see org.opengis.coverage.grid.GridCoverageReader#getFormat()
*/
public Format getFormat() {
return new ArcGridFormat();
}
/**
* Reads a {@link GridCoverage2D} possibly matching as close as possible the
* resolution computed by using the input params provided by using the
* parameters for this {@link #read(GeneralParameterValue[])}.
*
* <p>
* To have an idea about the possible read parameters take a look at
* {@link AbstractGridFormat} class and {@link ArcGridFormat} class.
*
* @param params
* an array of {@link GeneralParameterValue} containing the
* parameters to control this read process.
*
* @return a {@link GridCoverage2D}.
*
* @see AbstractGridFormat
* @see ArcGridFormat
* @see org.opengis.coverage.grid.GridCoverageReader#read(org.opengis.parameter.GeneralParameterValue[])
*/
@SuppressWarnings("unchecked")
public GridCoverage2D read(GeneralParameterValue[] params)
throws IllegalArgumentException, IOException {
GeneralEnvelope readEnvelope = null;
Rectangle requestedDim = null;
OverviewPolicy overviewPolicy=null;
if (params != null) {
final int length = params.length;
for (int i = 0; i < 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();
readEnvelope = new GeneralEnvelope((Envelope) gg.getEnvelope2D());
requestedDim = gg.getGridRange2D().getBounds();
continue;
}
if (name.equals(AbstractGridFormat.OVERVIEW_POLICY.getName().toString())) {
overviewPolicy=(OverviewPolicy) param.getValue();
}
}
}
return createCoverage(readEnvelope, requestedDim, overviewPolicy);
}
/**
* This method creates the GridCoverage2D from the underlying file.
*
* @param requestedDim
* @param readEnvelope
*
*
* @return a GridCoverage
*
* @throws java.io.IOException
*/
private GridCoverage2D createCoverage(GeneralEnvelope requestedEnvelope,
Rectangle requestedDim, OverviewPolicy overviewPolicy) throws IOException {
if (!closeMe) {
inStream.reset();
inStream.mark();
}
// /////////////////////////////////////////////////////////////////////
//
// Doing an image read for reading the coverage.
//
// /////////////////////////////////////////////////////////////////////
// //
//
// Setting subsampling factors with some checkings
// 1) the subsampling factors cannot be zero
// 2) the subsampling factors cannot be such that the w or h are zero
//
// //
final ImageReadParam readP = new ImageReadParam();
final Integer imageChoice;
try {
imageChoice = setReadParams(overviewPolicy, readP,
requestedEnvelope, requestedDim);
} catch (IOException e) {
if (LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
} catch (TransformException e) {
if (LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
}
// //
//
// image and metadata
//
// //
final ParameterBlock pbjImageRead = new ParameterBlock();
// prepare input to handle possible parallelism between different
// readers
if (source instanceof File) {
if (!gzipped){
if(inStreamSPI!=null)
pbjImageRead.add(inStreamSPI.createInputStreamInstance(source, ImageIO.getUseCache(), ImageIO.getCacheDirectory() ));
else
pbjImageRead.add(ImageIO.createImageInputStream(source));
}
else
pbjImageRead.add(ImageIO
.createImageInputStream(new GZIPInputStream(
new FileInputStream((File) source))));
} else if (source instanceof ImageInputStream
|| source instanceof InputStream)
pbjImageRead.add(inStream);
else if (source instanceof URL) {
if (gzipped)
ImageIO.createImageInputStream(new GZIPInputStream(
((URL) source).openConnection().getInputStream()));
else
pbjImageRead.add(ImageIO.createImageInputStream(((URL) source)
.openConnection().getInputStream()));
}
pbjImageRead.add(imageChoice);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(null);
pbjImageRead.add(null);
pbjImageRead.add(readP);
pbjImageRead.add(readerSPI.createReaderInstance());
final RenderedOp asciiCoverage = JAI.create("ImageRead", pbjImageRead,hints);
// //
//
// Creating the coverage
//
// //
try {
// //
//
// Categories
//
//
// //
Unit<?> uom = null;
final Category nan;
if (Double.isNaN(inNoData)) {
nan = new Category(Vocabulary
.formatInternational(VocabularyKeys.NODATA), new Color(
0, 0, 0, 0), 0);
} else {
nan = new Category(Vocabulary
.formatInternational(VocabularyKeys.NODATA),
new Color[] { new Color(0, 0, 0, 0) },
NumberRange.create(0, 0),
NumberRange.create(inNoData, inNoData));
}
//
// Sample dimension
//
final SampleModel sm = asciiCoverage.getSampleModel();
final ColorModel cm = asciiCoverage.getColorModel();
final ColorInterpretation colorInterpretation=TypeMap.getColorInterpretation(cm, 0);
if(colorInterpretation==null)
throw new IOException("Unrecognized sample dimension type");
final GridSampleDimension band = new GridSampleDimension(
coverageName, new Category[] { nan }, uom).geophysics(true);
final Map<String, Double> properties = new HashMap<String, Double>();
properties.put("GC_NODATA", new Double(inNoData));
//
// Coverage
//
return coverageFactory.create(
coverageName,
asciiCoverage,
originalEnvelope,
new GridSampleDimension[] { band },
null,
properties);
} catch (NoSuchElementException e) {
if (LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
throw new DataSourceException(e);
}
}
/**
* This method is responsible for building up an envelope according to the
* definition of the crs. It assumes that X coordinate on the ascii grid
* itself maps to longitude and y coordinate maps to latitude.
*
* @param reader
* The {@link ImageReader} to parse.
*
* @throws MismatchedDimensionException
*/
private void parseMetadata(ImageReader reader)
throws Exception {
// parse and set layout
setLayout(reader);
//
// Getting metadata
//
final Object metadata = reader.getImageMetadata(0);
if (!(metadata instanceof AsciiGridsImageMetadata)){
throw new DataSourceException(
"Unexpected error! Metadata are not of the expected class.");
}
// casting the metadata
final AsciiGridsImageMetadata gridMetadata = (AsciiGridsImageMetadata) metadata;
// getting metadata
final Node root = gridMetadata
.getAsTree("it.geosolutions.imageio.plugins.arcgrid.AsciiGridsImageMetadata_1.0");
// getting Grid Properties
Node child = root.getFirstChild();
NamedNodeMap attributes = child.getAttributes();
final boolean grass = attributes.getNamedItem("GRASS").getNodeValue().equalsIgnoreCase("True");
// getting Grid Properties
child = child.getNextSibling();
attributes = child.getAttributes();
final int hrWidth = Integer.parseInt(attributes.getNamedItem("nColumns").getNodeValue());
final int hrHeight = Integer.parseInt(attributes.getNamedItem("nRows").getNodeValue());
originalGridRange = new GridEnvelope2D(new Rectangle(0, 0, hrWidth,hrHeight));
final boolean pixelIsArea = AsciiGridsImageMetadata.RasterSpaceType.valueOf(attributes.getNamedItem("rasterSpaceType").getNodeValue()).equals(AsciiGridsImageMetadata.RasterSpaceType.PixelIsArea);
if (!grass) {
inNoData = Double.parseDouble(attributes.getNamedItem("noDataValue").getNodeValue());
}
// getting Envelope Properties
child = child.getNextSibling();
attributes = child.getAttributes();
final double cellsizeX = Double.parseDouble(attributes.getNamedItem(
"cellsizeX").getNodeValue());
final double cellsizeY = Double.parseDouble(attributes.getNamedItem(
"cellsizeY").getNodeValue());
double xll = Double.parseDouble(attributes.getNamedItem("xll")
.getNodeValue());
double yll = Double.parseDouble(attributes.getNamedItem("yll")
.getNodeValue());
//
// OGC specifications says that PixelIsArea map a pixel to the corner
// of the grid while PixelIsPoint map a pixel to the centre of the grid.
//
if (!pixelIsArea) {
final double correctionX = cellsizeX / 2d;
final double correctionY = cellsizeY / 2d;
xll -= correctionX;
yll -= correctionY;
}
originalEnvelope = new GeneralEnvelope(new double[] { xll, yll },
new double[] { xll + (hrWidth * cellsizeX),
yll + (hrHeight * cellsizeY) });
// setting the coordinate reference system for the envelope
originalEnvelope.setCoordinateReferenceSystem(crs);
}
/**
* Gets the coordinate system that will be associated to the
* {@link GridCoverage}. The WGS84 coordinate system is used by default. It
* is worth to point out that when reading from a stream which is not
* connected to a file, like from an http connection (e.g. from a WCS) we
* cannot rely on receiving a prj file too. In this case the exchange of
* information about referencing should proceed the exchange of data thus I
* rely on this and I ask the user who's invoking the read operation to
* provide me a valid crs and envelope through read parameters.
*
* @throws FactoryException
* @throws IOException
* @throws FileNotFoundException
*/
private void initCoordinateReferenceSystem() throws FileNotFoundException,
IOException {
// check to see if there is a projection file
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
sourceAsString = ((URL) source).getFile();
int index = sourceAsString.lastIndexOf(".");
final StringBuffer prjFileName;
if (index == -1)
prjFileName = new StringBuffer(sourceAsString);
else
prjFileName = new StringBuffer(sourceAsString.substring(0,
index));
prjFileName.append(".prj");
// does it exist?
final File prjFile = new File(prjFileName.toString());
if (prjFile.exists()) {
// it exists then we have top read it
PrjFileReader projReader = null;
try {
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.SEVERE, 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.SEVERE, 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.SEVERE, 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);
}
}
}
}
if (crs == null) {
crs = AbstractGridFormat.getDefaultCRS();
LOGGER.info( "Unable to find crs, continuing with default CRS" );
}
}
/**
* Number of coverages for this reader is 1
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return 1;
}
}