/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2007 - 2016, 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.imagemosaic; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Rectangle2D; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.RenderedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.ImageTypeSpecifier; import javax.imageio.spi.ImageInputStreamSpi; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.stream.ImageInputStream; import javax.media.jai.BorderExtender; import javax.media.jai.ImageLayout; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationNearest; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.ROIShape; import javax.media.jai.TileCache; import javax.media.jai.TileScheduler; import org.apache.commons.beanutils.MethodUtils; 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.footprint.FootprintBehavior; import org.geotools.coverage.grid.io.footprint.MultiLevelROI; import org.geotools.coverage.grid.io.imageio.MaskOverviewProvider; import org.geotools.coverage.grid.io.imageio.MaskOverviewProvider.SpiHelper; import org.geotools.coverage.grid.io.imageio.ReadType; import org.geotools.data.DataUtilities; import org.geotools.factory.Hints; import org.geotools.factory.Hints.Key; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.image.ImageWorker; import org.geotools.image.io.ImageIOExt; import org.geotools.image.jai.Registry; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.builder.GridToEnvelopeMapper; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.AffineTransform2D; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import it.geosolutions.jaiext.vectorbin.ROIGeometry; import it.geosolutions.jaiext.vectorbin.VectorBinarizeDescriptor; import it.geosolutions.jaiext.vectorbin.VectorBinarizeRIF; import org.opengis.feature.simple.SimpleFeature; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Geometry; import it.geosolutions.imageio.imageioimpl.EnhancedImageReadParam; import it.geosolutions.imageio.maskband.DatasetLayout; import it.geosolutions.imageio.pam.PAMDataset; import it.geosolutions.imageio.pam.PAMParser; import it.geosolutions.imageio.utilities.ImageIOUtilities; import it.geosolutions.jaiext.range.NoDataContainer; /** * A granuleDescriptor is a single piece of the mosaic, with its own overviews and everything. * * <p> * This class is responsible for caching the various size of the different levels of each single granuleDescriptor since computing them each time is * expensive (opening a file, looking for a reader, parsing metadata,etc...). * * <p> * Right now we are making the assumption that a single granuleDescriptor is made a by a single file with embedded overviews, either explicit or * intrinsic through wavelets like MrSID, ECW or JPEG2000. * * @author Simone Giannecchini, GeoSolutions S.A.S. * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs * @since 2.5.5 * * * @source $URL$ */ public class GranuleDescriptor { /** Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging .getLogger(GranuleDescriptor.class); private static final String AUXFILE_EXT = ".aux.xml"; /** Hints to use for avoiding to search for the ImageMosaic format */ public static final Hints EXCLUDE_MOSAIC = new Hints(Utils.EXCLUDE_MOSAIC, true); static { try { Registry.registerRIF(JAI.getDefaultInstance(), new VectorBinarizeDescriptor(), new VectorBinarizeRIF(), Registry.JAI_TOOLS_PRODUCT); } catch (Exception e) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, e.getLocalizedMessage()); } } } OverviewsController overviewsController; private GeneralEnvelope granuleEnvelope; public GeneralEnvelope getGranuleEnvelope() { return granuleEnvelope; } public void setGranuleEnvelope(GeneralEnvelope granuleEnvelope) { this.granuleEnvelope = granuleEnvelope; } /** * This class represent an overview level in a single granuleDescriptor. * * <p> * Notice that the internal transformations for the various levels are reffered to the corner, rather than to the centre. * * @author Simone Giannecchini, GeoSolutions S.A.S. * */ class GranuleOverviewLevelDescriptor { final double scaleX; final double scaleY; final int width; final int height; final AffineTransform2D baseToLevelTransform; final AffineTransform2D gridToWorldTransformCorner; final Rectangle rasterDimensions; public AffineTransform getBaseToLevelTransform() { return baseToLevelTransform; } public double getScaleX() { return scaleX; } public double getScaleY() { return scaleY; } public int getWidth() { return width; } public int getHeight() { return height; } public GranuleOverviewLevelDescriptor(final double scaleX, final double scaleY, final int width, final int height) { this.scaleX = scaleX; this.scaleY = scaleY; this.baseToLevelTransform = new AffineTransform2D( XAffineTransform.getScaleInstance(scaleX, scaleY, 0, 0)); final AffineTransform gridToWorldTransform_ = new AffineTransform(baseToLevelTransform); gridToWorldTransform_.preConcatenate(CoverageUtilities.CENTER_TO_CORNER); gridToWorldTransform_.preConcatenate(baseGridToWorld); this.gridToWorldTransformCorner = new AffineTransform2D(gridToWorldTransform_); this.width = width; this.height = height; this.rasterDimensions = new Rectangle(0, 0, width, height); } public Rectangle getBounds() { return (Rectangle) rasterDimensions.clone(); } public AffineTransform2D getGridToWorldTransform() { return gridToWorldTransformCorner; } @Override public String toString() { // build a decent representation for this level final StringBuilder buffer = new StringBuilder(); buffer.append("Description of a granuleDescriptor level").append("\n") .append("width:\t\t").append(width).append("\n").append("height:\t\t") .append(height).append("\n").append("scaleX:\t\t").append(scaleX).append("\n") .append("scaleY:\t\t").append(scaleY).append("\n") .append("baseToLevelTransform:\t\t").append(baseToLevelTransform.toString()) .append("\n").append("gridToWorldTransform:\t\t") .append(gridToWorldTransformCorner.toString()).append("\n"); return buffer.toString(); } } /** * Simple placeholder class to store the result of a Granule Loading which comprises of a raster as well as a {@link ROIShape} for its footprint. * * @author Daniele Romagnoli, GeoSolutions S.A.S. * */ public static class GranuleLoadingResult { RenderedImage loadedImage; ROI footprint; URL granuleUrl; boolean doFiltering; PAMDataset pamDataset; GranuleDescriptor granuleDescriptor; public ROI getFootprint() { return footprint; } public RenderedImage getRaster() { return loadedImage; } public URL getGranuleUrl() { return granuleUrl; } public PAMDataset getPamDataset() { return pamDataset; } public void setPamDataset(PAMDataset pamDataset) { this.pamDataset = pamDataset; } public boolean isDoFiltering() { return doFiltering; } public GranuleDescriptor getGranuleDescriptor() { return granuleDescriptor; } GranuleLoadingResult(RenderedImage loadedImage, ROI footprint, URL granuleUrl, final boolean doFiltering, final PAMDataset pamDataset, GranuleDescriptor granuleDescriptor) { this.loadedImage = loadedImage; Object roi = loadedImage.getProperty("ROI"); if (roi instanceof ROI) { this.footprint = (ROI) roi; } this.granuleUrl = granuleUrl; this.doFiltering = doFiltering; this.pamDataset = pamDataset; this.granuleDescriptor = granuleDescriptor; } } private static PAMParser pamParser = PAMParser.getInstance(); ReferencedEnvelope granuleBBOX; MultiLevelROI roiProvider; URL granuleUrl; int maxDecimationFactor = -1; final Map<Integer, GranuleOverviewLevelDescriptor> granuleLevels = Collections .synchronizedMap(new HashMap<Integer, GranuleOverviewLevelDescriptor>()); AffineTransform baseGridToWorld; ImageReaderSpi cachedReaderSPI; private SimpleFeature originator; PAMDataset pamDataset; boolean handleArtifactsFiltering = false; boolean filterMe = false; ImageInputStreamSpi cachedStreamSPI; private GridToEnvelopeMapper geMapper; /** {@link DatasetLayout} object containing information about granule internal structure */ private DatasetLayout layout; /** * {@link MaskOverviewProvider} used for handling external ROIs and Overviews */ private MaskOverviewProvider ovrProvider; protected void init(final BoundingBox granuleBBOX, final URL granuleUrl, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final boolean heterogeneousGranules, final boolean handleArtifactsFiltering, final Hints hints) { this.granuleBBOX = ReferencedEnvelope.reference(granuleBBOX); this.granuleUrl = granuleUrl; this.roiProvider = roiProvider; this.handleArtifactsFiltering = handleArtifactsFiltering; filterMe = handleArtifactsFiltering && roiProvider != null; // When looking for formats which may parse this file, make sure to exclude the ImageMosaicFormat as return File granuleFile = DataUtilities.urlToFile(granuleUrl); AbstractGridFormat format = GridFormatFinder.findFormat(granuleFile, EXCLUDE_MOSAIC); // create the base grid to world transformation AbstractGridCoverage2DReader gcReader = null; ImageInputStream inStream = null; ImageReader reader = null; try { gcReader = format.getReader(granuleFile, hints); // Getting Dataset Layout layout = gcReader.getDatasetLayout(); if(heterogeneousGranules) { // do not trust the index, use the reader instead (reprojection might be involved) this.granuleBBOX = ReferencedEnvelope.reference(gcReader.getOriginalEnvelope()); } // // get info about the raster we have to read // SpiHelper spiProvider = new SpiHelper(granuleFile, suggestedSPI); boolean isMultidim = spiProvider.isMultidim(); GeneralEnvelope envelope = gcReader.getOriginalEnvelope(); this.granuleEnvelope = envelope; ovrProvider = new MaskOverviewProvider(layout, granuleFile, spiProvider); // get a stream if (cachedStreamSPI == null) { cachedStreamSPI = ovrProvider.getInputStreamSpi(); } assert cachedStreamSPI != null : "no cachedStreamSPI available!"; inStream = cachedStreamSPI.createInputStreamInstance(granuleUrl, ImageIO.getUseCache(), ImageIO.getCacheDirectory()); if (inStream == null) { final File file = DataUtilities.urlToFile(granuleUrl); if (file != null) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log(Level.WARNING, Utils.getFileInfo(file)); } } throw new IllegalArgumentException( "Unable to get an input stream for the provided file " + granuleUrl.toString()); } // get a reader and try to cache the suggested SPI first if (cachedReaderSPI == null) { cachedReaderSPI = ovrProvider.getImageReaderSpi(); } if (reader == null) { if (cachedReaderSPI == null) { throw new IllegalArgumentException( "Unable to get a ReaderSPI for the provided input: " + granuleUrl.toString()); } reader = cachedReaderSPI.createReaderInstance(); } if (reader == null) throw new IllegalArgumentException( "Unable to get an ImageReader for the provided file " + granuleUrl.toString()); boolean ignoreMetadata = isMultidim ? customizeReaderInitialization(reader, hints) : false; reader.setInput(inStream, false, ignoreMetadata); // get selected level and base level dimensions final Rectangle originalDimension = Utils.getDimension(0, reader); // build the g2W for this tile, in principle we should get it // somehow from the tile itself or from the index, but at the moment // we do not have such info, hence we assume that it is a simple // scale and translate this.geMapper = new GridToEnvelopeMapper(new GridEnvelope2D(originalDimension), this.granuleBBOX); geMapper.setPixelAnchor(PixelInCell.CELL_CENTER);// this is the default behavior but it is nice to write it down anyway this.baseGridToWorld = geMapper.createAffineTransform(); // add the base level this.granuleLevels.put(Integer.valueOf(0), new GranuleOverviewLevelDescriptor(1, 1, originalDimension.width, originalDimension.height)); ////////////////////// Setting overviewController /////////////////////// if (heterogeneousGranules) { // // // // Right now we are setting up overviewsController by assuming that // overviews are internal images as happens in TIFF images // We can improve this by leveraging on coverageReaders // // // // Getting the first level descriptor final GranuleOverviewLevelDescriptor baseOverviewLevelDescriptor = granuleLevels .get(0); // Variables initialization final int numberOfOvervies = ovrProvider.getNumOverviews(); final AffineTransform2D baseG2W = baseOverviewLevelDescriptor .getGridToWorldTransform(); final int width = baseOverviewLevelDescriptor.getWidth(); final int height = baseOverviewLevelDescriptor.getHeight(); final double resX = AffineTransform2D.getScaleX0(baseG2W); final double resY = AffineTransform2D.getScaleY0(baseG2W); final double[] highestRes = new double[] { resX, resY }; // Populating overviews and initializing overviewsController final double[][] overviewsResolution = ovrProvider .getOverviewResolutions(highestRes[0] * width, highestRes[1] * height); overviewsController = new OverviewsController(highestRes, numberOfOvervies, overviewsResolution); } ////////////////////////////////////////////////////////////////////////// if (hints != null && hints.containsKey(Utils.CHECK_AUXILIARY_METADATA)) { boolean checkAuxiliaryMetadata = (Boolean) hints .get(Utils.CHECK_AUXILIARY_METADATA); if (checkAuxiliaryMetadata) { checkPamDataset(); } } } catch (IllegalStateException e) { throw new IllegalArgumentException(e); } catch (IOException e) { throw new IllegalArgumentException(e); } finally { // close/dispose stream and readers try { if (inStream != null) { inStream.close(); } } catch (Throwable e) { throw new IllegalArgumentException(e); } finally { if (reader != null) { reader.dispose(); } } if (gcReader != null) { try { gcReader.dispose(); } catch (Throwable t) { // Ignore it } } } } public OverviewsController getOverviewsController() { return overviewsController; } /** * Look for GDAL Auxiliary File and unmarshall it to setup a PamDataset if available * * @throws IOException */ private void checkPamDataset() throws IOException { final File file = DataUtilities.urlToFile(granuleUrl); final String path = file.getCanonicalPath(); final String auxFile = path + AUXFILE_EXT; pamDataset = pamParser.parsePAM(auxFile); } private boolean customizeReaderInitialization(ImageReader reader, Hints hints) { // Special Management for NetCDF readers to set external Auxiliary File if (hints != null && (hints.containsKey(Utils.AUXILIARY_FILES_PATH) || hints.containsKey(Utils.AUXILIARY_DATASTORE_PATH))) { try { updateReaderWithAuxiliaryPath(hints, reader, Utils.AUXILIARY_FILES_PATH, "setAuxiliaryFilesPath"); updateReaderWithAuxiliaryPath(hints, reader, Utils.AUXILIARY_DATASTORE_PATH, "setAuxiliaryDatastorePath"); return true; } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } return false; } private void updateReaderWithAuxiliaryPath(Hints hints, ImageReader reader, Key key, String method) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { String filePath = (String) hints.get(key); if (filePath != null && hints.containsKey(Utils.PARENT_DIR)) { String parentDir = (String) hints.get(Utils.PARENT_DIR); // if the path starts with the parentDir, it's already absolute (old configuration file) if (!filePath.startsWith(parentDir)) { filePath = parentDir + File.separatorChar + filePath; } } if (filePath != null) { MethodUtils.invokeMethod(reader, method, filePath); } } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider) { this(granuleLocation, granuleBBox, suggestedSPI, roiProvider, -1, false); } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final boolean heterogeneousGranules) { this(granuleLocation, granuleBBox, suggestedSPI, roiProvider, -1, heterogeneousGranules); } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final int maxDecimationFactor) { this(granuleLocation, granuleBBox, suggestedSPI, roiProvider, maxDecimationFactor, false); } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final int maxDecimationFactor, final boolean heterogeneousGranules) { this(granuleLocation, granuleBBox, suggestedSPI, roiProvider, maxDecimationFactor, heterogeneousGranules, false); } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final int maxDecimationFactor, final boolean heterogeneousGranules, final boolean handleArtifactsFiltering) { this(granuleLocation, granuleBBox, suggestedSPI, roiProvider, maxDecimationFactor, heterogeneousGranules, handleArtifactsFiltering, null); } public GranuleDescriptor(final String granuleLocation, final BoundingBox granuleBBox, final ImageReaderSpi suggestedSPI, final MultiLevelROI roiProvider, final int maxDecimationFactor, final boolean heterogeneousGranules, final boolean handleArtifactsFiltering, final Hints hints) { this.maxDecimationFactor = maxDecimationFactor; final URL rasterFile = DataUtilities.fileToURL(new File(granuleLocation)); if (rasterFile == null) { return; } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("File found " + granuleLocation); } this.originator = null; init(granuleBBox, rasterFile, suggestedSPI, roiProvider, heterogeneousGranules, handleArtifactsFiltering, hints); } /** * * @param feature * @param suggestedSPI * @param pathType * @param locationAttribute * @param parentLocation */ public GranuleDescriptor(final SimpleFeature feature, final ImageReaderSpi suggestedSPI, final PathType pathType, final String locationAttribute, final String parentLocation) { this(feature, suggestedSPI, pathType, locationAttribute, parentLocation, false); } public GranuleDescriptor(SimpleFeature feature, ImageReaderSpi suggestedSPI, PathType pathType, String locationAttribute, String parentLocation, boolean heterogeneousGranules, Hints hints) { this(feature, suggestedSPI, pathType, locationAttribute, parentLocation, null, heterogeneousGranules, hints); } public GranuleDescriptor(SimpleFeature feature, ImageReaderSpi suggestedSPI, PathType pathType, String locationAttribute, String parentLocation, boolean heterogeneousGranules) { this(feature, suggestedSPI, pathType, locationAttribute, parentLocation, heterogeneousGranules, null); } /** * Constructor for the {@link GranuleDescriptor} assuming it doesn't belong to an heterogeneous granules set. * * @param feature a {@link SimpleFeature} referring to that granule * @param suggestedSPI the suggested {@link ImageReaderSpi} to be used to get a reader to handle this granule. * @param pathType A {@link PathType} identifying if the granule location should be resolved as a relative or an absolute path. * @param locationAttribute the attribute containing the granule location. * @param parentLocation the location of the parent of that granule. * @param inclusionGeometry the footprint of that granule (if any). It may be null. */ public GranuleDescriptor(SimpleFeature feature, ImageReaderSpi suggestedSPI, PathType pathType, final String locationAttribute, final String parentLocation, final MultiLevelROI roiProvider) { this(feature, suggestedSPI, pathType, locationAttribute, parentLocation, roiProvider, false, null); } /** * Constructor for the {@link GranuleDescriptor} * * @param feature a {@link SimpleFeature} referring to that granule * @param suggestedSPI the suggested {@link ImageReaderSpi} to be used to get a reader to handle this granule. * @param pathType A {@link PathType} identifying if the granule location should be resolved as a relative or an absolute path. * @param locationAttribute the attribute containing the granule location. * @param parentLocation the location of the parent of that granule. * @param inclusionGeometry the footprint of that granule (if any). It may be null. * @param heterogeneousGranules if {@code true}, this granule belongs to a set of heterogeneous granules */ public GranuleDescriptor(final SimpleFeature feature, final ImageReaderSpi suggestedSPI, final PathType pathType, final String locationAttribute, final String parentLocation, final MultiLevelROI roiProvider, final boolean heterogeneousGranules, final Hints hints) { // Get location and envelope of the image to load. final String granuleLocation = (String) feature.getAttribute(locationAttribute); final ReferencedEnvelope granuleBBox = getFeatureBounds(feature); // If the granuleDescriptor is not there, dump a message and continue final URL rasterFile = pathType.resolvePath(parentLocation, granuleLocation); if (rasterFile == null) { throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "granuleLocation", granuleLocation)); } if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("File found " + granuleLocation); this.originator = feature; init(granuleBBox, rasterFile, suggestedSPI, roiProvider, heterogeneousGranules, false, hints); } /** * Extracts the referenced envelope of the default geometry (used to be feature.getBounds, but that method returns the bounds of all geometries in * the feature) * * @param feature * @return */ private ReferencedEnvelope getFeatureBounds(final SimpleFeature feature) { Geometry g = (Geometry) feature.getDefaultGeometry(); if (g == null) { return null; } CoordinateReferenceSystem crs = feature.getFeatureType().getCoordinateReferenceSystem(); ReferencedEnvelope granuleBBox = new ReferencedEnvelope(g.getEnvelopeInternal(), crs); return granuleBBox; } /** * Load a specified a raster as a portion of the granule describe by this {@link GranuleDescriptor}. * * @param imageReadParameters the {@link ImageReadParam} to use for reading. * @param index the index to use for the {@link ImageReader}. * @param cropBBox the bbox to use for cropping. * @param mosaicWorldToGrid the cropping grid to world transform. * @param request the incoming request to satisfy. * @param hints {@link Hints} to be used for creating this raster. * @return a specified a raster as a portion of the granule describe by this {@link GranuleDescriptor}. * @throws IOException in case an error occurs. */ public GranuleLoadingResult loadRaster(final ImageReadParam imageReadParameters, final int index, final ReferencedEnvelope cropBBox, final MathTransform2D mosaicWorldToGrid, final RasterLayerRequest request, final Hints hints) throws IOException { if (LOGGER.isLoggable(java.util.logging.Level.FINER)) { final String name = Thread.currentThread().getName(); LOGGER.finer("Thread:" + name + " Loading raster data for granuleDescriptor " + this.toString()); } ImageReadParam readParameters = null; int imageIndex; final boolean useFootprint = roiProvider != null && request.getFootprintBehavior() != FootprintBehavior.None; Geometry inclusionGeometry = useFootprint ? roiProvider.getFootprint() : null; final ReferencedEnvelope bbox = useFootprint ? new ReferencedEnvelope( granuleBBOX.intersection(inclusionGeometry.getEnvelopeInternal()), granuleBBOX.getCoordinateReferenceSystem()) : granuleBBOX; boolean doFiltering = false; if (filterMe && useFootprint) { doFiltering = Utils.areaIsDifferent(inclusionGeometry, baseGridToWorld, granuleBBOX); } // intersection of this tile bound with the current crop bbox final ReferencedEnvelope intersection = new ReferencedEnvelope(bbox.intersection(cropBBox), cropBBox.getCoordinateReferenceSystem()); if (intersection.isEmpty()) { if (LOGGER.isLoggable(java.util.logging.Level.FINE)) { LOGGER.fine(new StringBuilder("Got empty intersection for granule ") .append(this.toString()).append(" with request ").append(request.toString()) .append(" Resulting in no granule loaded: Empty result").toString()); } return null; } // check if the requested bbox intersects or overlaps the requested area if (useFootprint && inclusionGeometry != null && !JTS.toGeometry(cropBBox).intersects(inclusionGeometry)) { if (LOGGER.isLoggable(java.util.logging.Level.FINE)) { LOGGER.fine(new StringBuilder("Got empty intersection for granule ") .append(this.toString()).append(" with request ").append(request.toString()) .append(" Resulting in no granule loaded: Empty result").toString()); } return null; } ImageInputStream inStream = null; ImageReader reader = null; boolean cleanupInFinally = request.getReadType() != ReadType.JAI_IMAGEREAD; try { // // get info about the raster we have to read // // get a stream assert cachedStreamSPI != null : "no cachedStreamSPI available!"; inStream = cachedStreamSPI.createInputStreamInstance(granuleUrl, ImageIO.getUseCache(), ImageIO.getCacheDirectory()); if (inStream == null) return null; // get a reader and try to cache the relevant SPI if (cachedReaderSPI == null) { reader = ImageIOExt.getImageioReader(inStream); if (reader != null) cachedReaderSPI = reader.getOriginatingProvider(); } else reader = cachedReaderSPI.createReaderInstance(); if (reader == null) { if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) { LOGGER.warning( new StringBuilder("Unable to get s reader for granuleDescriptor ") .append(this.toString()).append(" with request ") .append(request.toString()) .append(" Resulting in no granule loaded: Empty result") .toString()); } return null; } // set input customizeReaderInitialization(reader, hints); reader.setInput(inStream); // check if the reader wants to be aware of the current request if (MethodUtils.getAccessibleMethod(reader.getClass(), "setRasterLayerRequest", RasterLayerRequest.class) != null) { try { MethodUtils.invokeMethod(reader, "setRasterLayerRequest", request); } catch(Exception exception) { throw new RuntimeException("Error setting raster layer request on reader.", exception); } } // Checking for heterogeneous granules and if the mosaic is not multidimensional if (request.isHeterogeneousGranules() && (originator == null || originator.getAttribute("imageindex") == null)) { // create read parameters readParameters = new ImageReadParam(); // override the overviews controller for the base layer imageIndex = ReadParamsController.setReadParams( request.spatialRequestHelper.getComputedResolution(), request.getOverviewPolicy(), request.getDecimationPolicy(), readParameters, request.rasterManager, overviewsController); } else { imageIndex = index; readParameters = imageReadParameters; } // Defining an Overview Index value int ovrIndex = imageIndex; boolean isExternal = ovrProvider.isExternalOverview(imageIndex); // Define a new URL to use (it may change if using external overviews) URL granuleURLUpdated = granuleUrl; // If the file is external we must update the Granule elements if (isExternal) { // Disposing File Reader and Stream try { if (inStream != null) { inStream.close(); } } finally { if (reader != null) { reader.dispose(); } } granuleURLUpdated = ovrProvider.getOvrURL(); assert ovrProvider .getExternalOverviewInputStreamSpi() != null : "no cachedStreamSPI available for external overview!"; inStream = ovrProvider.getExternalOverviewInputStreamSpi() .createInputStreamInstance(granuleURLUpdated, ImageIO.getUseCache(), ImageIO.getCacheDirectory()); // get a reader and try to cache the relevant SPI reader = ovrProvider.getImageReaderSpi().createReaderInstance(); if (reader == null) { if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) { LOGGER.warning( new StringBuilder("Unable to get s reader for granuleDescriptor ") .append(this.toString()).append(" with request ") .append(request.toString()) .append(" Resulting in no granule loaded: Empty result") .toString()); } return null; } // set input reader.setInput(inStream, false, false); // External Overview index ovrIndex = ovrProvider.getOverviewIndex(imageIndex); } else { ovrIndex = ovrProvider.getOverviewIndex(imageIndex); } // get selected level and base level dimensions final GranuleOverviewLevelDescriptor selectedlevel = getLevel(ovrIndex, reader, imageIndex, isExternal); // now create the crop grid to world which can be used to decide // which source area we need to crop in the selected level taking // into account the scale factors imposed by the selection of this // level together with the base level grid to world transformation AffineTransform2D cropWorldToGrid = new AffineTransform2D( selectedlevel.gridToWorldTransformCorner); cropWorldToGrid = (AffineTransform2D) cropWorldToGrid.inverse(); // computing the crop source area which lives into the // selected level raster space, NOTICE that at the end we need to // take into account the fact that we might also decimate therefore // we cannot just use the crop grid to world but we need to correct // it. Rectangle2D r2d = CRS.transform(cropWorldToGrid, intersection).toRectangle2D(); // if we are reading basically nothing, bail out immediately if (r2d.getWidth() < 0.1 || r2d.getHeight() < 0.1) { cleanupInFinally = true; return null; } final Rectangle sourceArea = r2d.getBounds(); // gutter if (selectedlevel.baseToLevelTransform.isIdentity()) { sourceArea.grow(2, 2); } XRectangle2D.intersect(sourceArea, selectedlevel.rasterDimensions, sourceArea);// make sure roundings don't bother us // is it empty?? if (sourceArea.isEmpty()) { if (LOGGER.isLoggable(java.util.logging.Level.FINE)) { LOGGER.fine("Got empty area for granuleDescriptor " + this.toString() + " with request " + request.toString() + " Resulting in no granule loaded: Empty result"); } return null; } else if (LOGGER.isLoggable(java.util.logging.Level.FINER)) { LOGGER.finer("Loading level " + imageIndex + " with source region: " + sourceArea + " subsampling: " + readParameters.getSourceXSubsampling() + "," + readParameters.getSourceYSubsampling() + " for granule:" + granuleUrl); } // Setting subsampling int newSubSamplingFactor = 0; final String pluginName = cachedReaderSPI.getPluginClassName(); if (pluginName != null && pluginName.equals(ImageUtilities.DIRECT_KAKADU_PLUGIN)) { final int ssx = readParameters.getSourceXSubsampling(); final int ssy = readParameters.getSourceYSubsampling(); newSubSamplingFactor = ImageIOUtilities.getSubSamplingFactor2(ssx, ssy); if (newSubSamplingFactor != 0) { if (newSubSamplingFactor > maxDecimationFactor && maxDecimationFactor != -1) { newSubSamplingFactor = maxDecimationFactor; } readParameters.setSourceSubsampling(newSubSamplingFactor, newSubSamplingFactor, 0, 0); } } // set the source region readParameters.setSourceRegion(sourceArea); // don't pass down the band selection if the original color model is indexed and // color expansion is enabled final boolean expandToRGB = request.getRasterManager().isExpandMe(); if(expandToRGB && getRawColorModel(reader, ovrIndex) instanceof IndexColorModel && readParameters instanceof EnhancedImageReadParam) { EnhancedImageReadParam erp = (EnhancedImageReadParam) readParameters; erp.setBands(null); } RenderedImage raster; try { // read raster = request.getReadType().read(readParameters, ovrIndex, granuleURLUpdated, selectedlevel.rasterDimensions, reader, hints, false); } catch (Throwable e) { if (LOGGER.isLoggable(java.util.logging.Level.FINE)) { LOGGER.log(java.util.logging.Level.FINE, "Unable to load raster for granuleDescriptor " + this.toString() + " with request " + request.toString() + " Resulting in no granule loaded: Empty result", e); } return null; } // perform band selection if necessary, so far netcdf is the only low level reader that // handles bands selection, if more readers start to support it a decent approach should // be used to know if the low level reader already performed the bands selection or if // image mosaic is responsible for do it if(request.getBands() != null && !reader.getFormatName().equalsIgnoreCase("netcdf")) { // if we are expanding the color model, do so before selecting the bands if(raster.getColorModel() instanceof IndexColorModel && expandToRGB) { raster = new ImageWorker(raster).forceComponentColorModel().getRenderedImage(); } int[] bands = request.getBands(); // delegate the band selection operation on JAI BandSelect operation raster = new ImageWorker(raster).retainBands(bands).getRenderedImage(); ColorModel colorModel = raster.getColorModel(); if (colorModel == null) { ImageLayout layout = (ImageLayout) hints.get(JAI.KEY_IMAGE_LAYOUT); if (layout == null) { layout = new ImageLayout(); } ColorModel newColorModel =ImageIOUtilities.createColorModel(raster.getSampleModel()); if(newColorModel != null) { layout.setColorModel(newColorModel); raster = new ImageWorker(raster).setRenderingHints(hints).format(raster.getSampleModel().getDataType()).getRenderedImage(); } } } // use fixed source area sourceArea.setRect(readParameters.getSourceRegion()); // // setting new coefficients to define a new affineTransformation // to be applied to the grid to world transformation // ----------------------------------------------------------------------------------- // // With respect to the original envelope, the obtained planarImage // needs to be rescaled. The scaling factors are computed as the // ratio between the cropped source region sizes and the read // image sizes. // // place it in the mosaic using the coords created above; double decimationScaleX = ((1.0 * sourceArea.width) / raster.getWidth()); double decimationScaleY = ((1.0 * sourceArea.height) / raster.getHeight()); final AffineTransform decimationScaleTranform = XAffineTransform .getScaleInstance(decimationScaleX, decimationScaleY); // keep into account translation to work into the selected level raster space final AffineTransform afterDecimationTranslateTranform = XAffineTransform .getTranslateInstance(sourceArea.x, sourceArea.y); // now we need to go back to the base level raster space final AffineTransform backToBaseLevelScaleTransform = selectedlevel.baseToLevelTransform; // now create the overall transform final AffineTransform finalRaster2Model = new AffineTransform(baseGridToWorld); finalRaster2Model.concatenate(CoverageUtilities.CENTER_TO_CORNER); if (!XAffineTransform.isIdentity(backToBaseLevelScaleTransform, CoverageUtilities.AFFINE_IDENTITY_EPS)) finalRaster2Model.concatenate(backToBaseLevelScaleTransform); if (!XAffineTransform.isIdentity(afterDecimationTranslateTranform, CoverageUtilities.AFFINE_IDENTITY_EPS)) finalRaster2Model.concatenate(afterDecimationTranslateTranform); if (!XAffineTransform.isIdentity(decimationScaleTranform, CoverageUtilities.AFFINE_IDENTITY_EPS)) finalRaster2Model.concatenate(decimationScaleTranform); // adjust roi if (useFootprint) { ROI transformed; try { // Getting Image Bounds Rectangle imgBounds = new Rectangle(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight()); // Getting Transformed ROI transformed = roiProvider.getTransformedROI(finalRaster2Model.createInverse(), imageIndex, imgBounds, readParameters, request.getReadType()); // Check for vectorial ROI if (transformed instanceof ROIGeometry && ((ROIGeometry) transformed).getAsGeometry().isEmpty()) { // inset might have killed the geometry fully return null; } // Check for Raster ROI if (transformed == null || transformed.getBounds().isEmpty()) { if (LOGGER.isLoggable(java.util.logging.Level.INFO)) LOGGER.info("Unable to create a granuleDescriptor " + this.toString() + " due to a problem when managing the ROI"); return null; } PlanarImage pi = PlanarImage.wrapRenderedImage(raster); if (!transformed.intersects(pi.getBounds())) { return null; } pi.setProperty("ROI", transformed); raster = pi; } catch (NoninvertibleTransformException e) { if (LOGGER.isLoggable(java.util.logging.Level.INFO)) LOGGER.info("Unable to create a granuleDescriptor " + this.toString() + " due to a problem when managing the ROI"); return null; } } // keep into account translation factors to place this tile finalRaster2Model.preConcatenate((AffineTransform) mosaicWorldToGrid); final Interpolation interpolation = request.getInterpolation(); // paranoiac check to avoid that JAI freaks out when computing its internal layouT on images that are too small Rectangle2D finalLayout = ImageUtilities.layoutHelper(raster, (float) finalRaster2Model.getScaleX(), (float) finalRaster2Model.getScaleY(), (float) finalRaster2Model.getTranslateX(), (float) finalRaster2Model.getTranslateY(), interpolation); if (finalLayout.isEmpty()) { if (LOGGER.isLoggable(java.util.logging.Level.INFO)) LOGGER.info("Unable to create a granuleDescriptor " + this.toString() + " due to jai scale bug creating a null source area"); return null; } // apply the affine transform conserving indexed color model final RenderingHints localHints = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, interpolation instanceof InterpolationNearest ? Boolean.FALSE : Boolean.TRUE); if (XAffineTransform.isIdentity(finalRaster2Model, CoverageUtilities.AFFINE_IDENTITY_EPS)) { return new GranuleLoadingResult(raster, null, granuleURLUpdated, doFiltering, pamDataset, this); } else { // // In case we are asked to use certain tile dimensions we tile // also at this stage in case the read type is Direct since // buffered images comes up untiled and this can affect the // performances of the subsequent affine operation. // final Dimension tileDimensions = request.getTileDimensions(); if (tileDimensions != null && request.getReadType().equals(ReadType.DIRECT_READ)) { final ImageLayout layout = new ImageLayout(); layout.setTileHeight(tileDimensions.width).setTileWidth(tileDimensions.height); localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout)); } else { if (hints != null && hints.containsKey(JAI.KEY_IMAGE_LAYOUT)) { final Object layout = hints.get(JAI.KEY_IMAGE_LAYOUT); if (layout != null && layout instanceof ImageLayout) { localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, ((ImageLayout) layout).clone())); } } } if (hints != null && hints.containsKey(JAI.KEY_TILE_CACHE)) { final Object cache = hints.get(JAI.KEY_TILE_CACHE); if (cache != null && cache instanceof TileCache) localHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, cache)); } if (hints != null && hints.containsKey(JAI.KEY_TILE_SCHEDULER)) { final Object scheduler = hints.get(JAI.KEY_TILE_SCHEDULER); if (scheduler != null && scheduler instanceof TileScheduler) localHints.add(new RenderingHints(JAI.KEY_TILE_SCHEDULER, scheduler)); } boolean addBorderExtender = true; if (hints != null && hints.containsKey(JAI.KEY_BORDER_EXTENDER)) { final Object extender = hints.get(JAI.KEY_BORDER_EXTENDER); if (extender != null && extender instanceof BorderExtender) { localHints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER, extender)); addBorderExtender = false; } } // BORDER extender if (addBorderExtender) { localHints.add(ImageUtilities.BORDER_EXTENDER_HINTS); } ImageWorker iw = new ImageWorker(raster); iw.setRenderingHints(localHints); iw.affine(finalRaster2Model, interpolation, request.getBackgroundValues()); RenderedImage renderedImage = iw.getRenderedImage(); Object roi = renderedImage.getProperty("ROI"); if (useFootprint && (roi instanceof ROIGeometry && ((ROIGeometry) roi).getAsGeometry().isEmpty()) || (roi instanceof ROI && ((ROI) roi).getBounds().isEmpty())) { // JAI not only transforms the ROI, but may also apply clipping to the image boundary // this results in an empty ROI in some edge cases return null; } // Propagate NoData if (iw.getNoData() != null) { PlanarImage t = PlanarImage.wrapRenderedImage(renderedImage); t.setProperty(NoDataContainer.GC_NODATA, new NoDataContainer(iw.getNoData())); renderedImage = t; } return new GranuleLoadingResult(renderedImage, null, granuleURLUpdated, doFiltering, pamDataset, this); } } catch (IllegalStateException e) { if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) { LOGGER.log(java.util.logging.Level.WARNING, new StringBuilder("Unable to load raster for granuleDescriptor ") .append(this.toString()).append(" with request ") .append(request.toString()) .append(" Resulting in no granule loaded: Empty result").toString(), e); } return null; } catch (org.opengis.referencing.operation.NoninvertibleTransformException e) { if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) { LOGGER.log(java.util.logging.Level.WARNING, new StringBuilder("Unable to load raster for granuleDescriptor ") .append(this.toString()).append(" with request ") .append(request.toString()) .append(" Resulting in no granule loaded: Empty result").toString(), e); } return null; } catch (TransformException e) { if (LOGGER.isLoggable(java.util.logging.Level.WARNING)) { LOGGER.log(java.util.logging.Level.WARNING, new StringBuilder("Unable to load raster for granuleDescriptor ") .append(this.toString()).append(" with request ") .append(request.toString()) .append(" Resulting in no granule loaded: Empty result").toString(), e); } return null; } finally { try { if (cleanupInFinally && inStream != null) { inStream.close(); } } finally { if (cleanupInFinally && reader != null) { reader.dispose(); } } } } /** * Returns the raw color model of the reader at the specified image index * @param reader * @param imageIndex * @return */ private ColorModel getRawColorModel(ImageReader reader, int imageIndex) { try { ImageTypeSpecifier imageType = reader.getRawImageType(imageIndex); if(imageType == null) { return null; } ColorModel cm = imageType.getColorModel(); return cm; } catch(Exception e) { LOGGER.log(Level.FINE, "Failed to determine the native color model of the reader", e); } return null; } private GranuleOverviewLevelDescriptor getLevel(final int index, final ImageReader reader, final int imageIndex, final boolean external) { // Level index may change if using external overviews int indexValue = external ? imageIndex : index; if (reader == null) throw new NullPointerException( "Null reader passed to the internal GranuleOverviewLevelDescriptor method"); synchronized (granuleLevels) { if (granuleLevels.containsKey(Integer.valueOf(indexValue))) return granuleLevels.get(Integer.valueOf(indexValue)); else { // load level // create the base grid to world transformation try { // // get info about the raster we have to read // // get selected level and base level dimensions final Rectangle levelDimension = Utils.getDimension(index, reader); final GranuleOverviewLevelDescriptor baseLevel = granuleLevels.get(0); final double scaleX = baseLevel.width / (1.0 * levelDimension.width); final double scaleY = baseLevel.height / (1.0 * levelDimension.height); // add the base level final GranuleOverviewLevelDescriptor newLevel = new GranuleOverviewLevelDescriptor( scaleX, scaleY, levelDimension.width, levelDimension.height); this.granuleLevels.put(Integer.valueOf(indexValue), newLevel); return newLevel; } catch (IllegalStateException e) { throw new IllegalArgumentException(e); } catch (IOException e) { throw new IllegalArgumentException(e); } } } } GranuleOverviewLevelDescriptor getLevel(final int index) { // load level // create the base grid to world transformation ImageInputStream inStream = null; ImageReader reader = null; try { // get a stream assert cachedStreamSPI != null : "no cachedStreamSPI available!"; inStream = cachedStreamSPI.createInputStreamInstance(granuleUrl, ImageIO.getUseCache(), ImageIO.getCacheDirectory()); if (inStream == null) throw new IllegalArgumentException( "Unable to create an inputstream for the granuleurl:" + (granuleUrl != null ? granuleUrl : "null")); // get a reader and try to cache the relevant SPI if (cachedReaderSPI == null) { reader = ImageIOExt.getImageioReader(inStream); if (reader != null) cachedReaderSPI = reader.getOriginatingProvider(); } else reader = cachedReaderSPI.createReaderInstance(); if (reader == null) throw new IllegalArgumentException( "Unable to get an ImageReader for the provided file " + granuleUrl.toString()); final boolean ignoreMetadata = customizeReaderInitialization(reader, null); reader.setInput(inStream, false, ignoreMetadata); // call internal method which will close everything return getLevel(index, reader, index, false); } catch (IllegalStateException e) { // clean up try { if (inStream != null) inStream.close(); } catch (Throwable ee) { } finally { if (reader != null) reader.dispose(); } throw new IllegalArgumentException(e); } catch (IOException e) { // clean up try { if (inStream != null) inStream.close(); } catch (Throwable ee) { } finally { if (reader != null) reader.dispose(); } throw new IllegalArgumentException(e); } } @Override public String toString() { // build a decent representation for this level final StringBuilder buffer = new StringBuilder(); buffer.append("Description of a granuleDescriptor ").append("\n"); buffer.append("BBOX:\t\t").append(granuleBBOX.toString()).append("\n"); buffer.append("file:\t\t").append(granuleUrl).append("\n"); buffer.append("gridToWorld:\t\t").append(baseGridToWorld).append("\n"); int i = 1; for (final GranuleOverviewLevelDescriptor granuleOverviewLevelDescriptor : granuleLevels .values()) { i++; buffer.append("Description of level ").append(i++).append("\n"); buffer.append(granuleOverviewLevelDescriptor.toString()).append("\n"); } return buffer.toString(); } public BoundingBox getGranuleBBOX() { return granuleBBOX; } public URL getGranuleUrl() { return granuleUrl; } public SimpleFeature getOriginator() { return originator; } public Geometry getFootprint() { if (roiProvider == null) { return null; } else { return roiProvider.getFootprint(); } } }