package org.geotools.gce.geotiff;
import java.awt.Rectangle;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import javax.imageio.ImageReadParam;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.util.Utilities;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
class RasterManager {
/**
* Simple support class for sorting overview resolutions
* @author Andrea Aime
* @author Simone Giannecchini, GeoSolutions.
* @since 2.5
*/
static 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 {
final ArrayList<RasterManager.OverviewLevel> resolutionsLevels = new ArrayList<OverviewLevel>();
public OverviewsController() {
// 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
resolutionsLevels.add(new OverviewLevel(1, highestRes[0],highestRes[1], 0,parent.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 requestedRes
*/
void computeDecimationFactors(
final int imageIndex,
final ImageReadParam readParameters,
final RasterLayerRequest request) {
{
// the read parameters cannot be null
Utils.ensureNonNull("readParameters", readParameters);
Utils.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=overviewsController.resolutionsLevels.get(imageIndex);
selectedRes[0] = level.resolutionX;
selectedRes[1] = level.resolutionY;
if (imageIndex == 0) {
// highest resolution
rasterWidth = domainManager.coverageRasterArea.width;
rasterHeight = domainManager.coverageRasterArea.height;
} else {
// work on overviews
final RasterLayout selectedLevelLayout= 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);
}
}
}
/**
* This class is responsible for putting together all the 2D spatial information needed for a certain raster.
*
* <p>
* Notice that when this structure will be extended to work in ND this will become much more complex or as an
* alternative a sibling TemporalDomainManager will be created.
*
* @author Simone Giannecchini, GeoSolutions SAS
*
*/
class DomainManager{
public DomainManager() throws TransformException, FactoryException {
setBaseParameters();
prepareCoverageSpatialElements();
}
/** The base envelope 2D */
ReferencedEnvelope coverageBBox;
/** The CRS for the coverage */
CoordinateReferenceSystem coverageCRS;
/** The CRS related to the base envelope 2D */
CoordinateReferenceSystem coverageCRS2D;
// ////////////////////////////////////////////////////////////////////////
//
// Base coverage properties
//
// ////////////////////////////////////////////////////////////////////////
/** The base envelope read from file */
GeneralEnvelope coverageEnvelope = null;
double[] coverageFullResolution;
/** WGS84 envelope 2D for this coverage */
ReferencedEnvelope coverageGeographicBBox;
CoordinateReferenceSystem coverageGeographicCRS2D;
MathTransform2D coverageGridToWorld2D;
/** The base grid range for the coverage */
Rectangle coverageRasterArea;
/**
* Initialize the 2D properties (CRS and Envelope) of this coverage
* @throws TransformException
*
* @throws FactoryException
* @throws TransformException
* @throws FactoryException
*/
private void prepareCoverageSpatialElements() throws TransformException, FactoryException {
//
// basic initialization
//
coverageGeographicBBox =new ReferencedEnvelope(CRS.transform(CRS.findMathTransform(coverageEnvelope.getCoordinateReferenceSystem(), DefaultGeographicCRS.WGS84, true),coverageEnvelope));
coverageGeographicCRS2D=coverageGeographicBBox.getCoordinateReferenceSystem();
//
// Get the original envelope 2d and its spatial reference system
//
coverageCRS2D = CRS.getHorizontalCRS(coverageCRS);
assert coverageCRS2D.getCoordinateSystem().getDimension() == 2;
if (coverageCRS.getCoordinateSystem().getDimension() != 2) {
final MathTransform transform=CRS.findMathTransform(coverageCRS,(CoordinateReferenceSystem) coverageCRS2D);
final GeneralEnvelope bbox = CRS.transform(transform,coverageEnvelope);
bbox.setCoordinateReferenceSystem(coverageCRS2D);
coverageBBox = new ReferencedEnvelope(bbox);
} else {
//it is already a bbox
coverageBBox = new ReferencedEnvelope(coverageEnvelope);
}
}
/**
* Set the main parameters of this coverage request, getting basic
* information from the reader.
*/
private void setBaseParameters() {
this.coverageEnvelope = RasterManager.this.getCoverageEnvelope().clone();
this.coverageRasterArea =(( GridEnvelope2D)RasterManager.this.getCoverageGridrange()).clone();
this.coverageCRS = RasterManager.this.getCoverageCRS();
this.coverageGridToWorld2D = (MathTransform2D) RasterManager.this.getRaster2Model();
this.coverageFullResolution = new double[2];
final OverviewLevel highestLevel= RasterManager.this.overviewsController.resolutionsLevels.get(0);
coverageFullResolution[0] = highestLevel.resolutionX;
coverageFullResolution[1] = highestLevel.resolutionY;
}
}
/** 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;
private 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;
GeoTiffReader parent;
private String locationAttribute;
boolean expandMe;
DomainManager domainManager;
public RasterManager(final GeoTiffReader reader) throws DataSourceException {
Utils.ensureNonNull("GeoTiffReader", 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();
coverageEnvelope = reader.getOriginalEnvelope();
coverageGridrange=reader.getOriginalGridRange();
coverageCRS = reader.getCrs();
raster2Model = reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER);
//resolution values
highestRes= reader.getHighestRes();
numberOfOverwies=reader.getNumberOfOverviews();
overviewsResolution=reader.getOverviewsResolution();
//instantiating controller for subsampling and overviews
overviewsController=new OverviewsController();
decimationController= new DecimationController();
try {
domainManager= new DomainManager();
} catch (TransformException e) {
throw new DataSourceException(e);
} catch (FactoryException e) {
throw new DataSourceException(e);
}
}
/**
* 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}.
*/
@SuppressWarnings("deprecation")
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 String getLocationAttribute() {
return locationAttribute;
}
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;
}
}