/* * 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.coverageio.jp2k; 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 java.util.logging.Level; import java.util.logging.Logger; 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.metadata.iso.extent.GeographicBoundingBoxImpl; import org.geotools.referencing.CRS; 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.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; /** * A RasterManager allows to handle granules, decimation, overviews and spatial * properties of a raster. * * @author Daniele Romagnoli, GeoSolutions S.A.S. * @author Simone Giannecchini, GeoSolutions S.A.S. */ class RasterManager { /** Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(RasterManager.class); final SoftValueHashMap<String, Granule> granulesCache = new SoftValueHashMap<String, Granule>(); /** * 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; public OverviewLevel( final double scaleFactor, final double resolutionX, final double resolutionY, int imageChoice) { 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); 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)); if (numberOfOverviews > 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)); } 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 { return 0; } 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; } } 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 performDecimation( 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 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; } double selectedRes[] = new double[2]; final OverviewLevel level = overviewsController.resolutionsLevels.get(imageIndex); selectedRes[0] = level.resolutionX; selectedRes[1] = level.resolutionY; final int rasterWidth, rasterHeight; if (imageIndex == 0) { // highest resolution rasterWidth = spatialDomainManager.coverageRasterArea.width; rasterHeight = spatialDomainManager.coverageRasterArea.height; } else { // work on overviews // TODO this is bad side effect of how the Overviews are managed // right now. There are two problems here, // first we are assuming that we are working with LON/LAT, // second is that we are getting just an approximation of // raster dimensions. The solution is to have the rater // dimensions on each level and to confront raster dimensions, // which means working rasterWidth = (int) Math.round(spatialDomainManager.coverageBBox.getSpan(0) / selectedRes[0]); rasterHeight = (int) Math.round(spatialDomainManager.coverageBBox.getSpan(1) / selectedRes[1]); } // ///////////////////////////////////////////////////////////////////// // 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; 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 SpatialDomainManager { public SpatialDomainManager() 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 = Utils.getReferencedEnvelopeFromGeographicBoundingBox(new GeographicBoundingBoxImpl(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()); 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 numberOfOverviews; 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; JP2KReader parent; boolean expandMe; SpatialDomainManager spatialDomainManager; public RasterManager(final JP2KReader reader) throws DataSourceException { Utils.ensureNonNull("JP2KReader", reader); this.parent = reader; this.expandMe = parent.expandMe; inputURL = reader.sourceURL; hints = reader.getHints(); coverageEnvelope = reader.getOriginalEnvelope(); coverageGridrange = reader.getOriginalGridRange(); coverageCRS = reader.getCrs(); raster2Model = reader.getOriginalGridToWorld(PixelInCell.CELL_CENTER); this.coverageIdentifier = reader.getName(); this.coverageFactory = reader.getGridCoverageFactory(); // resolution values highestRes = reader.getHighestRes(); numberOfOverviews = reader.getNumberOfOverviews(); overviewsResolution = reader.getOverviewsResolution(); // instantiating controller for subsampling and overviews overviewsController = new OverviewsController(); decimationController = new DecimationController(); try { spatialDomainManager = new SpatialDomainManager(); } catch (TransformException e) { throw new DataSourceException(e); } catch (FactoryException e) { throw new DataSourceException(e); } extractOverviewPolicy(); } /** * 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() { 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.getDefaultPolicy(); } 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()) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Request is empty: " + request.toString()); 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; } }