/*
* 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.granulecollector;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.media.jai.Histogram;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import org.apache.commons.io.FilenameUtils;
import org.geotools.data.DataUtilities;
import org.geotools.gce.imagemosaic.GranuleDescriptor;
import org.geotools.gce.imagemosaic.GranuleDescriptor.GranuleLoadingResult;
import org.geotools.gce.imagemosaic.GranuleLoader;
import org.geotools.gce.imagemosaic.MergeBehavior;
import org.geotools.gce.imagemosaic.MosaicElement;
import org.geotools.gce.imagemosaic.MosaicInputs;
import org.geotools.gce.imagemosaic.Mosaicker;
import org.geotools.gce.imagemosaic.RasterLayerResponse;
import org.geotools.gce.imagemosaic.Utils;
import org.geotools.gce.imagemosaic.egr.ROIExcessGranuleRemover;
import org.geotools.geometry.jts.JTS;
import org.geotools.image.ImageWorker;
import org.geotools.resources.coverage.CoverageUtilities;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import it.geosolutions.jaiext.vectorbin.ROIGeometry;
/**
* Basic submosaic producer. Accepts all granules and mosaics without any real special handling
*/
public class BaseSubmosaicProducer implements SubmosaicProducer {
final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(DefaultSubmosaicProducer.class);
/** The final lists for granules to be computed, splitted per dimension value. */
protected final List<Future<GranuleDescriptor.GranuleLoadingResult>> granulesFutures = new ArrayList<Future<GranuleDescriptor.GranuleLoadingResult>>();
protected final boolean dryRun;
protected RasterLayerResponse rasterLayerResponse;
/** The number of collected granules. **/
protected int granulesNumber;
protected double[][] sourceThreshold;
protected boolean hasAlpha;
protected boolean doInputTransparency;
protected Color inputTransparentColor;
private int[] alphaIndex = new int[1];
public BaseSubmosaicProducer(RasterLayerResponse rasterLayerResponse, boolean dryRun) {
this.rasterLayerResponse = rasterLayerResponse;
this.dryRun = dryRun;
inputTransparentColor = rasterLayerResponse.getRequest().getInputTransparentColor();
doInputTransparency = inputTransparentColor != null
&& !rasterLayerResponse.getFootprintBehavior().handleFootprints();
}
/**
* This methods collects the granules from their eventual multithreaded processing and turn them into a {@link MosaicInputs} object.
*
* @return a {@link MosaicInputs} ready to be mosaicked.
*/
protected MosaicInputs collectGranules() throws IOException {
// do we have anything to do?
if (granulesNumber <= 0) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "granules number <= 0");
}
return null;
}
// execute them all
final StringBuilder paths = new StringBuilder();
URL sourceUrl = null;
final List<MosaicElement> returnValues = new ArrayList<>();
// collect sources for the current dimension and then process them
for (Future<GranuleDescriptor.GranuleLoadingResult> future : granulesFutures) {
try {
// get the resulting RenderedImage
final GranuleDescriptor.GranuleLoadingResult result = future.get();
if (result == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Unable to load the raster for granule with request "
+ rasterLayerResponse.getRequest().toString());
}
continue;
}
final RenderedImage loadedImage = result.getRaster();
if (loadedImage == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,
"Unable to load the raster for granuleDescriptor "
+ result.getGranuleUrl() + " with request "
+ rasterLayerResponse.getRequest().toString());
}
continue;
}
// perform excess granule removal in case multithreaded loading is enabled
if(isMultithreadedLoadingEnabled()) {
ROIExcessGranuleRemover remover = rasterLayerResponse.getExcessGranuleRemover();
if(remover != null) {
if(remover.isRenderingAreaComplete()) {
break;
}
if(!remover.addGranule(result)) {
// skip this granule
continue;
}
}
}
// now process it
if (sourceThreshold == null) {
//
// 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();
hasAlpha = cm.hasAlpha();
if (hasAlpha) {
alphaIndex[0] = 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()) } };
}
// moving on
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Adding to mosaic granule " + result.getGranuleUrl());
}
// path management
File inputFile = DataUtilities.urlToFile(result.getGranuleUrl());
String canonicalPath = inputFile.getCanonicalPath();
// Remove ovr extension if present
String fileCanonicalPath = canonicalPath;
if (canonicalPath.endsWith(".ovr")) {
fileCanonicalPath = canonicalPath.substring(0, canonicalPath.length() - 4);
}
paths.append(canonicalPath).append(",");
// take only the first source URL found
if (sourceUrl == null) {
sourceUrl = result.getGranuleUrl();
}
// add to the mosaic collection, with preprocessing
// TODO pluggable mechanism for processing (artifacts,etc...)
MosaicElement input = preProcessGranuleRaster(loadedImage, result,
fileCanonicalPath);
returnValues.add(input);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("Adding to mosaic failed, original request was "
+ rasterLayerResponse.getRequest());
}
throw new IOException(e);
}
}
// collect paths
rasterLayerResponse.setGranulesPaths(
paths.length() > 1 ? paths.substring(0, paths.length() - 1) : "");
rasterLayerResponse.setSourceUrl(sourceUrl);
if (returnValues == null || returnValues.isEmpty()) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("The MosaicElement list is null or empty");
}
}
return new MosaicInputs(doInputTransparency, hasAlpha, returnValues, sourceThreshold);
}
private MosaicElement preProcessGranuleRaster(RenderedImage granule,
final GranuleDescriptor.GranuleLoadingResult result, String canonicalPath) {
//
// 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 (rasterLayerResponse.getRasterManager().isExpandMe()
&& granule.getColorModel() instanceof IndexColorModel) {
granule = new ImageWorker(granule).forceComponentColorModel().getRenderedImage();
}
//
// TRANSPARENT COLOR MANAGEMENT
//
if (doInputTransparency) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Support for alpha on input granule " + result.getGranuleUrl());
}
granule = new ImageWorker(granule).makeColorTransparent(inputTransparentColor)
.getRenderedImage();
hasAlpha = granule.getColorModel().hasAlpha();
if (!granule.getColorModel().hasAlpha()) {
// if the resulting image has no transparency (can happen with IndexColorModel then we need to try component
// color model
granule = new ImageWorker(granule).forceComponentColorModel(true)
.makeColorTransparent(inputTransparentColor).getRenderedImage();
hasAlpha = granule.getColorModel().hasAlpha();
}
assert hasAlpha;
}
PlanarImage alphaChannel = null;
if (hasAlpha || doInputTransparency) {
ImageWorker w = new ImageWorker(granule);
if (granule.getSampleModel() instanceof MultiPixelPackedSampleModel
|| granule.getColorModel() instanceof IndexColorModel) {
w.forceComponentColorModel();
granule = w.getRenderedImage();
}
// doing this here gives the guarantee that I get the correct index for the transparency band
alphaIndex[0] = granule.getColorModel().getNumComponents() - 1;
assert alphaIndex[0] < granule.getSampleModel().getNumBands();
//
// 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.
//
alphaChannel = w.retainBands(alphaIndex).getPlanarImage();
}
//
// ROI
//
// we need to add its roi in order to avoid problems with the mosaics sources overlapping
final Rectangle bounds = PlanarImage.wrapRenderedImage(granule).getBounds();
Geometry mask = JTS.toGeometry(new Envelope(bounds.getMinX(), bounds.getMaxX(),
bounds.getMinY(), bounds.getMaxY()));
ROI imageROI = new ROIGeometry(mask);
if (rasterLayerResponse.getFootprintBehavior().handleFootprints()) {
// get the real footprint
final ROI footprint = result.getFootprint();
if (footprint != null) {
if (imageROI.contains(footprint.getBounds2D().getBounds())) {
imageROI = footprint;
} else {
imageROI = imageROI.intersect(footprint);
}
}
// ARTIFACTS FILTERING
if (rasterLayerResponse.getDefaultArtifactsFilterThreshold() != Integer.MIN_VALUE
&& result.isDoFiltering()) {
int artifactThreshold = rasterLayerResponse.getDefaultArtifactsFilterThreshold();
if (rasterLayerResponse.getArtifactsFilterPTileThreshold() != -1) {
// Looking for a histogram for that granule in order to
// setup dynamic threshold
if (canonicalPath != 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(
rasterLayerResponse.getArtifactsFilterPTileThreshold());
artifactThreshold = (int) p[0];
}
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Filtering granules artifacts");
}
ImageWorker w = new ImageWorker(granule)
.setRenderingHints(rasterLayerResponse.getHints()).setROI(imageROI);
w.setBackground(new double[] { 0 });
w.artifactsFilter(artifactThreshold, 3);
granule = w.getRenderedImage();
}
}
// preparing input
return new MosaicElement(alphaChannel, imageROI, granule, result.getPamDataset());
}
@Override
public List<MosaicElement> createMosaic() throws IOException {
final MosaicElement mosaic = (new Mosaicker(this.rasterLayerResponse,
collectGranules(), MergeBehavior.FLAT)).createMosaic();
if (mosaic == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(mosaic);
}
}
@Override
public boolean accept(GranuleDescriptor granuleDescriptor) {
return this.acceptGranule(granuleDescriptor);
}
protected boolean acceptGranule(GranuleDescriptor granuleDescriptor) {
Object imageIndex = granuleDescriptor.getOriginator().getAttribute("imageindex");
if (imageIndex != null && imageIndex instanceof Integer) {
rasterLayerResponse.setImageChoice((Integer) imageIndex);
}
final GranuleLoader loader = new GranuleLoader(rasterLayerResponse.getBaseReadParameters(),
rasterLayerResponse.getImageChoice(), rasterLayerResponse.getMosaicBBox(),
rasterLayerResponse.getFinalWorldToGridCorner(), granuleDescriptor,
rasterLayerResponse.getRequest(), rasterLayerResponse.getHints());
if (!dryRun) {
final boolean multiThreadedLoading = isMultithreadedLoadingEnabled();
if (multiThreadedLoading) {
// MULTITHREADED EXECUTION submitting the task
final ExecutorService mtLoader = rasterLayerResponse
.getRasterManager().getParentReader().getMultiThreadedLoader();
granulesFutures.add(mtLoader.submit(loader));
} else {
// SINGLE THREADED Execution, we defer the execution to when we have done the loading
final FutureTask<GranuleDescriptor.GranuleLoadingResult> task = new FutureTask<>(
loader);
task.run(); // run in current thread
// perform excess granule removal, as it makes sense in single threaded mode to
// do it while loading, to allow for an early bail out reading granules
ROIExcessGranuleRemover remover = rasterLayerResponse.getExcessGranuleRemover();
GranuleLoadingResult result;
if(remover != null) {
try {
result = task.get();
if(!remover.addGranule(result)) {
return false;
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
granulesFutures.add(task);
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("We added the granule " + granuleDescriptor.toString());
}
// we added it
granulesNumber++;
return true;
}
private boolean isMultithreadedLoadingEnabled() {
final ExecutorService mtLoader = rasterLayerResponse
.getRasterManager().getParentReader().getMultiThreadedLoader();
final boolean multiThreadedLoading = rasterLayerResponse.isMultithreadingAllowed() && mtLoader != null;
return multiThreadedLoading;
}
public boolean doInputTransparency() {
return doInputTransparency;
}
public double[][] getSourceThreshold() {
return sourceThreshold;
}
public boolean hasAlpha() {
return hasAlpha;
}
}