Java tutorial
/* * 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.gce.imagemosaic; import java.awt.Color; 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.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.MultiPixelPackedSampleModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageReadParam; import javax.measure.unit.Unit; import javax.media.jai.BorderExtender; import javax.media.jai.Histogram; import javax.media.jai.ImageLayout; import javax.media.jai.Interpolation; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.ROI; import javax.media.jai.RenderedOp; import javax.media.jai.TileCache; import javax.media.jai.TileScheduler; import javax.media.jai.operator.ConstantDescriptor; import javax.media.jai.operator.FormatDescriptor; import javax.media.jai.operator.MosaicDescriptor; import javax.media.jai.operator.TranslateDescriptor; import org.apache.commons.io.FilenameUtils; import org.geotools.coverage.Category; import org.geotools.coverage.GridSampleDimension; import org.geotools.coverage.TypeMap; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridCoverageFactory; import org.geotools.coverage.grid.GridEnvelope2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader; import org.geotools.data.DataSourceException; import org.geotools.data.DataUtilities; import org.geotools.data.Query; import org.geotools.factory.Hints; import org.geotools.feature.visitor.MaxVisitor; import org.geotools.filter.IllegalFilterException; import org.geotools.filter.SortByImpl; import org.geotools.gce.imagemosaic.GranuleDescriptor.GranuleLoadingResult; import org.geotools.gce.imagemosaic.ImageMosaicReader.DomainDescriptor; import org.geotools.gce.imagemosaic.ImageMosaicReader.DomainManager; import org.geotools.gce.imagemosaic.OverviewsController.OverviewLevel; import org.geotools.gce.imagemosaic.catalog.GranuleCatalogVisitor; import org.geotools.gce.imagemosaic.processing.ArtifactsFilterDescriptor; import org.geotools.geometry.Envelope2D; 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.referencing.CRS; import org.geotools.referencing.operation.matrix.XAffineTransform; import org.geotools.referencing.operation.transform.AffineTransform2D; import org.geotools.resources.coverage.CoverageUtilities; import org.geotools.resources.coverage.FeatureUtilities; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; import org.geotools.resources.image.ImageUtilities; import org.geotools.util.NumberRange; import org.geotools.util.SimpleInternationalString; import org.jaitools.imageutils.ImageLayout2; import org.jaitools.imageutils.ROIGeometry; import org.opengis.coverage.ColorInterpretation; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.SampleDimensionType; import org.opengis.coverage.grid.GridCoverage; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.filter.Filter; import org.opengis.filter.expression.Expression; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.datum.PixelInCell; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransform1D; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.util.InternationalString; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; /** * A RasterLayerResponse. An instance of this class is produced everytime a * requestCoverage is called to a reader. * * @author Simone Giannecchini, GeoSolutions * @author Daniele Romagnoli, GeoSolutions * @author Stefan Alfons Krueger (alfonx), Wikisquare.de : Support for jar:file:foo.jar/bar.properties URLs */ @SuppressWarnings("rawtypes") class RasterLayerResponse { private static final class SimplifiedGridSampleDimension extends GridSampleDimension implements SampleDimension { /** * */ private static final long serialVersionUID = 2227219522016820587L; private double nodata; private double minimum; private double maximum; private double scale; private double offset; private Unit<?> unit; private SampleDimensionType type; private ColorInterpretation color; private Category bkg; public SimplifiedGridSampleDimension(CharSequence description, SampleDimensionType type, ColorInterpretation color, double nodata, double minimum, double maximum, double scale, double offset, Unit<?> unit) { super(description, !Double.isNaN(nodata) ? new Category[] { new Category(Vocabulary.formatInternational(VocabularyKeys.NODATA), new Color[] { new Color(0, 0, 0, 0) }, NumberRange.create(nodata, nodata), NumberRange.create(nodata, nodata)) } : null, unit); this.nodata = nodata; this.minimum = minimum; this.maximum = maximum; this.scale = scale; this.offset = offset; this.unit = unit; this.type = type; this.color = color; this.bkg = new Category("Background", Utils.TRANSPARENT, 0); } @Override public double getMaximumValue() { return maximum; } @Override public double getMinimumValue() { return minimum; } @Override public double[] getNoDataValues() throws IllegalStateException { return new double[] { nodata }; } @Override public double getOffset() throws IllegalStateException { return offset; } @Override public NumberRange<? extends Number> getRange() { return super.getRange(); } @Override public SampleDimensionType getSampleDimensionType() { return type; } @Override public MathTransform1D getSampleToGeophysics() { return super.getSampleToGeophysics(); } @Override public Unit<?> getUnits() { return unit; } @Override public double getScale() { return scale; } @Override public ColorInterpretation getColorInterpretation() { return color; } @Override public Category getBackground() { return bkg; } @Override public InternationalString[] getCategoryNames() throws IllegalStateException { return new InternationalString[] { SimpleInternationalString.wrap("Background") }; } } /** * My specific {@link MaxVisitor} that keeps track of the feature used for the maximum. * @author Simone Giannecchini, GeoSolutions SAS */ static class MaxVisitor2 extends MaxVisitor { private Comparable oldValue; private int oldNanCount; private int oldNullCount; private Feature targetFeature = null; public MaxVisitor2(Expression expr) throws IllegalFilterException { super(expr); } public MaxVisitor2(int attributeTypeIndex, SimpleFeatureType type) throws IllegalFilterException { super(attributeTypeIndex, type); } public Feature getTargetFeature() { return targetFeature; } public MaxVisitor2(String attrName, SimpleFeatureType type) throws IllegalFilterException { super(attrName, type); } public MaxVisitor2(String attributeTypeName) { super(attributeTypeName); } @Override public void reset() { super.reset(); this.oldValue = null; this.targetFeature = null; } @Override public void setValue(Object result) { super.setValue(result); this.oldValue = null; this.targetFeature = null; } @SuppressWarnings("unchecked") @Override public void visit(Feature feature) { super.visit(feature); // if we got a NAN let's leave final int nanCount = getNaNCount(); if (oldNanCount != nanCount) { oldNanCount = nanCount; return; } // if we got a null let's leave final int nullCount = getNullCount(); if (oldNullCount != nullCount) { oldNullCount = nullCount; return; } // check if we got a real value final Comparable max = getMax(); if (oldValue == null || max.compareTo(oldValue) != 0) { targetFeature = feature; oldValue = max; } } } /** * This class is responsible for putting together the granules for the final mosaic. * * @author Simone Giannecchini, GeoSolutions SAS * */ class MosaicBuilder implements GranuleCatalogVisitor { private final int maxNumberOfGranules; private final List<Future<GranuleLoadingResult>> tasks = new ArrayList<Future<GranuleLoadingResult>>(); private int granulesNumber; private List<ROI> rois = new ArrayList<ROI>(); private Color inputTransparentColor; private PlanarImage[] alphaChannels; private RasterLayerRequest request; private ROI[] sourceRoi; private double[][] sourceThreshold; private boolean doInputTransparency; private List<RenderedImage> sources = new ArrayList<RenderedImage>(); /** * Default {@link Constructor} */ public MosaicBuilder(final RasterLayerRequest request) { this.request = request; maxNumberOfGranules = request.getMaximumNumberOfGranules(); } public RenderedImage[] getSourcesAsArray() { RenderedImage[] imageSources = new RenderedImage[sources.size()]; sources.toArray(imageSources); return imageSources; } public void visit(GranuleDescriptor granuleDescriptor, Object o) { // don't collect more than the specified amount of granules // SG20092011 this might not happen since we set the max features in the query, but // who knows! // if(maxNumberOfGranules>0 &&granulesNumber >=maxNumberOfGranules) { // return; // } // // load raster data // // create a granuleDescriptor loader final Geometry bb = JTS.toGeometry((BoundingBox) mosaicBBox); final Geometry inclusionGeometry = granuleDescriptor.inclusionGeometry; if (!footprintManagement || inclusionGeometry == null || footprintManagement && inclusionGeometry.intersects(bb)) { final GranuleLoader loader = new GranuleLoader(baseReadParameters, imageChoice, mosaicBBox, finalWorldToGridCorner, granuleDescriptor, request, hints); if (multithreadingAllowed && rasterManager.parent.multiThreadedLoader != null) tasks.add(rasterManager.parent.multiThreadedLoader.submit(loader)); else tasks.add(new FutureTask<GranuleLoadingResult>(loader)); granulesNumber++; } } public void produce() { // reusable parameters alphaChannels = new PlanarImage[granulesNumber]; int granuleIndex = 0; inputTransparentColor = request.getInputTransparentColor(); doInputTransparency = inputTransparentColor != null && !footprintManagement; // execute them all boolean firstGranule = true; int[] alphaIndex = null; StringBuilder paths = new StringBuilder(); for (Future<GranuleLoadingResult> future : tasks) { final RenderedImage loadedImage; final GranuleLoadingResult result; final URL url; final File inputFile; final String canonicalPath; boolean doFiltering; try { if (!multithreadingAllowed || rasterManager.parent.multiThreadedLoader == null) { //run the loading in this thread final FutureTask<GranuleLoadingResult> task = (FutureTask<GranuleLoadingResult>) future; task.run(); } result = future.get(); if (result == null) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Unable to load the raster for granule " + granuleIndex + " with request " + request.toString()); continue; } loadedImage = result.getRaster(); doFiltering = result.isDoFiltering(); url = result.granuleUrl; inputFile = DataUtilities.urlToFile(url); canonicalPath = inputFile.getCanonicalPath(); if (loadedImage == null) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Unable to load the raster for granuleDescriptor " + granuleIndex + " with request " + request.toString()); continue; } if (firstGranule) { // // We check here if the images have an alpha channel or some // other sort of transparency. In case we have transparency // I also save the index of the transparent channel. // // Specifically, I have to check if the loaded image have // transparency, because if we do a ROI and/or we have a // transparent color to set we have to remove it. // final ColorModel cm = loadedImage.getColorModel(); alphaIn = cm.hasAlpha(); if (alphaIn || doInputTransparency) alphaIndex = new int[] { cm.getNumComponents() - 1 }; // // we set the input threshold accordingly to the input // image data type. I find the default value (which is 0) very bad // for data type other than byte and ushort. With float and double // it can cut off a large par of the dynamic. // sourceThreshold = new double[][] { { CoverageUtilities .getMosaicThreshold(loadedImage.getSampleModel().getDataType()) } }; firstGranule = false; } } catch (Exception e) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Adding to mosaic image number " + granuleIndex + " failed, original request was " + request); continue; } if (LOGGER.isLoggable(Level.FINER)) { LOGGER.finer("Adding to mosaic image number " + granuleIndex); } // // add to the mosaic collection, with preprocessing // RenderedImage raster = processGranuleRaster(loadedImage, granuleIndex, alphaIndex, alphaIn, alphaChannels, doInputTransparency, inputTransparentColor); // we need to add its roi in order to avoid problems with the mosaic overlapping Rectangle bounds = PlanarImage.wrapRenderedImage(raster).getBounds(); Geometry mask = JTS.toGeometry( new Envelope(bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(), bounds.getMaxY())); ROI imageBounds = new ROIGeometry(mask); if (footprintManagement) { final ROI footprint = result.getFootprint(); if (footprint != null) { if (imageBounds.contains(footprint.getBounds2D().getBounds())) { imageBounds = footprint; } else { imageBounds = imageBounds.intersect(footprint); } } //Artifacts filtering processing if (defaultArtifactsFilterThreshold != Integer.MIN_VALUE && doFiltering) { int artifactThreshold = defaultArtifactsFilterThreshold; if (artifactsFilterPTileThreshold != -1) { //Looking for a histogram for that granule in order to //setup dynamic threshold if (url != null) { final String path = FilenameUtils.getFullPath(canonicalPath); final String baseName = FilenameUtils.getBaseName(canonicalPath); final String histogramPath = path + baseName + "." + "histogram"; final Histogram histogram = Utils.getHistogram(histogramPath); if (histogram != null) { final double[] p = histogram.getPTileThreshold(artifactsFilterPTileThreshold); artifactThreshold = (int) p[0]; } } } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Filtering granules artifacts"); } raster = ArtifactsFilterDescriptor.create(raster, imageBounds, new double[] { 0 }, artifactThreshold, 3, hints); } } rois.add(imageBounds); // add to mosaic sources.add(raster); paths.append(granuleIndex > 0 ? "," : "").append(canonicalPath); //increment index granuleIndex++; } granulesNumber = granuleIndex; if (granulesNumber == 0) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Unable to load any granuleDescriptor "); return; } granulesPaths = paths.toString(); sourceRoi = rois.toArray(new ROI[rois.size()]); } } /** Logger. */ private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(RasterLayerResponse.class); /** * The GridCoverage produced after a {@link #compute()} method call */ private GridCoverage2D gridCoverage; /** The {@link RasterLayerRequest} originating this response */ private RasterLayerRequest request; /** The coverage factory producing a {@link GridCoverage} from an image */ private GridCoverageFactory coverageFactory; /** The base envelope related to the input coverage */ private GeneralEnvelope coverageEnvelope; private RasterManager rasterManager; private Color finalTransparentColor; private ReferencedEnvelope mosaicBBox; private Rectangle rasterBounds; private MathTransform2D finalGridToWorldCorner; private MathTransform2D finalWorldToGridCorner; private int imageChoice = 0; private ImageReadParam baseReadParameters = new ImageReadParam(); private boolean multithreadingAllowed = false; private boolean footprintManagement = !Utils.IGNORE_FOOTPRINT; private int defaultArtifactsFilterThreshold = Integer.MIN_VALUE; private double artifactsFilterPTileThreshold = ImageMosaicFormat.DEFAULT_ARTIFACTS_FILTER_PTILE_THRESHOLD; private boolean setRoiProperty; private boolean alphaIn = false; private boolean oversampledRequest = false; private MathTransform baseGridToWorld; private Interpolation interpolation; private boolean needsReprojection; private double[] backgroundValues; private Hints hints; private String granulesPaths; /** * Construct a {@code RasterLayerResponse} given a specific * {@link RasterLayerRequest}, a {@code GridCoverageFactory} to produce * {@code GridCoverage}s and an {@code ImageReaderSpi} to be used for * instantiating an Image Reader for a read operation, * * @param request * a {@link RasterLayerRequest} originating this response. * @param coverageFactory * a {@code GridCoverageFactory} to produce a {@code * GridCoverage} when calling the {@link #compute()} method. * @param readerSpi * the Image Reader Service provider interface. */ public RasterLayerResponse(final RasterLayerRequest request, final RasterManager rasterManager) { this.request = request; coverageEnvelope = rasterManager.spatialDomainManager.coverageEnvelope; this.coverageFactory = rasterManager.getCoverageFactory(); this.rasterManager = rasterManager; this.hints = rasterManager.getHints(); baseGridToWorld = rasterManager.spatialDomainManager.coverageGridToWorld2D; finalTransparentColor = request.getOutputTransparentColor(); // are we doing multithreading? multithreadingAllowed = request.isMultithreadingAllowed(); footprintManagement = request.isFootprintManagement(); setRoiProperty = request.isSetRoiProperty(); backgroundValues = request.getBackgroundValues(); interpolation = request.getInterpolation(); needsReprojection = request.isNeedsReprojection(); defaultArtifactsFilterThreshold = request.getDefaultArtifactsFilterThreshold(); artifactsFilterPTileThreshold = request.getArtifactsFilterPTileThreshold(); } /** * Compute the coverage request and produce a grid coverage which will be * returned by {@link #createResponse()}. The produced grid coverage may be * {@code null} in case of empty request. * * @return the {@link GridCoverage} produced as computation of this response * using the {@link #compute()} method. * @throws IOException * @uml.property name="gridCoverage" */ public GridCoverage2D createResponse() throws IOException { processRequest(); return gridCoverage; } /** * @return the {@link RasterLayerRequest} originating this response. * * @uml.property name="request" */ public RasterLayerRequest getOriginatingCoverageRequest() { return request; } /** * This method creates the GridCoverage2D from the underlying file given a * specified envelope, and a requested dimension. * * @param iUseJAI * specify if the underlying read process should leverage on a * JAI ImageRead operation or a simple direct call to the {@code * read} method of a proper {@code ImageReader}. * @param overviewPolicy * the overview policy which need to be adopted * @return a {@code GridCoverage} * * @throws java.io.IOException */ private void processRequest() throws IOException { if (request.isEmpty()) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.log(Level.FINE, "Request is empty: " + request.toString()); this.gridCoverage = null; return; } // assemble granules final RenderedImage mosaic = prepareResponse(); if (mosaic == null) { this.gridCoverage = null; return; } //postproc RenderedImage finalRaster = postProcessRaster(mosaic); //create the coverage gridCoverage = prepareCoverage(finalRaster); } private RenderedImage postProcessRaster(RenderedImage image) { // alpha on the final mosaic if (finalTransparentColor != null) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Support for alpha on final mosaic"); return ImageUtilities.maskColor(finalTransparentColor, image); } if (!needsReprojection) { try { // creating source grid to world corrected to the pixel corner final AffineTransform sourceGridToWorld = new AffineTransform( (AffineTransform) finalGridToWorldCorner); // target world to grid at the corner final AffineTransform targetGridToWorld = new AffineTransform(request.getRequestedGridToWorld()); targetGridToWorld.concatenate(CoverageUtilities.CENTER_TO_CORNER); // target world to grid at the corner final AffineTransform targetWorldToGrid = targetGridToWorld.createInverse(); // final complete transformation targetWorldToGrid.concatenate(sourceGridToWorld); //update final grid to world finalGridToWorldCorner = new AffineTransform2D(targetGridToWorld); // // Check and see if the affine transform is doing a copy. // If so call the copy operation. // // we are in raster space here, so 1E-3 is safe if (XAffineTransform.isIdentity(targetWorldToGrid, Utils.AFFINE_IDENTITY_EPS)) return image; // create final image // TODO this one could be optimized further depending on how the affine is created // // 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 Hints localHints = new Hints(hints); 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(ImageUtilities.EXTEND_BORDER_BY_COPYING); } } // image = AffineDescriptor.create(image, targetWorldToGrid , interpolation, backgroundValues, localHints); ImageWorker iw = new ImageWorker(image); iw.setRenderingHints(localHints); iw.affine(targetWorldToGrid, interpolation, backgroundValues); image = iw.getRenderedImage(); } catch (NoninvertibleTransformException e) { if (LOGGER.isLoggable(Level.SEVERE)) { LOGGER.log(Level.SEVERE, "Unable to create the requested mosaic ", e); } } } return image; } /** * This method loads the granules which overlap the requested * {@link GeneralEnvelope} using the provided values for alpha and input * ROI. * @return * @throws DataSourceException */ private RenderedImage prepareResponse() throws DataSourceException { try { // // prepare the params for executing a mosaic operation. // // It might important to set the mosaic type to blend otherwise // sometimes strange results jump in. // select the relevant overview, notice that at this time we have // relaxed a bit the requirement to have the same exact resolution // for all the overviews, but still we do not allow for reading the // various grid to world transform directly from the input files, // therefore we are assuming that each granuleDescriptor has a scale and // translate only grid to world that can be deduced from its base // level dimension and envelope. The grid to world transforms for // the other levels can be computed accordingly knowing the scale // factors. if (request.getRequestedBBox() != null && request.getRequestedRasterArea() != null && !request.isHeterogeneousGranules()) imageChoice = ReadParamsController.setReadParams(request.getRequestedResolution(), request.getOverviewPolicy(), request.getDecimationPolicy(), baseReadParameters, request.rasterManager, request.rasterManager.overviewsController); // use general overviews controller else imageChoice = 0; assert imageChoice >= 0; if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(new StringBuffer("Loading level ").append(imageChoice) .append(" with subsampling factors ").append(baseReadParameters.getSourceXSubsampling()) .append(" ").append(baseReadParameters.getSourceYSubsampling()).toString()); // ok we got something to return, let's load records from the index final BoundingBox cropBBOX = request.getCropBBox(); if (cropBBOX != null) mosaicBBox = ReferencedEnvelope.reference(cropBBOX); else mosaicBBox = new ReferencedEnvelope(coverageEnvelope); //compute final world to grid // base grid to world for the center of pixels final AffineTransform g2w; final OverviewLevel baseLevel = rasterManager.overviewsController.resolutionsLevels.get(0); final OverviewLevel selectedLevel = rasterManager.overviewsController.resolutionsLevels .get(imageChoice); final double resX = baseLevel.resolutionX; final double resY = baseLevel.resolutionY; final double[] requestRes = request.getRequestedResolution(); g2w = new AffineTransform((AffineTransform) baseGridToWorld); g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER); if ((requestRes[0] < resX || requestRes[1] < resY)) { // Using the best available resolution oversampledRequest = true; } else { // SG going back to working on a per level basis to do the composition // g2w = new AffineTransform(request.getRequestedGridToWorld()); g2w.concatenate( AffineTransform.getScaleInstance(selectedLevel.scaleFactor, selectedLevel.scaleFactor)); g2w.concatenate(AffineTransform.getScaleInstance(baseReadParameters.getSourceXSubsampling(), baseReadParameters.getSourceYSubsampling())); } // move it to the corner finalGridToWorldCorner = new AffineTransform2D(g2w); finalWorldToGridCorner = finalGridToWorldCorner.inverse();// compute raster bounds final GeneralEnvelope tempRasterBounds = CRS.transform(finalWorldToGridCorner, mosaicBBox); rasterBounds = tempRasterBounds.toRectangle2D().getBounds(); // SG using the above may lead to problems since the reason is that may be a little (1 px) bigger // than what we need. The code below is a bit better since it uses a proper logic (see GridEnvelope // Javadoc) // rasterBounds = new GridEnvelope2D(new Envelope2D(tempRasterBounds), PixelInCell.CELL_CORNER); if (rasterBounds.width == 0) rasterBounds.width++; if (rasterBounds.height == 0) rasterBounds.height++; if (oversampledRequest) rasterBounds.grow(2, 2); // make sure we do not go beyond the raster dimensions for this layer final GeneralEnvelope levelRasterArea_ = CRS.transform(finalWorldToGridCorner, rasterManager.spatialDomainManager.coverageBBox); final GridEnvelope2D levelRasterArea = new GridEnvelope2D(new Envelope2D(levelRasterArea_), PixelInCell.CELL_CORNER); XRectangle2D.intersect(levelRasterArea, rasterBounds, rasterBounds); // create the index visitor and visit the feature final MosaicBuilder visitor = new MosaicBuilder(request); final List times = request.getRequestedTimes(); final List elevations = request.getElevation(); final Map<String, List> additionalDomains = request.getRequestedAdditionalDomains(); final Filter filter = request.getFilter(); final boolean hasTime = (times != null && times.size() > 0); final boolean hasElevation = (elevations != null && elevations.size() > 0); final boolean hasAdditionalDomains = additionalDomains.size() > 0; final boolean hasFilter = filter != null && !Filter.INCLUDE.equals(filter); // create query final SimpleFeatureType type = rasterManager.granuleCatalog.getType(); Query query = null; Filter bbox = null; if (type != null) { query = new Query(rasterManager.granuleCatalog.getType().getTypeName()); bbox = FeatureUtilities.DEFAULT_FILTER_FACTORY.bbox( FeatureUtilities.DEFAULT_FILTER_FACTORY .property(rasterManager.granuleCatalog.getType().getGeometryDescriptor().getName()), mosaicBBox); query.setFilter(bbox); } else { throw new IllegalStateException("GranuleCatalog feature type was null!!!"); } // prepare eventual filter for filtering granules // handle elevation indexing first since we then combine this with the max in case we are asking for current in time if (hasElevation) { final Filter elevationF = rasterManager.parent.elevationDomainManager .createFilter(ImageMosaicReader.ELEVATION_DOMAIN, elevations); query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), elevationF)); } // handle generic filter since we then combine this with the max in case we are asking for current in time if (hasFilter) { query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), filter)); } // fuse time query with the bbox query if (hasTime) { final Filter timeFilter = this.rasterManager.parent.timeDomainManager .createFilter(ImageMosaicReader.TIME_DOMAIN, times); query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), timeFilter)); } if (hasAdditionalDomains) { final List<Filter> additionalFilter = new ArrayList<Filter>(); for (Entry<String, List> entry : additionalDomains.entrySet()) { // build a filter for each dimension final String domainName = entry.getKey() + DomainDescriptor.DOMAIN_SUFFIX; additionalFilter.add( rasterManager.parent.domainsManager.createFilter(domainName, (List) entry.getValue())); } // merge with existing ones query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), FeatureUtilities.DEFAULT_FILTER_FACTORY.and(additionalFilter))); } // // handle secondary query parameters // // max number of elements if (request.getMaximumNumberOfGranules() > 0) { query.setMaxFeatures(request.getMaximumNumberOfGranules()); } // sort by clause final String sortByClause = request.getSortClause(); if (sortByClause != null && sortByClause.length() > 0) { final String[] elements = sortByClause.split(","); if (elements != null && elements.length > 0) { final List<SortBy> clauses = new ArrayList<SortBy>(elements.length); for (String element : elements) { // check if (element == null || element.length() <= 0) { continue;// next, please! } try { // which clause? // ASCENDING element = element.trim(); if (element.endsWith(Utils.ASCENDING_ORDER_IDENTIFIER)) { String attribute = element.substring(0, element.length() - 2); clauses.add( new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute), SortOrder.ASCENDING)); } else // DESCENDING if (element.contains(Utils.DESCENDING_ORDER_IDENTIFIER)) { String attribute = element.substring(0, element.length() - 2); clauses.add( new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute), SortOrder.DESCENDING)); } // if(element.startsWith(Utils.ASCENDING_ORDER_IDENTIFIER)){ // String attribute=element.substring(Utils.ASCENDING_ORDER_IDENTIFIER.length()+1); // attribute=attribute.substring(0, attribute.length()-1); // clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),SortOrder.ASCENDING)); // } else // // DESCENDING // if(element.startsWith(Utils.DESCENDING_ORDER_IDENTIFIER)){ // String attribute=element.substring(Utils.DESCENDING_ORDER_IDENTIFIER.length()+1); // attribute=attribute.substring(0, attribute.length()-1); // clauses.add(new SortByImpl(FeatureUtilities.DEFAULT_FILTER_FACTORY.property(attribute),SortOrder.DESCENDING)); // } else { else { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Ignoring sort clause :" + element); } } } catch (Exception e) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.log(Level.INFO, e.getLocalizedMessage(), e); } } } // assign to query if sorting is supported! final SortBy[] sb = clauses.toArray(new SortBy[] {}); if (rasterManager.granuleCatalog.getQueryCapabilities().supportsSorting(sb)) { query.setSortBy(sb); } } } // collect granules rasterManager.getGranules(query, visitor); // get those granules visitor.produce(); // // Did we actually load anything?? Notice that it might happen that // either we have holes inside the definition area for the mosaic // or we had some problem with missing tiles, therefore it might // happen that for some bboxes we don't have anything to load. // RenderedImage returnValue = null; if (visitor.granulesNumber >= 1) { // // Create the mosaic image by doing a crop if necessary and also // managing the transparent color if applicable. Be aware that // management of the transparent color involves removing // transparency information from the input images. // returnValue = buildMosaic(visitor); if (returnValue != null) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Loaded bbox " + mosaicBBox.toString() + " while crop bbox " + request.getCropBBox().toString()); return returnValue; } } // Redo the query without filter to check whether we got no granules due // to a filter. In that case we need to return null if (hasTime || hasElevation || hasFilter || hasAdditionalDomains) { query.setFilter(bbox); rasterManager.getGranules(query, visitor); // get those granules visitor.produce(); if (visitor.granulesNumber >= 1) { // It means the previous lack of granule was due to a filter excluding all the results. Then we return null return null; } } if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Creating constant image for area with no data"); // if we get here that means that we do not have anything to load // but still we are inside the definition area for the mosaic, // therefore we create a fake coverage using the background values, // if provided (defaulting to 0), as well as the compute raster // bounds, envelope and grid to world. final Number[] values = ImageUtilities.getBackgroundValues(rasterManager.defaultSM, backgroundValues); // create a constant image with a proper layout RenderedImage finalImage = ConstantDescriptor.create(Float.valueOf(rasterBounds.width), Float.valueOf(rasterBounds.height), values, null); if (rasterBounds.x != 0 || rasterBounds.y != 0) { finalImage = TranslateDescriptor.create(finalImage, Float.valueOf(rasterBounds.x), Float.valueOf(rasterBounds.y), Interpolation.getInstance(Interpolation.INTERP_NEAREST), null); } if (rasterManager.defaultCM != null) { final ImageLayout2 il = new ImageLayout2(); il.setColorModel(rasterManager.defaultCM); Dimension tileSize = request.getTileDimensions(); if (tileSize == null) { tileSize = JAI.getDefaultTileSize(); } il.setSampleModel( rasterManager.defaultCM.createCompatibleSampleModel(tileSize.width, tileSize.height)); il.setTileGridXOffset(0).setTileGridYOffset(0).setTileWidth((int) tileSize.getWidth()) .setTileHeight((int) tileSize.getHeight()); return FormatDescriptor.create(finalImage, Integer.valueOf(il.getSampleModel(null).getDataType()), new RenderingHints(JAI.KEY_IMAGE_LAYOUT, il)); } return finalImage; } catch (Exception e) { throw new DataSourceException("Unable to create this mosaic", e); } } private RenderedImage processGranuleRaster(RenderedImage granule, final int granuleIndex, final int[] alphaIndex, final boolean alphaIn, final PlanarImage[] alphaChannels, final boolean doTransparentColor, final Color transparentColor) { // // INDEX COLOR MODEL EXPANSION // // Take into account the need for an expansions of the original color // model. // // If the original color model is an index color model an expansion // might be requested in case the different palettes are not all the // same. In this case the mosaic operator from JAI would provide wrong // results since it would take the first palette and use that one for // all the other images. // // There is a special case to take into account here. In case the input // images use an IndexColorModel it might happen that the transparent // color is present in some of them while it is not present in some // others. This case is the case where for sure a color expansion is // needed. However we have to take into account that during the masking // phase the images where the requested transparent color was present // will have 4 bands, the other 3. If we want the mosaic to work we // have to add an extra band to the latter type of images for providing // alpha information to them. // // if (rasterManager.expandMe && granule.getColorModel() instanceof IndexColorModel) { granule = new ImageWorker(granule).forceComponentColorModel().getRenderedImage(); } // // TRANSPARENT COLOR MANAGEMENT // if (doTransparentColor) { if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Support for alpha on input image number " + granuleIndex); granule = ImageUtilities.maskColor(transparentColor, granule); alphaIndex[0] = granule.getColorModel().getNumComponents() - 1; } // // ROI // if (alphaIn || doTransparentColor) { ImageWorker w = new ImageWorker(granule); if (granule.getSampleModel() instanceof MultiPixelPackedSampleModel) w.forceComponentColorModel(); // // ALPHA in INPUT // // I have to select the alpha band and provide it to the final // mosaic operator. I have to force going to ComponentColorModel in // case the image is indexed. // if (granule.getColorModel() instanceof IndexColorModel) { alphaChannels[granuleIndex] = w.forceComponentColorModel().retainLastBand().getPlanarImage(); } else alphaChannels[granuleIndex] = w.retainBands(alphaIndex).getPlanarImage(); } return granule; } /** * Once we reach this method it means that we have loaded all the images * which were intersecting the requested envelope. Next step is to create * the final mosaic image and cropping it to the exact requested envelope. * @param visitor * * @return A {@link RenderedImage}}. */ private RenderedImage buildMosaic(final MosaicBuilder visitor) throws IOException { // build final layout and use it for cropping purposes final ImageLayout layout = new ImageLayout(rasterBounds.x, rasterBounds.y, rasterBounds.width, rasterBounds.height); //prepare hints final Dimension tileDimensions = request.getTileDimensions(); if (tileDimensions != null) { layout.setTileHeight(tileDimensions.width).setTileWidth(tileDimensions.height); } final RenderingHints localHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); if (hints != null && !hints.isEmpty()) { if (hints.containsKey(JAI.KEY_TILE_CACHE)) { final Object tc = hints.get(JAI.KEY_TILE_CACHE); if (tc != null && tc instanceof TileCache) localHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, (TileCache) tc)); } 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, (BorderExtender) extender)); addBorderExtender = false; } } if (addBorderExtender) { localHints.add(ImageUtilities.BORDER_EXTENDER_HINTS); } if (hints.containsKey(JAI.KEY_TILE_SCHEDULER)) { final Object ts = hints.get(JAI.KEY_TILE_SCHEDULER); if (ts != null && ts instanceof TileScheduler) localHints.add(new RenderingHints(JAI.KEY_TILE_SCHEDULER, (TileScheduler) ts)); } } // // SPECIAL CASE // 1 single tile, we try not do a mosaic. final ROI[] sourceRoi = visitor.sourceRoi; if (visitor.granulesNumber == 1 && Utils.OPTIMIZE_CROP) { // the roi is exactly equal to the final ROI roi = visitor.rois.get(0); Rectangle bounds = Utils.toRectangle(roi.getAsShape()); if (bounds != null) { RenderedImage image = visitor.getSourcesAsArray()[0]; Rectangle imageBounds = PlanarImage.wrapRenderedImage(image).getBounds(); if (imageBounds.equals(bounds)) { // do we need to crop? (image is bigger than requested?) if (!rasterBounds.contains(imageBounds)) { // we have to crop XRectangle2D.intersect(imageBounds, rasterBounds, imageBounds); if (imageBounds.isEmpty()) { // return back a constant image return null; } // crop ImageWorker iw = new ImageWorker(image); iw.setRenderingHints(localHints); iw.crop(imageBounds.x, imageBounds.y, imageBounds.width, imageBounds.height); image = iw.getRenderedImage(); imageBounds = PlanarImage.wrapRenderedImage(image).getBounds(); } // and, do we need to add a border around the image? if (!imageBounds.contains(rasterBounds)) { image = MosaicDescriptor.create(new RenderedImage[] { image }, request.isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND : MosaicDescriptor.MOSAIC_TYPE_OVERLAY, (alphaIn || visitor.doInputTransparency) ? visitor.alphaChannels : null, sourceRoi, visitor.sourceThreshold, backgroundValues, localHints); } return image; } } } // // Final Merge // // I can even do a stacking merge or a flat merge final RenderedImage mosaic = request.getMergeBehavior().process(visitor.getSourcesAsArray(), backgroundValues, visitor.sourceThreshold, (alphaIn || visitor.doInputTransparency) ? visitor.alphaChannels : null, sourceRoi, request.isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND : MosaicDescriptor.MOSAIC_TYPE_OVERLAY, localHints); if (setRoiProperty) { //Adding globalRoi to the output RenderedOp rop = (RenderedOp) mosaic; ROI globalRoi = null; ROI[] rois = sourceRoi; for (int i = 0; i < rois.length; i++) { if (globalRoi == null) { globalRoi = new ROIGeometry(((ROIGeometry) rois[i]).getAsGeometry()); } else { globalRoi = globalRoi.add(rois[i]); } } rop.setProperty("ROI", globalRoi); } if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine("Mosaic created "); // create the coverage return mosaic; } /** * This method is responsible for creating a coverage from the supplied {@link RenderedImage}. * * @param image * @return * @throws IOException */ private GridCoverage2D prepareCoverage(RenderedImage image) throws IOException { // creating bands final SampleModel sm = image.getSampleModel(); final ColorModel cm = image.getColorModel(); final int numBands = sm.getNumBands(); final GridSampleDimension[] bands = new GridSampleDimension[numBands]; Set<String> bandNames = new HashSet<String>(); // setting bands names. for (int i = 0; i < numBands; i++) { ColorInterpretation colorInterpretation = null; String bandName = null; if (cm != null) { // === color interpretation colorInterpretation = TypeMap.getColorInterpretation(cm, i); if (colorInterpretation == null) { throw new IOException("Unrecognized sample dimension type"); } bandName = colorInterpretation.name(); if (bandNames.contains(bandName)) {// make sure we create no duplicate band names bandName = "Band" + (i + 1); } } else { // no color model bandName = "Band" + (i + 1); colorInterpretation = ColorInterpretation.UNDEFINED; } // sample dimension type final SampleDimensionType st = TypeMap.getSampleDimensionType(sm, i); // set some no data values, as well as Min and Max values final double noData; double min = -Double.MAX_VALUE, max = Double.MAX_VALUE; if (backgroundValues != null) { // sometimes background values are not specified as 1 per each band, therefore we need to be careful noData = backgroundValues[backgroundValues.length > i ? i : 0]; } else { if (st.compareTo(SampleDimensionType.REAL_32BITS) == 0) noData = Float.NaN; else if (st.compareTo(SampleDimensionType.REAL_64BITS) == 0) noData = Double.NaN; else if (st.compareTo(SampleDimensionType.SIGNED_16BITS) == 0) { noData = Short.MIN_VALUE; min = Short.MIN_VALUE; max = Short.MAX_VALUE; } else if (st.compareTo(SampleDimensionType.SIGNED_32BITS) == 0) { noData = Integer.MIN_VALUE; min = Integer.MIN_VALUE; max = Integer.MAX_VALUE; } else if (st.compareTo(SampleDimensionType.SIGNED_8BITS) == 0) { noData = -128; min = -128; max = 127; } else { //unsigned noData = 0; min = 0; // compute max if (st.compareTo(SampleDimensionType.UNSIGNED_1BIT) == 0) max = 1; else if (st.compareTo(SampleDimensionType.UNSIGNED_2BITS) == 0) max = 3; else if (st.compareTo(SampleDimensionType.UNSIGNED_4BITS) == 0) max = 7; else if (st.compareTo(SampleDimensionType.UNSIGNED_8BITS) == 0) max = 255; else if (st.compareTo(SampleDimensionType.UNSIGNED_16BITS) == 0) max = 65535; else if (st.compareTo(SampleDimensionType.UNSIGNED_32BITS) == 0) max = Math.pow(2, 32) - 1; } } bands[i] = new SimplifiedGridSampleDimension(bandName, st, colorInterpretation, noData, min, max, 1, //no scale 0, //no offset null).geophysics(true); } // creating the final coverage by keeping into account the fact that we Map<String, String> properties = null; if (granulesPaths != null) { properties = new HashMap<String, String>(); properties.put(AbstractGridCoverage2DReader.FILE_SOURCE_PROPERTY, granulesPaths); } return coverageFactory.create(rasterManager.getCoverageIdentifier(), image, new GridGeometry2D(new GridEnvelope2D(PlanarImage.wrapRenderedImage(image).getBounds()), PixelInCell.CELL_CORNER, finalGridToWorldCorner, this.mosaicBBox.getCoordinateReferenceSystem(), hints), bands, null, properties); } }