/*
* 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.
*/
package org.geotools.gce;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.ROIShape;
import org.geotools.coverage.grid.RasterLayout;
import org.geotools.coverage.grid.io.imageio.ImageReaderSource;
import org.geotools.data.DataUtilities;
import org.geotools.gce.geotiff.GeoTiffUtils;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.geometry.XRectangle2D;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
/**
* A RasterDescriptor is an elementar piece of data image, with its own
* levels and everything.
*
* <p>
* This class is responsible for caching the various size of the different
* levels of each single rasterGranuleLoader.
*
* <p>
* Right now we are making the assumption that a single rasterGranuleLoader is
* made a by a single file with embedded levels, either explicit or intrinsic
* through wavelets like MrSID, ECW or JPEG2000.
*
* @author Simone Giannecchini, GeoSolutions S.A.S.
* @since 2.5.5
*
* @source $URL$
*/
public class RasterDescriptor {
/** Logger. */
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(RasterDescriptor.class);
/**
* This class represent an overview level in a single rasterGranuleLoader.
*
* @author Simone Giannecchini, GeoSolutions S.A.S.
*
*/
public class RasterLevelDescriptor {
final ImageReaderSource source;
final double scaleX;
final double scaleY;
final AffineTransform2D baseToLevelTransform;
final AffineTransform2D gridToWorldTransformCorner;
final RasterLayout rasterDimensions;
public ImageReaderSource getSource() {
return source;
}
public AffineTransform getBaseToLevelTransform() {
return baseToLevelTransform;
}
public double getScaleX() {
return scaleX;
}
public double getScaleY() {
return scaleY;
}
public RasterLevelDescriptor(
final ImageReaderSource source,
final double scaleX,
final double scaleY,
final RasterLayout rasterLayout) {
this.source=source;
this.scaleX = scaleX;
this.scaleY = scaleY;
this.baseToLevelTransform = new AffineTransform2D(XAffineTransform.getScaleInstance(scaleX, scaleY, 0, 0));
final AffineTransform gridToWorldTransform_ = new AffineTransform(baseToLevelTransform);
gridToWorldTransform_.preConcatenate(CoverageUtilities.CENTER_TO_CORNER);
gridToWorldTransform_.preConcatenate(baseGridToWorld);
this.gridToWorldTransformCorner = new AffineTransform2D(gridToWorldTransform_);
this.rasterDimensions = (RasterLayout) rasterLayout.clone();
}
public Rectangle getBounds() {
return (Rectangle) rasterDimensions.clone();
}
public AffineTransform2D getGridToWorldTransformCorner() {
return gridToWorldTransformCorner;
}
@Override
public String toString() {
return "RasterLevelDescriptor [source=" + source + ", scaleX=" + scaleX + ", scaleY="
+ scaleY + ", baseToLevelTransform=" + baseToLevelTransform
+ ", gridToWorldTransformCorner=" + gridToWorldTransformCorner
+ ", rasterDimensions=" + rasterDimensions + "]";
}
}
/**
* Simple placeholder class to store the result of a Granule Loading
* which comprises of a raster as well as a {@link ROIShape} for its footprint.
*
* @author Daniele Romagnoli, GeoSolutions S.A.S.
*
*/
static class RasterLoadingResult {
RenderedImage raster;
AffineTransform gridToWorld;
public AffineTransform getGridToWorld() {
return gridToWorld;
}
public RenderedImage getRaster() {
return raster;
}
RasterLoadingResult(RenderedImage raster, final AffineTransform gridToWorld) {
this.raster = raster;
this.gridToWorld = gridToWorld;
}
}
ReferencedEnvelope rasterBBOX;
final List<RasterLevelDescriptor> levels =new ArrayList<RasterLevelDescriptor>();
AffineTransform baseGridToWorld;
//
// public RasterDescriptor(RasterManager rasterManager) {
//
// // get basic info
// this.rasterBBOX = new ReferencedEnvelope(rasterManager.spatialDomainManager.coverageBBox);
// this.baseGridToWorld=new AffineTransform((AffineTransform) rasterManager.spatialDomainManager.coverageGridToWorld2D);
// final File granuleFile = DataUtilities.urlToFile(rasterManager.parent.sourceURL);
//
// // create the base grid to world transformation
// ImageInputStream inStream = null;
// ImageReader reader = null;
// try {
// //
// // LOAD INFO FROM MAIN FILE
// //
//
// // get a stream
// inStream = ImageIOExt.createImageInputStream(granuleFile);
// if (inStream == null) {
// throw new IllegalArgumentException(
// "Unable to get an input stream for the provided file "
// + granuleFile.toString());
// }
//
// // get a reader
// reader = GeoTiffUtils.TIFFREADERFACTORY.createReaderInstance();
// reader.setInput(inStream);
//
// // cache stream SPI
// ImageInputStreamSpi streamSPI=ImageIOExt.getImageInputStreamSPI(granuleFile);
//
// // load info from main sourceFile
// int numRasters=reader.getNumImages(true);
// int i=0;
// int baseLevelWidth=-1, baseLevelHeight=-1;
// for(;i<numRasters;i++){
// final int width=reader.getWidth(i);
// final int height=reader.getHeight(i);
// // add the base level
// if(i==0) {
// baseLevelWidth=width;
// baseLevelHeight=height;
// this.levels.add(
// new RasterLevelDescriptor(
// ImageReaderSource.wrapFile(i,granuleFile, streamSPI, GeoTiffUtils.TIFFREADERFACTORY),
// 1,
// 1,
// new RasterLayout(0, 0, width, height,reader.getTileGridXOffset(i),reader.getTileGridYOffset(i),reader.getTileWidth(i),reader.getTileHeight(i))
// )
// );
// }
// else {
// final double scaleX = baseLevelWidth / (1.0 * width);
// final double scaleY = baseLevelHeight / (1.0 * height);
// // add the base level
// this.levels.add(
// new RasterLevelDescriptor(
// ImageReaderSource.wrapFile(i,granuleFile, streamSPI, GeoTiffUtils.TIFFREADERFACTORY),
// scaleX,
// scaleY,
// new RasterLayout(0, 0, width, height,reader.getTileGridXOffset(i),reader.getTileGridYOffset(i),reader.getTileWidth(i),reader.getTileHeight(i))
// )
// );
// }
//
// }
//
// //
// // EXTERNAL Overviews management
// //
// if (rasterManager.parent.extOvrImgChoice >= 0 ) {
//
// // close current stream and reopen new one
// try {
// if (inStream != null) {
// inStream.close();
// }
// } catch (Throwable e) {
//
// }
//
// try {
// if (reader != null)
// reader.dispose();
// } catch (Throwable e) {
//
// }
//
// // get a stream
// inStream = rasterManager.parent.ovrInStreamSPI.createInputStreamInstance(rasterManager.parent.ovrSource, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
// if (inStream == null) {
// throw new IllegalArgumentException(
// "Unable to get an input stream for the provided file "
// + granuleFile.toString());
// }
// streamSPI=ImageIOExt.getImageInputStreamSPI(rasterManager.parent.ovrSource);
// // get a reader
// reader = GeoTiffUtils.TIFFREADERFACTORY.createReaderInstance();
// reader.setInput(inStream);
//
// // load info from main sourceFile
// numRasters=reader.getNumImages(true);
// for(int k=0;k<numRasters;k++,i++){
// final int width=reader.getWidth(k);
// final int height=reader.getHeight(k);
// final double scaleX = baseLevelWidth / (1.0 * width);
// final double scaleY = baseLevelHeight / (1.0 * height);
// // add the level
// this.levels.add(
// new RasterLevelDescriptor(
// ImageReaderSource.wrapFile(k,rasterManager.parent.ovrSource, rasterManager.parent.ovrInStreamSPI, GeoTiffUtils.TIFFREADERFACTORY),
// scaleX,
// scaleY,
// new RasterLayout(0, 0, width, height,reader.getTileGridXOffset(k),reader.getTileGridYOffset(k),reader.getTileWidth(k),reader.getTileHeight(k))
// )
// );
// }
//
//
// }
//
//
// } catch (IllegalStateException e) {
// throw new IllegalArgumentException(e);
//
// } catch (IOException e) {
// throw new IllegalArgumentException(e);
// } finally {
// try {
// if (inStream != null) {
// inStream.close();
// }
// } catch (Throwable e) {
// throw new IllegalArgumentException(e);
// } finally {
// if (reader != null)
// reader.dispose();
// }
// }
// }
public RasterDescriptor(final List<RasterSlice> slices) {
final RasterSlice fullRes= slices.get(0);
// get basic info
this.rasterBBOX = new ReferencedEnvelope(new ReferencedEnvelope(fullRes.envelope));
this.baseGridToWorld = new AffineTransform((AffineTransform) fullRes.gridToWorld.clone());
this.levels.add(new RasterLevelDescriptor(fullRes.source, 1.0, 1.0, fullRes.rasterDimensions));
final int baseLevelWidth=fullRes.rasterDimensions.getWidth(), baseLevelHeight=fullRes.rasterDimensions.getHeight();
final int size= slices.size();
for(int i =1;i<size;i++){
final RasterSlice slice= slices.get(i);
final double scaleX = baseLevelWidth / (1.0 * slice.rasterDimensions.getWidth());
final double scaleY = baseLevelHeight / (1.0 * slice.rasterDimensions.getHeight());
this.levels.add(new RasterLevelDescriptor(slice.source, scaleX, scaleY, slice.rasterDimensions));
}
}
public RasterLoadingResult loadRaster(
final ImageReadParam readParameters,
final int overviewIndex,
final ReferencedEnvelope cropBBox,
final MathTransform2D requestedWorldToGrid,
final RasterLayerRequest request,
final Dimension tileDimension) throws IOException {
if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.fine("Loading raster data for RasterDescriptor " + this.toString());
}
ImageInputStream inStream = null;
final ReferencedEnvelope bbox = new ReferencedEnvelope(rasterBBOX);
// intersection of this tile bound with the current crop bbox
final ReferencedEnvelope intersection = new ReferencedEnvelope(bbox.intersection(cropBBox),
cropBBox.getCoordinateReferenceSystem());
ImageReader reader = null;
try {
//
// get info about the raster we have to read
//
// get selected level and base level dimensions
final RasterLevelDescriptor selectedlevel=levels.get(Integer.valueOf(overviewIndex));
final Object input = selectedlevel.source.getSource();
inStream = selectedlevel.source.getInputStreamSPI().createInputStreamInstance(input, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
int imageChoice= selectedlevel.source.getImageIndex();
if (inStream == null) {
return null;
}
// get a reader
reader = GeoTiffUtils.TIFFREADERFACTORY.createReaderInstance();
reader.setInput(inStream);
// now create the crop grid to world which can be used to decide
// which sourceFile area we need to crop in the selected level taking
// into account the scale factors imposed by the selection of this
// level together with the base level grid to world transformation
final MathTransform2D cropGridToWorldCorner = (MathTransform2D) ProjectiveTransform.create(selectedlevel.gridToWorldTransformCorner);
final MathTransform2D cropWorldToGrid = cropGridToWorldCorner.inverse();
// computing the crop sourceFile area which leaves straight into the
// selected level raster space, NOTICE that at the end we need to
// take into account the fact that we might also decimate therefore
// we cannot just use the crop grid to world but we need to correct
// it.
final Rectangle sourceArea = CRS.transform(cropWorldToGrid, new GeneralEnvelope(intersection)).toRectangle2D().getBounds();
XRectangle2D.intersect(sourceArea, selectedlevel.rasterDimensions.getBounds(), sourceArea);
// make sure roundings don't bother us
// is it empty??
if (sourceArea.isEmpty()) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.warning("Got empty area for rasterGranuleLoader " + this.toString()
+ " with request " + request.toString());
}
return null;
} else if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.fine("Loading level " + overviewIndex + " with sourceFile region " + sourceArea);
}
// set the sourceFile region
// readParameters.setSourceRegion(sourceAreaWithCollar);
readParameters.setSourceRegion(sourceArea);
// read
// TODO make this generic
RenderedImage raster = request.getReadType().read(readParameters, imageChoice, (File) input, selectedlevel.rasterDimensions.getBounds(), tileDimension);
if (raster == null) {
return null;
}
try {
raster.getWidth();
} catch (Throwable e) {
if (LOGGER.isLoggable(java.util.logging.Level.FINE)) {
LOGGER.log(java.util.logging.Level.FINE,
"Unable to load raster for rasterGranuleLoader " + this.toString()
+ " with request " + request.toString(), e);
}
return null;
}
// use fixed sourceFile area
sourceArea.setRect(readParameters.getSourceRegion());
//
// 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 sourceFile region sizes and the read
// image sizes.
//
// place it in the mosaic using the coords created above;
double decimationScaleX = ((1.0 * sourceArea.width) / raster.getWidth());
double decimationScaleY = ((1.0 * sourceArea.height) / raster.getHeight());
final AffineTransform decimationScaleTranform = XAffineTransform.getScaleInstance(decimationScaleX, decimationScaleY);
// keep into account translation to work into the selected level
// raster space
final AffineTransform afterDecimationTranslateTranform = XAffineTransform.getTranslateInstance(sourceArea.x, sourceArea.y);
// now we need to go back to the base level raster space
final AffineTransform backToBaseLevelScaleTransform =selectedlevel.getBaseToLevelTransform();
// now create the overall transform
final AffineTransform finalRaster2Model = new AffineTransform(baseGridToWorld);
finalRaster2Model.concatenate(CoverageUtilities.CENTER_TO_CORNER);
if(!XAffineTransform.isIdentity(backToBaseLevelScaleTransform, GeoTiffUtils.AFFINE_IDENTITY_EPS))
finalRaster2Model.concatenate(backToBaseLevelScaleTransform);
if(!XAffineTransform.isIdentity(afterDecimationTranslateTranform, GeoTiffUtils.AFFINE_IDENTITY_EPS))
finalRaster2Model.concatenate(afterDecimationTranslateTranform);
if(!XAffineTransform.isIdentity(decimationScaleTranform, GeoTiffUtils.AFFINE_IDENTITY_EPS))
finalRaster2Model.concatenate(decimationScaleTranform);
// return raster + its own transformation
return new RasterLoadingResult(raster, finalRaster2Model);
} catch (IllegalStateException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
"Unable to load raster for rasterGranuleLoader " + this.toString()
+ " with request " + request.toString(), e);
}
return null;
} catch (org.opengis.referencing.operation.NoninvertibleTransformException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
"Unable to load raster for rasterGranuleLoader " + this.toString()
+ " with request " + request.toString(), e);
}
return null;
} catch (TransformException e) {
if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) {
LOGGER.log(java.util.logging.Level.WARNING,
"Unable to load raster for rasterGranuleLoader " + this.toString()
+ " with request " + request.toString(), e);
}
return null;
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (Throwable t) {
}
}
if (reader != null) {
try {
reader.dispose();
} catch (Throwable t) {
}
}
}
}
// private RasterLevelDescriptor getLevel(ImageReader reader, RasterLayerRequest request, int imageChoice, int overviewIndex) {
// synchronized (levels) {
// if (levels.containsKey(Integer.valueOf(overviewIndex))) {
// return levels.get(Integer.valueOf(overviewIndex));
// } else {
// // load level
// // create the base grid to world transformation
// try {
// //
// // get info about the raster we have to read
// //
// // get selected level and base level dimensions
// final Rectangle levelDimension = new Rectangle(0, 0, reader.getWidth(imageChoice), reader.getHeight(imageChoice));
//
// final RasterLevelDescriptor baseLevel = levels.get(0);
// final double scaleX = baseLevel.width / (1.0 * levelDimension.width);
// final double scaleY = baseLevel.height / (1.0 * levelDimension.height);
//
// // add the base level
// final RasterLevelDescriptor newLevel = new RasterLevelDescriptor(scaleX, scaleY, levelDimension.width,levelDimension.height);
// this.granuleLevels.put(Integer.valueOf(overviewIndex), newLevel);
// return newLevel;
//
// } catch (IllegalStateException e) {
// throw new IllegalArgumentException(e);
//
// } catch (IOException e) {
// throw new IllegalArgumentException(e);
// }
// }
//
// }
// }
@Override
public String toString() {
// build a decent representation for this level
final StringBuilder buffer = new StringBuilder();
buffer.append("Description of a rasterGranuleLoader ").append("\n").append("BBOX:\t\t")
.append(rasterBBOX.toString()).append("gridToWorld:\t\t").append(baseGridToWorld);
int i = 1;
for (final RasterLevelDescriptor rasterLevelDescriptor : levels) {
i++;
buffer.append("Description of level ").append(i++).append("\n")
.append(rasterLevelDescriptor.toString()).append("\n");
}
return super.toString();
}
public BoundingBox getBBOX() {
return rasterBBOX;
}
}