public static HashMap<String, Double> getRegionParamsFromGridCoverage(
      GridCoverage2D gridCoverage) {
    HashMap<String, Double> envelopeParams = new HashMap<String, Double>();

    Envelope envelope = gridCoverage.getEnvelope();

    DirectPosition lowerCorner = envelope.getLowerCorner();
    double[] westSouth = lowerCorner.getCoordinate();
    DirectPosition upperCorner = envelope.getUpperCorner();
    double[] eastNorth = upperCorner.getCoordinate();

    GridGeometry2D gridGeometry = gridCoverage.getGridGeometry();
    GridEnvelope2D gridRange = gridGeometry.getGridRange2D();
    int height = gridRange.height;
    int width = gridRange.width;

    AffineTransform gridToCRS = (AffineTransform) gridGeometry.getGridToCRS();
    double xRes = XAffineTransform.getScaleX0(gridToCRS);
    double yRes = XAffineTransform.getScaleY0(gridToCRS);

    envelopeParams.put(NORTH, eastNorth[1]);
    envelopeParams.put(SOUTH, westSouth[1]);
    envelopeParams.put(WEST, westSouth[0]);
    envelopeParams.put(EAST, eastNorth[0]);
    envelopeParams.put(XRES, xRes);
    envelopeParams.put(YRES, yRes);
    envelopeParams.put(ROWS, (double) height);
    envelopeParams.put(COLS, (double) width);

    return envelopeParams;
  }
  /**
   * Tests the {@link OverviewsController} with support for different resolutions/different number
   * of overviews.
   *
   * <p>world_a.tif => Pixel Size = (0.833333333333333,-0.833333333333333); 4 overviews world_b.tif
   * => Pixel Size = (1.406250000000000,-1.406250000000000); 2 overviews
   *
   * @throws IOException
   * @throws MismatchedDimensionException
   * @throws FactoryException
   * @throws TransformException
   */
  @Test
  public void testHeterogeneousGranules()
      throws IOException, MismatchedDimensionException, FactoryException, TransformException {

    final CoordinateReferenceSystem WGS84 = CRS.decode("EPSG:4326", true);
    final ReferencedEnvelope TEST_BBOX_A = new ReferencedEnvelope(-180, 0, -90, 90, WGS84);
    final ReferencedEnvelope TEST_BBOX_B = new ReferencedEnvelope(0, 180, 0, 90, WGS84);

    URL heterogeneousGranulesURL = TestData.url(this, "heterogeneous");
    // //
    //
    // Initialize mosaic variables
    //
    // //
    final Hints hints = new Hints(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM, WGS84);
    final AbstractGridFormat format =
        (AbstractGridFormat) GridFormatFinder.findFormat(heterogeneousGranulesURL, hints);
    Assert.assertNotNull(format);
    Assert.assertFalse("UknownFormat", format instanceof UnknownFormat);

    final ImageMosaicReader reader =
        (ImageMosaicReader) format.getReader(heterogeneousGranulesURL, hints);
    Assert.assertNotNull(reader);

    final int nOv = reader.getNumberOfOvervies();
    final double[] hRes = reader.getHighestRes();
    final RasterManager rasterManager = new RasterManager(reader);

    // //
    //
    // Initialize granules related variables
    //
    // //
    final File g1File = new File(DataUtilities.urlToFile(heterogeneousGranulesURL), "world_a.tif");
    final File g2File = new File(DataUtilities.urlToFile(heterogeneousGranulesURL), "world_b.tif");
    final ImageReadParam readParamsG1 = new ImageReadParam();
    final ImageReadParam readParamsG2 = new ImageReadParam();
    int imageIndexG1 = 0;
    int imageIndexG2 = 0;

    final GranuleDescriptor granuleDescriptor1 =
        new GranuleDescriptor(g1File.getAbsolutePath(), TEST_BBOX_A, spi, (Geometry) null, true);
    final GranuleDescriptor granuleDescriptor2 =
        new GranuleDescriptor(g2File.getAbsolutePath(), TEST_BBOX_B, spi, (Geometry) null, true);
    assertNotNull(granuleDescriptor1.toString());
    assertNotNull(granuleDescriptor2.toString());

    final OverviewsController ovControllerG1 = granuleDescriptor1.overviewsController;
    final OverviewsController ovControllerG2 = granuleDescriptor2.overviewsController;

    // //
    //
    // Initializing read request
    //
    // //
    final GeneralEnvelope envelope = reader.getOriginalEnvelope();
    final GridEnvelope originalRange = reader.getOriginalGridRange();
    final Rectangle rasterArea =
        new Rectangle(
            0,
            0,
            (int) Math.ceil(originalRange.getSpan(0) / 9.0),
            (int) Math.ceil(originalRange.getSpan(1) / 9.0));
    final GridEnvelope2D range = new GridEnvelope2D(rasterArea);
    final GridToEnvelopeMapper geMapper = new GridToEnvelopeMapper(range, envelope);
    geMapper.setPixelAnchor(PixelInCell.CELL_CENTER);
    final AffineTransform gridToWorld = geMapper.createAffineTransform();
    final double requestedResolution[] =
        new double[] {
          XAffineTransform.getScaleX0(gridToWorld), XAffineTransform.getScaleY0(gridToWorld)
        };

    TestSet at = null;
    if (nOv == 4 && Math.abs(hRes[0] - 0.833333333333) <= THRESHOLD) {
      at = at1;
    } else if (nOv == 2 && Math.abs(hRes[0] - 1.40625) <= THRESHOLD) {
      at = at2;
    } else {
      return;
    }

    // //
    //
    // Starting OverviewsController tests
    //
    // //
    final OverviewPolicy[] ovPolicies =
        new OverviewPolicy[] {
          OverviewPolicy.QUALITY,
          OverviewPolicy.SPEED,
          OverviewPolicy.NEAREST,
          OverviewPolicy.IGNORE
        };
    for (int i = 0; i < ovPolicies.length; i++) {
      OverviewPolicy ovPolicy = ovPolicies[i];
      LOGGER.info("Testing with OverviewPolicy = " + ovPolicy.toString());
      imageIndexG1 =
          ReadParamsController.setReadParams(
              requestedResolution,
              ovPolicy,
              DecimationPolicy.ALLOW,
              readParamsG1,
              rasterManager,
              ovControllerG1);
      imageIndexG2 =
          ReadParamsController.setReadParams(
              requestedResolution,
              ovPolicy,
              DecimationPolicy.ALLOW,
              readParamsG2,
              rasterManager,
              ovControllerG2);
      assertSame(at.ot[i].g1.imageIndex, imageIndexG1);
      assertSame(at.ot[i].g2.imageIndex, imageIndexG2);
      assertSame(at.ot[i].g1.ssx, readParamsG1.getSourceXSubsampling());
      assertSame(at.ot[i].g1.ssy, readParamsG1.getSourceYSubsampling());
      assertSame(at.ot[i].g2.ssx, readParamsG2.getSourceXSubsampling());
      assertSame(at.ot[i].g2.ssy, readParamsG2.getSourceYSubsampling());
    }
  }
  /**
   * Collect georeferencing information about this geotiff.
   *
   * @param hints
   * @throws DataSourceException
   */
  private void getHRInfo(Hints hints) throws DataSourceException {
    ImageReader reader = null;
    ImageReader ovrReader = null;
    ImageInputStream ovrStream = null;
    try {
      // //
      //
      // Get a reader for this format
      //
      // //
      reader = READER_SPI.createReaderInstance();

      // //
      //
      // get the METADATA
      //
      // //
      inStream.mark();
      reader.setInput(inStream);
      final IIOMetadata iioMetadata = reader.getImageMetadata(0);
      final GeoTiffIIOMetadataDecoder metadata = new GeoTiffIIOMetadataDecoder(iioMetadata);
      gtcs = new GeoTiffMetadata2CRSAdapter(hints);

      // //
      //
      // get the CRS INFO
      //
      // //
      final Object tempCRS = this.hints.get(Hints.DEFAULT_COORDINATE_REFERENCE_SYSTEM);
      if (tempCRS != null) {
        this.crs = (CoordinateReferenceSystem) tempCRS;
        if (LOGGER.isLoggable(Level.FINE))
          LOGGER.log(Level.FINE, "Using forced coordinate reference system");
      } else {

        // check external prj first
        crs = getCRS(source);

        // now, if we did not want to override the inner CRS or we did not have any external PRJ at
        // hand
        // let's look inside the geotiff
        if (!OVERRIDE_INNER_CRS || crs == null) {
          if (metadata.hasGeoKey() && gtcs != null) {
            crs = gtcs.createCoordinateSystem(metadata);
          }
        }
      }

      //
      // No data
      //
      if (metadata.hasNoData()) {
        noData = metadata.getNoData();
      }

      //
      // parse and set layout
      //
      setLayout(reader);

      // //
      //
      // get the dimension of the hr image and build the model as well as
      // computing the resolution
      // //
      numOverviews = reader.getNumImages(true) - 1;
      int hrWidth = reader.getWidth(0);
      int hrHeight = reader.getHeight(0);
      final Rectangle actualDim = new Rectangle(0, 0, hrWidth, hrHeight);
      originalGridRange = new GridEnvelope2D(actualDim);

      if (gtcs != null
          && metadata != null
          && (metadata.hasModelTrasformation()
              || (metadata.hasPixelScales() && metadata.hasTiePoints()))) {
        this.raster2Model = GeoTiffMetadata2CRSAdapter.getRasterToModel(metadata);
      } else {
        // world file
        this.raster2Model = parseWorldFile(source);

        // now world file --> mapinfo?
        if (raster2Model == null) {
          MapInfoFileReader mifReader = parseMapInfoFile(source);
          if (mifReader != null) {
            raster2Model = mifReader.getTransform();
            crs = mifReader.getCRS();
          }
        }
      }

      if (crs == null) {
        if (LOGGER.isLoggable(Level.WARNING)) {
          LOGGER.warning("Coordinate Reference System is not available");
        }
        crs = AbstractGridFormat.getDefaultCRS();
      }

      if (this.raster2Model == null) {
        TiePoint[] modelTiePoints = metadata.getModelTiePoints();
        if (modelTiePoints != null && modelTiePoints.length > 1) {
          // use a unit transform and expose the GCPs
          gcps = new GroundControlPoints(Arrays.asList(modelTiePoints), crs);
          raster2Model = ProjectiveTransform.create(new AffineTransform());
          crs = AbstractGridFormat.getDefaultCRS();
        } else {
          throw new DataSourceException("Raster to Model Transformation is not available");
        }
      }

      // create envelope using corner transformation
      final AffineTransform tempTransform = new AffineTransform((AffineTransform) raster2Model);
      tempTransform.concatenate(CoverageUtilities.CENTER_TO_CORNER);
      originalEnvelope =
          CRS.transform(ProjectiveTransform.create(tempTransform), new GeneralEnvelope(actualDim));
      originalEnvelope.setCoordinateReferenceSystem(crs);

      // ///
      //
      // setting the higher resolution available for this coverage
      //
      // ///
      highestRes = new double[2];
      highestRes[0] = XAffineTransform.getScaleX0(tempTransform);
      highestRes[1] = XAffineTransform.getScaleY0(tempTransform);

      if (ovrInStreamSPI != null) {
        ovrReader = READER_SPI.createReaderInstance();
        ovrStream =
            ovrInStreamSPI.createInputStreamInstance(
                ovrSource, ImageIO.getUseCache(), ImageIO.getCacheDirectory());
        ovrReader.setInput(ovrStream);
        // this includes the real image as this is a image index, we need to add one.
        extOvrImgChoice = numOverviews + 1;
        numOverviews = numOverviews + ovrReader.getNumImages(true);
        if (numOverviews < extOvrImgChoice) extOvrImgChoice = -1;
      }

      // //
      //
      // get information for the successive images
      //
      // //
      if (numOverviews >= 1) {
        overViewResolutions = new double[numOverviews][2];
        // Internal overviews start at 1, so lastInternalOverview matches numOverviews if no
        // external.
        int firstExternalOverview = extOvrImgChoice == -1 ? numOverviews : extOvrImgChoice - 1;
        double spanRes0 = highestRes[0] * this.originalGridRange.getSpan(0);
        double spanRes1 = highestRes[1] * this.originalGridRange.getSpan(1);
        for (int i = 0; i < firstExternalOverview; i++) {
          overViewResolutions[i][0] = spanRes0 / reader.getWidth(i + 1);
          overViewResolutions[i][1] = spanRes1 / reader.getHeight(i + 1);
        }
        for (int i = firstExternalOverview; i < numOverviews; i++) {
          overViewResolutions[i][0] = spanRes0 / ovrReader.getWidth(i - firstExternalOverview);
          overViewResolutions[i][1] = spanRes1 / ovrReader.getHeight(i - firstExternalOverview);
        }

      } else overViewResolutions = null;
    } catch (Throwable e) {
      throw new DataSourceException(e);
    } finally {
      if (reader != null)
        try {
          reader.dispose();
        } catch (Throwable t) {
        }

      if (ovrReader != null)
        try {
          ovrReader.dispose();
        } catch (Throwable t) {
        }

      if (ovrStream != null)
        try {
          ovrStream.close();
        } catch (Throwable t) {
        }

      if (inStream != null)
        try {
          inStream.reset();
        } catch (Throwable t) {
        }
    }
  }
  /**
   * Computes the requested resolution which is going to be used for selecting overviews and or
   * deciding decimation factors on the target coverage.
   *
   * <p>In case the requested envelope is in the same {@link CoordinateReferenceSystem} of the
   * coverage we compute the resolution using the requested {@link MathTransform}. Notice that it
   * must be a {@link LinearTransform} or else we fail.
   *
   * <p>In case the requested envelope is not in the same {@link CoordinateReferenceSystem} of the
   * coverage we
   *
   * @throws DataSourceException in case something bad happens during reprojections and/or
   *     intersections.
   */
  private void computeRequestedResolution() throws DataSourceException {

    try {

      // let's try to get the resolution from the requested gridToWorld
      if (requestedGridToWorld instanceof LinearTransform) {

        //
        // the crs of the request and the one of the coverage are NOT the
        // same and the conversion is not , we can get the resolution from envelope + raster
        // directly
        //
        if (destinationToSourceTransform != null && !destinationToSourceTransform.isIdentity()) {

          //
          // compute the approximated resolution in the request crs, notice that we are
          // assuming a reprojection that keeps the raster area unchanged hence
          // the effect is a degradation of quality, but we might take that into account emprically
          //
          requestedResolution = null;
          //
          // // compute the raster that correspond to the crop bbox at the highest resolution
          // final Rectangle sourceRasterArea = new GeneralGridEnvelope(
          // CRS.transform(
          // PixelTranslation.translate(rasterManager.getRaster2Model(),PixelInCell.CELL_CENTER,PixelInCell.CELL_CORNER).inverse(),
          // cropBBox),PixelInCell.CELL_CORNER,false).toRectangle();
          // XRectangle2D.intersect(sourceRasterArea,
          // rasterManager.spatialDomainManager.coverageRasterArea, sourceRasterArea);
          // if(sourceRasterArea.isEmpty())
          // throw new DataSourceException("The request source raster area is empty");

          final GridToEnvelopeMapper geMapper =
              new GridToEnvelopeMapper(new GridEnvelope2D(destinationRasterArea), cropBBox);
          final AffineTransform tempTransform = geMapper.createAffineTransform();
          // final double scaleX=XAffineTransform.getScaleX0((AffineTransform)
          // requestedGridToWorld)/XAffineTransform.getScaleX0(tempTransform);
          // final double scaleY=XAffineTransform.getScaleY0((AffineTransform)
          // requestedGridToWorld)/XAffineTransform.getScaleY0(tempTransform);
          // //
          // // empiric adjustment to get a finer resolution to have better quality when
          // reprojecting
          // // TODO make it parametric
          // //
          // requestedRasterScaleFactors= new double[2];
          // requestedRasterScaleFactors[0]=scaleX*1.0;
          // requestedRasterScaleFactors[1]=scaleY*1.0;

          requestedResolution =
              new double[] {
                XAffineTransform.getScaleX0(tempTransform),
                XAffineTransform.getScaleY0(tempTransform)
              };

        } else {

          // the crs of the request and the one of the coverage are the
          // same, we can get the resolution from the grid to world
          requestedResolution =
              new double[] {
                XAffineTransform.getScaleX0(requestedGridToWorld),
                XAffineTransform.getScaleY0(requestedGridToWorld)
              };
        }
      } else
        // should not happen
        throw new UnsupportedOperationException(
            Errors.format(ErrorKeys.UNSUPPORTED_OPERATION_$1, requestedGridToWorld.toString()));

      // leave
      return;
    } catch (Throwable e) {
      if (LOGGER.isLoggable(Level.INFO))
        LOGGER.log(Level.INFO, "Unable to compute requested resolution", e);
    }

    //
    // use the coverage resolution since we cannot compute the requested one
    //
    LOGGER.log(Level.WARNING, "Unable to compute requested resolution, using highest available");
    requestedResolution = coverageProperties.fullResolution;
  }