/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 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 static org.geotools.gce.imagemosaic.ImageMosaicConfigHandler.LOGGER; import java.awt.*; import java.awt.image.RenderedImage; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.BorderExtender; import javax.media.jai.ImageLayout; 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.MosaicDescriptor; import org.geotools.coverage.grid.io.footprint.FootprintBehavior; import org.geotools.geometry.jts.JTS; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.image.ImageWorker; import org.geotools.resources.geometry.XRectangle2D; import org.geotools.resources.image.ImageUtilities; import org.geotools.util.logging.Logging; import it.geosolutions.jaiext.vectorbin.ROIGeometry; import it.geosolutions.imageio.pam.PAMDataset; import it.geosolutions.jaiext.range.NoDataContainer; /** * A class doing the mosaic operation on top of a List of {@link MosaicElement}s. * * @author Simone Giannecchini, GeoSolutions SAS */ public class Mosaicker { private static final Logger LOGGER = Logging.getLogger(Mosaicker.class); private final List<MosaicElement> inputs; private final double[][] sourceThreshold; private final boolean doInputTransparency; private final boolean hasAlpha; private final MergeBehavior mergeBehavior; private RasterLayerResponse rasterLayerResponse; public Mosaicker(RasterLayerResponse rasterLayerResponse, MosaicInputs inputs, MergeBehavior mergeBehavior) { this.inputs = new ArrayList<>(inputs.getSources()); this.sourceThreshold = inputs.getSourceThreshold(); this.doInputTransparency = inputs.isDoInputTransparency(); this.hasAlpha = inputs.isHasAlpha(); this.mergeBehavior = mergeBehavior; this.rasterLayerResponse = rasterLayerResponse; } /** * @return * @param useFinalImageLayout whether the layout should be created from the requested bounds or no layout should be provided */ private RenderingHints prepareHints(boolean useFinalImageLayout) { final RenderingHints localHints = new RenderingHints(null); if (useFinalImageLayout) { // build final layout and use it for cropping purposes final ImageLayout layout = new ImageLayout(rasterLayerResponse.getRasterBounds().x, rasterLayerResponse.getRasterBounds().y, rasterLayerResponse.getRasterBounds().width, rasterLayerResponse.getRasterBounds().height); Dimension tileDimensions = rasterLayerResponse.getRequest().getTileDimensions(); if (tileDimensions == null) { tileDimensions = (Dimension) JAI.getDefaultTileSize().clone(); } layout.setTileGridXOffset(0).setTileGridYOffset(0); layout.setTileHeight(tileDimensions.height).setTileWidth(tileDimensions.width); localHints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout)); } // look for additional hints for caching and tile scheduling if (rasterLayerResponse.getHints() != null && !rasterLayerResponse.getHints().isEmpty()) { // TileCache TileCache tc = Utils.getTileCacheHint(rasterLayerResponse.getHints()); if (tc != null) { localHints.add(new RenderingHints(JAI.KEY_TILE_CACHE, tc)); } // BorderExtender localHints.add(ImageUtilities.BORDER_EXTENDER_HINTS);// default BorderExtender be = Utils.getBorderExtenderHint(rasterLayerResponse.getHints()); if (be != null) { localHints.add(new RenderingHints(JAI.KEY_BORDER_EXTENDER, be)); } // TileScheduler TileScheduler tileScheduler = Utils.getTileSchedulerHint(rasterLayerResponse.getHints()); if (tileScheduler != null) { localHints.add(new RenderingHints(JAI.KEY_TILE_SCHEDULER, tileScheduler)); } } return localHints; } /** * 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. * * @return A {@link MosaicElement}}. */ public MosaicElement createMosaic() throws IOException { return createMosaic(true); } /** * 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. * * @return A {@link MosaicElement}}. * @param useFinalImageLayout whether the final image layout requested should be used. if false then a default layout will be used. useful if * your mosaic layout doesn't match the final layout. default layout will be whatever layout is necessary to do the mosaic op */ public MosaicElement createMosaic(boolean useFinalImageLayout) throws IOException { return createMosaic(useFinalImageLayout, false); } /** * 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. * * @return A {@link MosaicElement}}. * @param useFinalImageLayout whether the final image layout requested should be used. if false then a default layout will be used. useful if * your mosaic layout doesn't match the final layout. default layout * @param skipSingleElementOptimization whether the single element case should be optimized. some callers wish to skip this since there are a * few differences along this path that can cause issues (namely using the final image layout for operations) */ public MosaicElement createMosaic(boolean useFinalImageLayout, boolean skipSingleElementOptimization) throws IOException { // anything to do? final int size = inputs.size(); if (size <= 0) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Unable to load any granuleDescriptor "); } return null; } // === prepare hints final RenderingHints localHints = prepareHints(useFinalImageLayout); // // SPECIAL CASE // 1 single tile, we try not do a mosaic. if (!skipSingleElementOptimization && size == 1 && Utils.OPTIMIZE_CROP) { // prepare input MosaicElement in = inputs.get(0); if (in == null) { throw new NullPointerException( "The list of MosaicElements contains one element but it's null"); } PAMDataset pamDataset = in.pamDataset; // the roi is exactly equal to the image ROI roi = in.roi; if (roi != null) { Rectangle bounds = Utils.toRectangle(roi.getAsShape()); if (bounds != null) { RenderedImage mosaic = in.source; Rectangle imageBounds = PlanarImage.wrapRenderedImage(mosaic).getBounds(); if (imageBounds.equals(bounds)) { // do we need to crop? (image is bigger than requested?) if (!rasterLayerResponse.getRasterBounds().contains(imageBounds)) { // we have to crop XRectangle2D.intersect(imageBounds, rasterLayerResponse.getRasterBounds(), imageBounds); if (imageBounds.isEmpty()) { // return back a constant image return null; } // crop ImageWorker iw = new ImageWorker(mosaic); iw.setRenderingHints(localHints); iw.crop(imageBounds.x, imageBounds.y, imageBounds.width, imageBounds.height); mosaic = iw.getRenderedImage(); // Propagate NoData PlanarImage t = PlanarImage.wrapRenderedImage(mosaic); if (iw.getNoData() != null) { t.setProperty(NoDataContainer.GC_NODATA, new NoDataContainer(iw.getNoData())); mosaic = t; } imageBounds = t.getBounds(); } // and, do we need to add a BORDER around the image? if (!imageBounds.contains(rasterLayerResponse.getRasterBounds())) { mosaic = MergeBehavior.FLAT.process(new RenderedImage[] { mosaic }, rasterLayerResponse.getBackgroundValues(), sourceThreshold, (hasAlpha || doInputTransparency) ? new PlanarImage[] { in.alphaChannel } : new PlanarImage[] { null }, new ROI[] { in.roi }, rasterLayerResponse.getRequest().isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND : MosaicDescriptor.MOSAIC_TYPE_OVERLAY, localHints); roi = roi.add(new ROIGeometry(JTS.toGeometry(new ReferencedEnvelope( rasterLayerResponse.getRasterBounds(), null)))); if (rasterLayerResponse.getFootprintBehavior() != FootprintBehavior.None) { // Adding globalRoi to the output RenderedOp rop = (RenderedOp) mosaic; rop.setProperty("ROI", in.roi); mosaic = rasterLayerResponse.getFootprintBehavior().postProcessMosaic(mosaic, in.roi, localHints); } } // add to final list return new MosaicElement(in.alphaChannel, roi, mosaic, pamDataset); } } } } // === do the mosaic as usual // prepare sources for the mosaic operation final RenderedImage[] sources = new RenderedImage[size]; final PlanarImage[] alphas = new PlanarImage[size]; ROI[] rois = new ROI[size]; final PAMDataset[] pams = new PAMDataset[size]; int realROIs = 0; for (int i = 0; i < size; i++) { final MosaicElement mosaicElement = inputs.get(i); sources[i] = mosaicElement.source; alphas[i] = mosaicElement.alphaChannel; rois[i] = mosaicElement.roi; pams[i] = mosaicElement.pamDataset; // If we have an alpha, mask it by the ROI if (alphas[i] != null && rois[i] != null) { // Get ROI as image, fix color space ImageWorker roi = new ImageWorker(rois[i].getAsImage()); roi.forceComponentColorModel(); ImageWorker alpha = new ImageWorker(alphas[i]); alpha.multiply(roi.getRenderedImage()); alphas[i] = alpha.getPlanarImage(); } // compose the overall ROI if needed if (mosaicElement.roi != null) { realROIs++; } } if (realROIs == 0) { rois = null; } // execute mosaic final RenderedImage mosaic = mergeBehavior .process(sources, rasterLayerResponse.getBackgroundValues(), sourceThreshold, (hasAlpha || doInputTransparency) ? alphas : null, rois, rasterLayerResponse.getRequest().isBlend() ? MosaicDescriptor.MOSAIC_TYPE_BLEND : MosaicDescriptor.MOSAIC_TYPE_OVERLAY, localHints); Object property = mosaic.getProperty("ROI"); ROI overallROI = (property instanceof ROI) ? (ROI) property : null; final RenderedImage postProcessed = rasterLayerResponse.getFootprintBehavior() .postProcessMosaic(mosaic, overallROI, localHints); // prepare for next step if (hasAlpha || doInputTransparency) { return new MosaicElement( new ImageWorker(postProcessed).retainLastBand().getPlanarImage(), overallROI, postProcessed, Utils.mergePamDatasets(pams)); } else { return new MosaicElement(null, overallROI, postProcessed, Utils.mergePamDatasets(pams)); } } }