/* * 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.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 javax.imageio.ImageTypeSpecifier; 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 { ArrayList<RasterManager.OverviewLevel> resolutionsLevels; public OverviewsController() { resolutionsLevels = new ArrayList<OverviewLevel>(); // 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 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 = 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(), Utils.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; ImageTypeSpecifier baseImageType; public RasterManager(final GeoTiffReader reader) throws DataSourceException { Utilities.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(); // base image type baseImageType = reader.baseImageType; 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}. */ 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; } }