/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.imagecollection;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
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.GridFormatFinder;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.coverage.grid.io.UnknownFormat;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.gce.geotiff.GeoTiffReader;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.util.SoftValueHashMap;
import org.geotools.util.Utilities;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
class RasterManager {
/**
* Simple support class for sorting overview resolutions
*
* @author Andrea Aime
* @author Simone Giannecchini, GeoSolutions.
* @since 2.5
*/
class OverviewLevel implements Comparable<OverviewLevel> {
double scaleFactor;
double resolutionX;
double resolutionY;
int imageChoice;
RasterLayout rasterLayout;
public OverviewLevel(final double scaleFactor,
final double resolutionX, final double resolutionY,
final int imageChoice, final RasterLayout rasterLayout) {
this.rasterLayout = rasterLayout;
this.scaleFactor = scaleFactor;
this.resolutionX = resolutionX;
this.resolutionY = resolutionY;
this.imageChoice = imageChoice;
}
public int compareTo(final OverviewLevel other) {
if (scaleFactor > other.scaleFactor)
return 1;
else if (scaleFactor < other.scaleFactor)
return -1;
else
return 0;
}
@Override
public String toString() {
return "OverviewLevel[Choice=" + imageChoice + ",scaleFactor="
+ scaleFactor + "]";
}
@Override
public int hashCode() {
int hash = Utilities.hash(imageChoice, 31);
hash = Utilities.hash(resolutionX, hash);
hash = Utilities.hash(resolutionY, hash);
hash = Utilities.hash(scaleFactor, hash);
hash = Utilities.hash(rasterLayout, hash);
return hash;
}
}
class OverviewsController {
ArrayList<RasterManager.OverviewLevel> resolutionsLevels = new ArrayList<OverviewLevel>();;
public OverviewsController(RasterLayout hrLayout, MathTransform grid2world) {
// notice that we assume what follows:
// -highest resolution image is at level 0.
// -all the overviews share the same envelope
// -the aspect ratio for the overviews is constant
// -the provided resolutions are taken directly from the grid
if (grid2world == null){
resolutionsLevels.add(new OverviewLevel(1, highestRes[0], highestRes[1], 0, hrLayout));
} else {
AffineTransform at = (AffineTransform) grid2world;
resolutionsLevels.add(new OverviewLevel(1, XAffineTransform.getScaleX0(at), XAffineTransform.getScaleY0(at), 0, hrLayout));
}
// if (numberOfOverwies > 0) {
// for (int i = 0; i < overviewsResolution.length; i++)
// resolutionsLevels.add(new OverviewLevel(
// overviewsResolution[i][0] / highestRes[0],
// overviewsResolution[i][0],
// overviewsResolution[i][1], i + 1,
// parent.overViewLayouts[i]));
// Collections.sort(resolutionsLevels);
// }
}
int pickOverviewLevel(final OverviewPolicy policy, final RasterLayerRequest request) {
// //
//
// If this file has only
// one page we use decimation, otherwise we use the best page
// available.
// Future versions should use both.
//
// //
if (resolutionsLevels == null || resolutionsLevels.size() <= 0)
return 0;
// Now search for the best matching resolution.
// Check also for the "perfect match"... unlikely in practice unless
// someone tunes the clients to request exactly the resolution embedded in
// the overviews, something a perf sensitive person might do in fact
// requested scale factor for least reduced axis
final OverviewLevel max = (OverviewLevel) resolutionsLevels.get(0);
// the requested resolutions
final double requestedScaleFactorX;
final double requestedScaleFactorY;
final double[] requestedRes = request.getRequestedResolution();
if (requestedRes != null) {
final double reqx = requestedRes[0];
final double reqy = requestedRes[1];
requestedScaleFactorX = reqx / max.resolutionX;
requestedScaleFactorY = reqy / max.resolutionY;
} else {
final double[] scaleFactors = request.getRequestedRasterScaleFactors();
if (scaleFactors == null)
return 0;
requestedScaleFactorX = scaleFactors[0];
requestedScaleFactorY = scaleFactors[1];
}
final int leastReduceAxis = requestedScaleFactorX <= requestedScaleFactorY ? 0 : 1;
final double requestedScaleFactor = leastReduceAxis == 0 ? requestedScaleFactorX : requestedScaleFactorY;
// are we looking for a resolution even higher than the native one?
if (requestedScaleFactor <= 1)
return max.imageChoice;
// are we looking for a resolution even lower than the smallest
// overview?
final OverviewLevel min = (OverviewLevel) resolutionsLevels.get(resolutionsLevels.size() - 1);
if (requestedScaleFactor >= min.scaleFactor)
return min.imageChoice;
// Ok, so we know the overview is between min and max, skip the first
// and search for an overview with a resolution lower than the one requested,
// that one and the one from the previous step will bound the searched resolution
OverviewLevel prev = max;
final int size = resolutionsLevels.size();
for (int i = 1; i < size; i++) {
final OverviewLevel curr = resolutionsLevels.get(i);
// perfect match check
if (curr.scaleFactor == requestedScaleFactor) {
return curr.imageChoice;
}
// middle check. The first part of the condition should be sufficient, but
// there are cases where the x resolution is satisfied by the lowest resolution,
// the y by the one before the lowest (so the aspect ratio of the request is
// different than the one of the overviews), and we would end up going out of the loop
// since not even the lowest can "top" the request for one axis
if (curr.scaleFactor > requestedScaleFactor || i == size - 1) {
if (policy == OverviewPolicy.QUALITY)
return prev.imageChoice;
else if (policy == OverviewPolicy.SPEED)
return curr.imageChoice;
else if (requestedScaleFactor - prev.scaleFactor < curr.scaleFactor
- requestedScaleFactor)
return prev.imageChoice;
else
return curr.imageChoice;
}
prev = curr;
}
// fallback
return max.imageChoice;
}
}
/**
* This class is responsible for doing decimation once the best overview
* available has been selected (this include the case when no overview is
* available).
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
class DecimationController {
public DecimationController() {
}
/**
* This method is responsible for evaluating possible subsampling
* factors once the best resolution level has been found, in case we
* have support for overviews, or starting from the original coverage in
* case there are no overviews available.
*
* Anyhow this method should not be called directly but subclasses
* should make use of the setReadParams method instead in order to
* transparently look for overviews.
*
* @param imageIndex
* @param readParameters
* @param imageManager
* @param requestedRes
*/
void computeDecimationFactors(final int imageIndex,
final ImageReadParam readParameters,
final RasterLayerRequest request) {
// the read parameters cannot be null
Utilities.ensureNonNull("readParameters", readParameters);
Utilities.ensureNonNull("request", request);
// get the requested resolution in order to guess what we are
// looking for
final double[] requestedRes = request.getRequestedResolution();
if (requestedRes == null) {
// if there is no requested resolution we don't do any
// subsampling
readParameters.setSourceSubsampling(1, 1, 0, 0);
return;
}
final int rasterWidth, rasterHeight;
double selectedRes[] = new double[2];
// are we working against a certain overview?
final OverviewLevel level = request.imageManager.overviewsController.resolutionsLevels.get(imageIndex);
selectedRes[0] = level.resolutionX;
selectedRes[1] = level.resolutionY;
if (imageIndex == 0) {
// highest resolution
rasterWidth = request.imageManager.coverageRasterArea.width;
rasterHeight = request.imageManager.coverageRasterArea.height;
} else {
// work on overviews
final RasterLayout selectedLevelLayout = request.imageManager.overviewsController.resolutionsLevels
.get(imageIndex).rasterLayout;
rasterWidth = selectedLevelLayout.width;
rasterHeight = selectedLevelLayout.height;
}
// //
// DECIMATION ON READING
// Setting subsampling factors with some checks
// 1) the subsampling factors cannot be zero
// 2) the subsampling factors cannot be such that the w or h are
// zero
// //
int subSamplingFactorX = (int) Math.floor(requestedRes[0] / selectedRes[0]);
subSamplingFactorX = subSamplingFactorX == 0 ? 1 : subSamplingFactorX;
while (rasterWidth / subSamplingFactorX <= 0 && subSamplingFactorX >= 0)
subSamplingFactorX--;
subSamplingFactorX = subSamplingFactorX <= 0 ? 1 : subSamplingFactorX;
int subSamplingFactorY = (int) Math.floor(requestedRes[1] / selectedRes[1]);
subSamplingFactorY = subSamplingFactorY == 0 ? 1
: subSamplingFactorY;
while (rasterHeight / subSamplingFactorY <= 0 && subSamplingFactorY >= 0)
subSamplingFactorY--;
subSamplingFactorY = subSamplingFactorY <= 0 ? 1 : subSamplingFactorY;
// set the read parameters
readParameters.setSourceSubsampling(subSamplingFactorX, subSamplingFactorY, 0, 0);
}
}
/**
* @TODO
*/
class ImageManager {
public ImageManager(ImageProperty property) {
this.property = property;
init();
}
ImageProperty property;
/** The base envelope 2D */
ReferencedEnvelope coverageBBox;
/** The CRS for the coverage */
CoordinateReferenceSystem coverageCRS;
// ////////////////////////////////////////////////////////////////////////
//
// Base coverage properties
//
// ////////////////////////////////////////////////////////////////////////
/** The base envelope read from file */
GeneralEnvelope coverageEnvelope = null;
double[] coverageFullResolution;
MathTransform coverageGridToWorld2D;
/** The base grid range for the coverage */
Rectangle coverageRasterArea;
OverviewsController overviewsController;
/**
* Set the main parameters of this coverage request, getting basic
* information from the reader.
*/
private void init() {
//TODO: Re-enable this code when leveraging on y as DISPLAY_DOWN
final boolean useDisplayCRS = false;//parent.defaultValues.epsgCode == 404001 ? false : true;
if (!property.isGeoSpatial()){
this.coverageCRS = useDisplayCRS ? Utils.DISPLAY_CRS : Utils.GENERIC2D_CRS;
this.coverageEnvelope = new GeneralEnvelope(new Rectangle2D.Double(0, useDisplayCRS? 0 : -property.getHeight(), property.getWidth(), property.getHeight()));
this.coverageRasterArea = new GridEnvelope2D(0, 0, property.getWidth(), property.getHeight());
this.coverageEnvelope.setCoordinateReferenceSystem(this.coverageCRS);
this.coverageGridToWorld2D = ProjectiveTransform.create(useDisplayCRS ? Utils.IDENTITY : Utils.IDENTITY_FLIP);
this.coverageFullResolution = new double[]{1.0, 1.0};
overviewsController = new OverviewsController(new RasterLayout(0,0, property.getWidth(), property.getHeight()), null);
} else {
this.coverageEnvelope = property.getEnvelope();
this.coverageRasterArea = new GridEnvelope2D(0, 0, property.getWidth(), property.getHeight());
this.coverageCRS = this.coverageEnvelope.getCoordinateReferenceSystem();
GridToEnvelopeMapper geMapper = new GridToEnvelopeMapper((GridEnvelope)coverageRasterArea, (Envelope)this.coverageEnvelope);
geMapper.setPixelAnchor(PixelInCell.CELL_CENTER);
this.coverageGridToWorld2D = geMapper.createTransform();
overviewsController = new OverviewsController(new RasterLayout(0,0, property.getWidth(), property.getHeight()), this.coverageGridToWorld2D);
final OverviewLevel highestLevel= overviewsController.resolutionsLevels.get(0);
coverageFullResolution = new double[2];
coverageFullResolution[0] = highestLevel.resolutionX;
coverageFullResolution[1] = highestLevel.resolutionY;
}
coverageBBox = new ReferencedEnvelope(coverageEnvelope);
}
}
private static final long CHECK_INTERVAL = 1000*60;
SoftValueHashMap<String, ImageManager> datasetManagerCache = new SoftValueHashMap<String, ImageManager>();
/** The CRS of the input coverage */
private CoordinateReferenceSystem coverageCRS;
/** The base envelope related to the input coverage */
private GeneralEnvelope coverageEnvelope;
/** The coverage factory producing a {@link GridCoverage} from an image */
private GridCoverageFactory coverageFactory;
/**
* The name of the input coverage TODO consider URI
*/
private String coverageIdentifier;
double[] highestRes;
/** The hints to be used to produce this coverage */
private Hints hints;
private URL inputURL;
// private int numberOfOverwies;
//
// private double[][] overviewsResolution;
// ////////////////////////////////////////////////////////////////////////
//
// Information obtained by the coverageRequest instance
//
// ////////////////////////////////////////////////////////////////////////
/** The coverage grid to world transformation */
private MathTransform raster2Model;
// OverviewsController overviewsController;
private GridEnvelope coverageGridrange;
OverviewPolicy overviewPolicy;
DecimationController decimationController;
ImageCollectionReader parent;
boolean expandMe;
public RasterManager(final ImageCollectionReader reader) throws DataSourceException {
Utilities.ensureNonNull("ImageCollectionReader", reader);
this.parent = reader;
this.expandMe = parent.expandMe;
inputURL = reader.sourceURL;
coverageIdentifier = reader.getName();
hints = reader.getHints();
coverageFactory = reader.getGridCoverageFactory();
// get the overviews policy
extractOverviewPolicy();
highestRes = new double[]{1.0, 1.0};
//TODO: What to do when I remove that default file?
ImageManager imageManager = getDatasetManager(Utils.FAKE_IMAGE_PATH);
// ImageManager imageManager = getDatasetManager(parent.rootPath + parent.defaultPath);
coverageEnvelope = imageManager.coverageEnvelope;
coverageGridrange = new GridEnvelope2D(imageManager.coverageRasterArea);
coverageCRS = imageManager.coverageCRS;
raster2Model = imageManager.coverageGridToWorld2D;
if (imageManager.property.isGeoSpatial()){
//Updating highestRes
OverviewLevel level0 = imageManager.overviewsController.resolutionsLevels.get(0);
highestRes[0] = level0.resolutionX;
highestRes[1] = level0.resolutionY;
}
// coverageEnvelope = reader.getOriginalEnvelope();
// coverageGridrange = reader.getOriginalGridRange();
// coverageCRS = reader.getCrs();
// highestRes = reader.getHighestRes();
//
// // TODO: USE IDENTITY??
// raster2Model = ProjectiveTransform.create(Utils.IDENTITY);
// raster2Model = ProjectiveTransform.create(Utils.IDENTITY_HALFPIXEL);
// reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER);
// resolution values
// numberOfOverwies = reader.getNumberOfOverviews();
// overviewsResolution = reader.getOverviewsResolution();
// instantiating controller for subsampling and overviews
decimationController = new DecimationController();
}
/**
* This method is responsible for checking the overview policy as defined by
* the provided {@link Hints}.
*
* @return the overview policy which can be one of
* {@link Hints#VALUE_OVERVIEW_POLICY_IGNORE},
* {@link Hints#VALUE_OVERVIEW_POLICY_NEAREST},
* {@link Hints#VALUE_OVERVIEW_POLICY_SPEED},
* {@link Hints#VALUE_OVERVIEW_POLICY_QUALITY}. Default is
* {@link Hints#VALUE_OVERVIEW_POLICY_NEAREST}.
*/
private OverviewPolicy extractOverviewPolicy() {
// check if a policy was provided using hints (check even the
// deprecated one)
if (this.hints != null)
if (this.hints.containsKey(Hints.OVERVIEW_POLICY))
overviewPolicy = (OverviewPolicy) this.hints.get(Hints.OVERVIEW_POLICY);
// use default if not provided. Default is nearest
if (overviewPolicy == null) {
overviewPolicy = OverviewPolicy.NEAREST;
}
assert overviewPolicy != null;
return overviewPolicy;
}
public Collection<GridCoverage2D> read(final GeneralParameterValue[] params)
throws IOException {
// create a request
final RasterLayerRequest request = new RasterLayerRequest(params, this);
if (request.isEmpty()) {
return Collections.emptyList();
}
// create a response for the provided request
final RasterLayerResponse response = new RasterLayerResponse(request, this);
// execute the request
final GridCoverage2D elem = response.createResponse();
if (elem != null) {
return Collections.singletonList(elem);
}
return Collections.emptyList();
}
public void dispose() {
}
public URL getInputURL() {
return inputURL;
}
public String getCoverageIdentifier() {
return coverageIdentifier;
}
public Hints getHints() {
return hints;
}
public CoordinateReferenceSystem getCoverageCRS() {
return coverageCRS;
}
public GeneralEnvelope getCoverageEnvelope() {
return coverageEnvelope;
}
public GridCoverageFactory getCoverageFactory() {
return coverageFactory;
}
public MathTransform getRaster2Model() {
return raster2Model;
}
public GridEnvelope getCoverageGridrange() {
return coverageGridrange;
}
/**
* Get back from the imageManager cache the {@link ImageManager} instance related to the
* dataset referred by the specified filePath.
*
* @param filePath the absolute path referring to a specific dataset.
* @return the ImageManager related to the specified dataset.
* @throws DataSourceException
*/
synchronized ImageManager getDatasetManager(String filePath) throws DataSourceException {
if (filePath == null){
throw new IllegalArgumentException("Must specify a valid filePath whilst NULL have been provided");
}
// if (filePath.equalsIgnoreCase(Utils.DEFAULT_IMAGE_PATH)){
// return new ImageManager(new ImageProperty());
// }
ImageManager imageManager = null;
boolean isValid = false;
if (datasetManagerCache.containsKey(filePath)) {
imageManager = datasetManagerCache.get(filePath);
if (imageManager != null) {
// the dataset manager is already available on cache
// check for updates.
final long now = System.currentTimeMillis();
if((now - imageManager.property.lastCheckTime) > CHECK_INTERVAL) {
imageManager.property.lastCheckTime = now;
final File file = new File(filePath);
if (Utils.checkFileReadable(file)){
final long modTime = file.lastModified();
if (modTime == imageManager.property.lastModifiedTime){
// The lastModifiedTime of the file isn't changed.
// it is still valid.
isValid = true;
}
} else {
// The file no more exists. Remove it from cache
datasetManagerCache.remove(filePath);
}
} else {
// The file is still recent. No need to check it yet
isValid = true;
}
}
}
if (isValid){
return imageManager;
}
// There isn't any valid object in cache or the cached object needs to be updated.
imageManager = initDatasetManager(filePath);
datasetManagerCache.put(filePath, imageManager);
return imageManager;
}
/**
* Initialize the dataset manager by gathering main properties from the specified file.
* @param filePath
* @return
* @throws DataSourceException
*/
private ImageManager initDatasetManager(final String filePath) throws DataSourceException {
if (filePath.equalsIgnoreCase(Utils.FAKE_IMAGE_PATH)){
//USING FAKE VALUES
ImageProperty imageProperty = new ImageProperty();
imageProperty.setHeight(parent.defaultValues.maxHeight);
imageProperty.setWidth(parent.defaultValues.maxWidth);
if (parent.defaultValues.isGeoSpatial){
imageProperty.setGeoSpatial(parent.defaultValues.isGeoSpatial);
imageProperty.setEnvelope(parent.defaultValues.envelope);
}
return new ImageManager(imageProperty);
}
ImageManager imageManager = null;
final File file = new File(filePath);
ImageInputStream stream = null;
ImageReader reader = null;
GeoTiffReader gtReader = null;
try {
if (file.exists() && file.canRead()) {
if (!parent.defaultValues.isGeoSpatial){
stream = ImageIO.createImageInputStream(file);
reader = Utils.getReader(stream);
if (reader != null) {
reader.setInput(stream);
// Setting up properties
final ImageReaderSpi spi = reader.getOriginatingProvider();
final int width = reader.getWidth(0);
final int height = reader.getHeight(0);
int numOverviews = reader.getNumImages(false) - 1;
if (numOverviews < 0) {
numOverviews = 0;
}
final long lastModified = file.lastModified();
final ImageProperty property = new ImageProperty(filePath, width, height, numOverviews, spi, lastModified);
imageManager = new ImageManager(property);
} else {
throw new DataSourceException("Unable to get a reader for the specified path " + filePath);
}
} else {
gtReader = new GeoTiffReader(file);
GeneralEnvelope envelope = gtReader.getOriginalEnvelope();
GridEnvelope range = gtReader.getOriginalGridRange();
final long lastModified = file.lastModified();
final ImageProperty property = new ImageProperty(filePath, range.getSpan(0), range.getSpan(1),
0, Utils.TIFF_SPI, lastModified);
property.setEnvelope(envelope);
property.setGeoSpatial(true);
imageManager = new ImageManager(property);
}
} else {
throw new DataSourceException("The specified path doesn't exist or can't be read: " + filePath);
}
} catch (IOException ioe) {
DataSourceException dse = new DataSourceException("IOException occurred while accessing the specified path " + filePath);
dse.initCause(ioe);
throw dse;
} finally {
// Release resources. Close stream and dispose reader
if (stream != null) {
try {
stream.close();
} catch (Throwable t) {
}
}
if (reader != null) {
try {
reader.dispose();
} catch (Throwable t) {
}
}
if (gtReader != null) {
try {
gtReader.dispose();
} catch (Throwable t) {
}
}
}
return imageManager;
}
}