/*
* 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.image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.renderable.ParameterBlock;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.channels.FileChannel;
import java.util.Iterator;
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.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
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.PrjFileReader;
import org.geotools.data.WorldFileReader;
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.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.image.ImageUtilities;
import org.opengis.coverage.grid.Format;
import org.opengis.coverage.grid.GridCoverageReader;
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.NoSuchAuthorityCodeException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.TransformException;
/**
* Reads a GridCoverage from a given source. WorldImage sources only support one
* GridCoverage so hasMoreGridCoverages() will return true until the only
* GridCoverage is read. No metadata is currently supported, so all related
* methods return null. In the early future we will start (hopefully supporting
* them).
*
* @author simone giannecchini
* @author alessio fabiani
* @author rgould
*
*
* @source $URL$
*/
public final class WorldImageReader extends AbstractGridCoverage2DReader
implements GridCoverageReader {
/** Logger. */
private Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gce.image");
private boolean wmsRequest;
private boolean metaFile;
private String parentPath;
private String extension;
private ImageReaderSpi readerSPI;
/**
* Class constructor. Construct a new ImageWorldReader to read a
* GridCoverage from the source object. The source must point to the raster
* file itself, not the world file. If the source is a Java URL it checks if
* it is ponting to a file and if so it converts the url into a file.
*
* @param input
* The source of a GridCoverage, can be a File, a URL or an input
* stream.
* @throws DataSourceException
*/
public WorldImageReader(Object input) throws DataSourceException {
this(input, null);
}
/**
* Class constructor. Construct a new ImageWorldReader to read a
* GridCoverage from the source object. The source must point to the raster
* file itself, not the world file. If the source is a Java URL it checks if
* it is ponting to a file and if so it converts the url into a file.
*
* @param input
* The source of a GridCoverage, can be a File, a URL or an input
* stream.
* @throws DataSourceException
*/
public WorldImageReader(Object input,final Hints hints) throws DataSourceException {
// /////////////////////////////////////////////////////////////////////
//
// Checking input
//
// /////////////////////////////////////////////////////////////////////
if (input == null) {
final IOException ex = new IOException(
"WorldImage:No source set to read this coverage.");
LOGGER.logp(Level.SEVERE, WorldImageReader.class.toString(),
"WorldImageReader", ex.getLocalizedMessage(), ex);
throw new DataSourceException(ex);
}
this.source=input;
// //
//
// managing hints
//
// //
if (this.hints == null)
this.hints= new Hints();
if (hints != null) {
this.hints.add(hints);
}
// GridCoverageFactory initialization
if (this.hints.containsKey(Hints.GRID_COVERAGE_FACTORY)) {
final Object factory = this.hints.get(Hints.GRID_COVERAGE_FACTORY);
if (factory != null && factory instanceof GridCoverageFactory) {
this.coverageFactory = (GridCoverageFactory) factory;
}
}
if (this.coverageFactory == null) {
this.coverageFactory = CoverageFactoryFinder.getGridCoverageFactory(this.hints);
}
coverageName = "image_coverage";
try {
boolean closeMe = true;
// /////////////////////////////////////////////////////////////////////
//
// Source management
//
// /////////////////////////////////////////////////////////////////////
if (input instanceof URL) {
// URL that point to a file
final URL sourceURL = ((URL) input);
if (sourceURL.getProtocol().compareToIgnoreCase("file") == 0) {
String auth = sourceURL.getAuthority();
String path = sourceURL.getPath();
if (auth != null && !auth.equals("")) {
path = "//"+auth+path;
}
this.source = input = new File(URLDecoder.decode(path, "UTF-8"));
} else if (sourceURL.getProtocol().equalsIgnoreCase("http")) {
// // getting a stream to the reader
// this.source = sourceURL.openStream();
// /////////////////////////////////////////////////////////////////////
//
// WMS Request? I want to be able to handle that case too
//
// /////////////////////////////////////////////////////////////////////
wmsRequest = WMSRequest(input);
}
}
// //
//
// Name, path, etc...
//
// //
if (input instanceof File) {
final File sourceFile = (File) input;
final String filename = sourceFile.getName();
final int i = filename.lastIndexOf('.');
final int length = filename.length();
if (i > 0 && i < length - 1) {
extension = filename.substring(i + 1).toLowerCase();
}
this.parentPath = sourceFile.getParent();
this.coverageName = filename;
final int dotIndex = coverageName.lastIndexOf(".");
coverageName = (dotIndex == -1) ? coverageName : coverageName
.substring(0, dotIndex);
} else if (input instanceof URL)
input = ((URL) input).openStream();
// //
//
// Get a stream in order to read from it for getting the basic
// information for this coverfage
//
// //
if (input instanceof ImageInputStream){
closeMe = false;
}else{
inStreamSPI= ImageIOExt.getImageInputStreamSPI(source);
if (inStreamSPI == null)
throw new DataSourceException(
"No input stream for the provided source");
inStream = inStreamSPI.createInputStreamInstance( this.source, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
}
if (inStream == null)
throw new IllegalArgumentException(
"No input stream for the provided source");
// /////////////////////////////////////////////////////////////////////
//
// CRS
//
// /////////////////////////////////////////////////////////////////////
if (!wmsRequest) {
final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
if (tempCRS != null) {
this.crs = (CoordinateReferenceSystem) tempCRS;
LOGGER.log(Level.WARNING, "Using forced coordinate reference system ");
} else
readCRS();
}
// /////////////////////////////////////////////////////////////////////
//
// Informations about multiple levels and such
//
// /////////////////////////////////////////////////////////////////////
getHRInfo();
// release the stream
if (closeMe)
inStream.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
throw new DataSourceException(e);
} catch (TransformException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
throw new DataSourceException(e);
}
}
/**
* Gets the relevant information for the underlying raster.
*
* @throws IOException
* @throws TransformException
*/
private void getHRInfo() throws IOException, TransformException {
// //
//
// Get a reader for this format
// TODO optimize this using image file extension when possible
//
// //
final Iterator<ImageReader> it = ImageIO.getImageReaders(inStream);
if (!it.hasNext())
throw new DataSourceException("No reader avalaible for this source");
final ImageReader reader = (ImageReader) it.next();
readerSPI = reader.getOriginatingProvider();
reader.setInput(inStream);
// //
//
// get the dimension of the hr image and build the model as well as
// computing the resolution
// //
numOverviews = wmsRequest ? 0 : reader.getNumImages(true) - 1;
int hrWidth = reader.getWidth(0);
int hrHeight = reader.getHeight(0);
final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight);
originalGridRange = new GridEnvelope2D(actualDim);
// /////////////////////////////////////////////////////////////////////
//
// Envelope, coverage name and other resolution information
//
// /////////////////////////////////////////////////////////////////////
if (source instanceof File) {
prepareWorldImageGridToWorldTransform();
// //
//
// In case we read from a real world file we have toget the envelope
//
// //
if (!metaFile) {
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);
highestRes = new double[2];
highestRes[0]=XAffineTransform.getScaleX0(tempTransform);
highestRes[1]=XAffineTransform.getScaleY0(tempTransform);
}
else
{
// ///
//
// setting the higher resolution available for this coverage
//
// ///
highestRes = getResolution(originalEnvelope, actualDim, crs);
final GridToEnvelopeMapper mapper = new GridToEnvelopeMapper(originalGridRange, originalEnvelope);
mapper.setPixelAnchor(PixelInCell.CELL_CENTER);
this.raster2Model=mapper.createTransform();
}
}
// //
//
// get information for the overviews in case ony exists
//
// //
if (numOverviews >=1) {
overViewResolutions = new double[numOverviews][2];
for (int i = 0; i < numOverviews; i++) {
overViewResolutions[i][0] = (highestRes[0]*this.originalGridRange.getSpan(0))/reader.getWidth(i+1);
overViewResolutions[i][1] = (highestRes[1]*this.originalGridRange.getSpan(1))/reader.getHeight(i+1);
}
} else
overViewResolutions = null;
}
/**
* Returns the format that this Reader accepts.
*
* @return a new WorldImageFormat class
*/
public Format getFormat() {
return new WorldImageFormat();
}
/**
* Reads an image from a source stream. Loads an image from a source stream,
* then loads the values from the world file and constructs a new
* GridCoverage from this information. When reading from a remote stream we
* do not look for a world fiel but we suppose those information comes from
* a different way (xml, gml, pigeon?)
*
* @param params
* WorldImageReader supports no parameters, it just ignores them.
*
* @return a new GridCoverage read from the source.
*
* @throws IllegalArgumentException
* DOCUMENT ME!
* @throws IOException
* DOCUMENT ME!
*/
public GridCoverage2D read(GeneralParameterValue[] params)
throws IllegalArgumentException, IOException {
// /////////////////////////////////////////////////////////////////////
//
// do we have parameters to use for reading from the specified source
//
// /////////////////////////////////////////////////////////////////////
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();
if (!wmsRequest) {
try {
imageChoice = setReadParams(overviewPolicy,readP, requestedEnvelope, dim);
} catch (TransformException e) {
new DataSourceException(e);
}
}
// /////////////////////////////////////////////////////////////////////
//
// Reading the source layer
//
// /////////////////////////////////////////////////////////////////////
// final ImageReader reader = readerSPI.createReaderInstance();
// final ImageInputStream inStream = wmsRequest ? ImageIO
// .createImageInputStream(((URL) source).openStream()) : ImageIO
// .createImageInputStream(source);
//
final Hints newHints = (Hints) hints.clone();
// if (!wmsRequest) {
// reader.setInput(inStream);
// 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();
final ParameterBlock pbjRead = new ParameterBlock();
pbjRead.add(inStreamSPI!=null?inStreamSPI.createInputStreamInstance(source, ImageIO.getUseCache(), ImageIO.getCacheDirectory()):ImageIO.createImageInputStream(source));
// pbjRead.add(wmsRequest ? ImageIO
// .createImageInputStream(((URL) source).openStream()) : 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.getSpan(0) / (1.0 * ssWidth);
final double scaleY = originalGridRange.getSpan(1) / (1.0 * ssHeight);
final AffineTransform tempRaster2Model = new AffineTransform((AffineTransform) raster2Model);
tempRaster2Model.concatenate(new AffineTransform(scaleX, 0, 0, scaleY, 0, 0));
return createImageCoverage(coverageRaster, ProjectiveTransform.create((AffineTransform) tempRaster2Model));
}
/**
* This method is used to check if we are connecting directly to a WMS with
* a getmap request. In such a case we skip reading all the parameters we
* can read from this http string.
*
* @param input
*
* @return true if we are dealing with a WMS request, false otherwise.
*/
private boolean WMSRequest(Object input) {
// TODO do we need the requested envelope?
if (input instanceof URL
&& (((URL) input).getProtocol().equalsIgnoreCase("http"))) {
try {
// getting the query
final String query = java.net.URLDecoder.decode(((URL) input)
.getQuery().intern(), "UTF-8");
// should we proceed? Let's look for a getmap WMS request
if (query.intern().indexOf("GetMap") == -1) {
return false;
}
// tokenizer on $
final String[] pairs = query.split("&");
// parse each pair
final int numPairs = pairs.length;
String[] kvp = null;
for (int i = 0; i < numPairs; i++) {
// splitting the pairs
kvp = pairs[i].split("=");
// checking the fields
// BBOX
if (kvp[0].equalsIgnoreCase("BBOX")) {
// splitting fields
kvp = kvp[1].split(",");
originalEnvelope = new GeneralEnvelope(new double[] {
Double.parseDouble(kvp[0]),
Double.parseDouble(kvp[1]) }, new double[] {
Double.parseDouble(kvp[2]),
Double.parseDouble(kvp[3]) });
}
// SRS
if (kvp[0].equalsIgnoreCase("SRS")) {
crs = CRS.decode(kvp[1], true);
}
// layers
if (kvp[0].equalsIgnoreCase("layers")) {
this.coverageName = kvp[1].replaceAll(",", "_");
}
}
} catch (IOException e) {
// TODO how to handle this?
return false;
} catch (NoSuchAuthorityCodeException e) {
// TODO how to handle this?
return false;
} catch (MismatchedDimensionException e) {
// TODO how to handle this?
return false;
} catch (IndexOutOfBoundsException e) {
// TODO how to handle this?
return false;
} catch (FactoryException e) {
// TODO how to handle this?
return false;
}
return true;
}
return false;
}
/**
* This method is responsible for reading the CRS whhther a projection file
* is provided. If no projection file is provided the second choice is the
* CRS supplied via the crs paramter. If even this one is not avalaible we
* default to EPSG:4326.
*
* @throws IOException
*/
private void readCRS() throws 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 {
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 StringBuffer base = new StringBuffer(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);
}
}
}
}
if (crs == null) {
crs = AbstractGridFormat.getDefaultCRS();
LOGGER.info("Unable to find crs, continuing with default CRS");
}
}
/**
* This method is in charge for reading the metadata file and for creating a
* valid envelope (whether possible);
*
* TODO it would be great to having a centralized management for the world
* file
*
* @throws IOException
*/
private void prepareWorldImageGridToWorldTransform() throws IOException {
// getting name and extension
final String base = (parentPath != null) ? new StringBuffer(
this.parentPath).append(File.separator).append(coverageName)
.toString() : coverageName;
// We can now construct the baseURL from this string.
File file2Parse = new File(new StringBuffer(base).append(".wld")
.toString());
if (file2Parse.exists()) {
final WorldFileReader reader = new WorldFileReader(file2Parse);
raster2Model = reader.getTransform();
} else {
// looking for another extension
final Set<String> ext=WorldImageFormat.getWorldExtension(extension);
final Iterator<String> it=ext.iterator();
if(!it.hasNext())
throw new DataSourceException("Unable to parse extension "+extension);
do{
file2Parse = new File(new StringBuffer(base).append((String)it.next()
).toString());
}while(!file2Parse.exists()&&it.hasNext());
if (file2Parse.exists()) {
// parse world file
final WorldFileReader reader = new WorldFileReader(file2Parse);
raster2Model = reader.getTransform();
metaFile = false;
} else {
// looking for a meta file
file2Parse = new File(new StringBuffer(base).append(".meta")
.toString());
if (file2Parse.exists()) {
parseMetaFile(file2Parse);
metaFile = true;
} else {
final IOException ex = new IOException(
"No file with meta information found!");
LOGGER
.logp(
Level.SEVERE,
WorldImageReader.class.toString(),
"private void prepareWorldImage2Model() throws IOException",
ex.getLocalizedMessage(), ex);
throw ex;
}
}
}
}
/**
* This method is responsible for parsing a META file which is nothing more
* than another format of a WorldFile used by the GIDB database.
*
* @param file2Parse
*
*
* @throws NumberFormatException
* @throws IOException
*
* @task move me to a separate implementation
*/
private void parseMetaFile(File file2Parse) throws NumberFormatException,
IOException {
double xMin = 0.0;
double yMax = 0.0;
double xMax = 0.0;
double yMin = 0.0;
// getting a buffered reader
final BufferedReader in = new BufferedReader(new FileReader(file2Parse));
// parsing the lines
String str = null;
int index = 0;
double value = 0;
while ((str = in.readLine()) != null) {
switch (index) {
case 1:
value = Double.parseDouble(str.substring("Origin Longitude = "
.intern().length()));
xMin = value;
break;
case 2:
value = Double.parseDouble(str.substring("Origin Latitude = "
.intern().length()));
yMin = value;
break;
case 3:
value = Double.parseDouble(str.substring("Corner Longitude = "
.intern().length()));
xMax = value;
break;
case 4:
value = Double.parseDouble(str.substring("Corner Latitude = "
.intern().length()));
yMax = value;
break;
default:
break;
}
index++;
}
in.close();
// building up envelope of this coverage
originalEnvelope = new GeneralEnvelope(new double[] { xMin, yMin },
new double[] { xMax, yMax });
originalEnvelope.setCoordinateReferenceSystem(crs);
}
/**
* Number of coverages for this reader is 1
*
* @return the number of coverages for this reader.
*/
@Override
public int getGridCoverageCount() {
return 1;
}
/**
* Returns the file extension of the image.
*
* @since 2.7
*/
public String getExtension() {
return extension;
}
}