/*
* 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.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
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.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageReadParam;
import javax.measure.unit.Unit;
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.operator.ConstantDescriptor;
import javax.media.jai.operator.MosaicDescriptor;
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.GranuleSource;
import org.geotools.coverage.grid.io.GridCoverage2DReader;
import org.geotools.coverage.grid.io.footprint.FootprintBehavior;
import org.geotools.data.DataSourceException;
import org.geotools.data.Query;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.filter.SortByImpl;
import org.geotools.gce.imagemosaic.OverviewsController.OverviewLevel;
import org.geotools.gce.imagemosaic.RasterManager.DomainDescriptor;
import org.geotools.gce.imagemosaic.catalog.GranuleCatalogVisitor;
import org.geotools.gce.imagemosaic.egr.ROIExcessGranuleRemover;
import org.geotools.gce.imagemosaic.granulecollector.DefaultSubmosaicProducer;
import org.geotools.gce.imagemosaic.granulecollector.DefaultSubmosaicProducerFactory;
import org.geotools.gce.imagemosaic.granulecollector.SubmosaicProducer;
import org.geotools.gce.imagemosaic.granulecollector.SubmosaicProducerFactory;
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.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.geotools.util.Utilities;
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.simple.SimpleFeature;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.geometry.BoundingBox;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.InternationalString;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.util.Assert;
import it.geosolutions.imageio.imageioimpl.EnhancedImageReadParam;
import it.geosolutions.imageio.pam.PAMDataset;
import it.geosolutions.jaiext.range.NoDataContainer;
import it.geosolutions.jaiext.range.Range;
import it.geosolutions.jaiext.range.RangeFactory;
import it.geosolutions.jaiext.utilities.ImageLayout2;
/**
* 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")
public class RasterLayerResponse {
private final SubmosaicProducerFactory submosaicProducerFactory;
private SortBy[] sortBy;
class MosaicOutput {
public MosaicOutput(MosaicElement element) {
this.image = element.source;
this.pamDataset = element.pamDataset;
}
public MosaicOutput(RenderedImage image, PAMDataset pamDataset) {
super();
this.image = image;
this.pamDataset = pamDataset;
}
public RenderedImage getImage() {
return image;
}
public void setImage(RenderedImage image) {
this.image = image;
}
public PAMDataset getPamDataset() {
return pamDataset;
}
public void setPamDataset(PAMDataset pamDataset) {
this.pamDataset = pamDataset;
}
RenderedImage image;
PAMDataset pamDataset;
}
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;
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)) }
: 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;
}
@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 Unit<?> getUnits() {
return unit;
}
@Override
public double getScale() {
return scale;
}
@Override
public ColorInterpretation getColorInterpretation() {
return color;
}
@Override
public InternationalString[] getCategoryNames() throws IllegalStateException {
return new InternationalString[] { SimpleInternationalString.wrap("Background") };
}
}
/**
* This class is responsible for putting together the granules for the final mosaic.
*
* @author Simone Giannecchini, GeoSolutions SAS
*/
private class MosaicProducer implements GranuleCatalogVisitor {
/**
* The number of granules actually dispatched to the internal collectors.
*/
private int granulesNumber;
/**
* The {@link MergeBehavior} indicated into the request.
*/
private MergeBehavior mergeBehavior;
/**
* The internal collectors for incoming granules.
*/
private List<SubmosaicProducer> granuleCollectors = new ArrayList<>();
/**
* Default {@link Constructor}
*/
private MosaicProducer(List<SubmosaicProducer> collectors) {
this(false, collectors);
}
/**
* {@link MosaicProducer} constructor. It can be used to specify that we want to perform a dry run just to count the granules we would load
* with the specified query.
* <p>
* <p>
* A dry run means: no tasks are executed.
*
* @param dryRun <code>true</code> for a dry run, <code>false</code> otherwise.
* @param collectorsFactory
*/
private MosaicProducer(final boolean dryRun, List<SubmosaicProducer> collectors) {
this.granuleCollectors = collectors;
this.mergeBehavior = request.getMergeBehavior();
}
/**
* This method accepts incming granules and dispatch them to the correct {@link DefaultSubmosaicProducer} depending on the internal
* {@link Filter} per the dimension.
* <p>
* <p>
* If not {@link MergeBehavior#STACK}ing is required, we collect them all together with an include filter.
*/
public void visit(GranuleDescriptor granuleDescriptor, SimpleFeature sf) {
//
// load raster data
//
// create a granuleDescriptor loader
final Geometry bb = JTS.toGeometry((BoundingBox) mosaicBBox);
Geometry inclusionGeometry = granuleDescriptor.getFootprint();
boolean intersects = false;
if (inclusionGeometry != null) {
CoordinateReferenceSystem granuleCRS = granuleDescriptor.getGranuleEnvelope().getCoordinateReferenceSystem();
CoordinateReferenceSystem mosaicCRS = mosaicBBox.getCoordinateReferenceSystem();
try {
if(!CRS.equalsIgnoreMetadata(granuleCRS, mosaicCRS)) {
MathTransform mt = CRS.findMathTransform(granuleCRS, mosaicCRS);
inclusionGeometry = JTS.transform(inclusionGeometry, mt);
}
intersects = inclusionGeometry.intersects(bb);
} catch (FactoryException | MismatchedDimensionException | TransformException e) {
// in case there was a reprojection issue assume intersection
intersects = true;
}
}
if (!footprintBehavior.handleFootprints() || inclusionGeometry == null
|| (footprintBehavior.handleFootprints() && intersects)) {
// find the right filter for this granule
boolean found = false;
for (SubmosaicProducer submosaicProducer : granuleCollectors) {
if (submosaicProducer.accept(granuleDescriptor)) {
granulesNumber++;
found = true;
break;
}
}
// did we find a place for it? If we are doing EGR then it's ok, otherwise not so much
if (!found && getExcessGranuleRemover() == null) {
throw new IllegalStateException("Unable to locate a filter for this granule:\n"
+ granuleDescriptor.toString());
}
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("We rejected for non ROI inclusion the granule "
+ granuleDescriptor.toString());
}
}
}
@Override
public boolean isVisitComplete() {
ROIExcessGranuleRemover remover = getExcessGranuleRemover();
return remover != null && remover.isRenderingAreaComplete();
}
/**
* This method is responsible for producing the final mosaic.
* <p>
* <p>
* Depending on whether or not a {@link MergeBehavior#STACK}ing is required, we perform 1 or 2 steps.
* <ol>
* <li>step 1 is for merging flat on each value for the dimension</li>
* <li>step 2 is for merging stack on the resulting mosaics</li>
* </ol>
*
* @return
* @throws IOException
*/
private MosaicOutput produce() throws IOException {
// checks
if (granulesNumber == 0) {
LOGGER.log(Level.FINE, "Unable to load any granuleDescriptor");
return null;
}
// STEP 1 collect all the mosaics from each single dimension
LOGGER.fine("Producing the final mosaic, step 1, loop through granule collectors");
final List<MosaicElement> mosaicInputs = new ArrayList<MosaicElement>();
SubmosaicProducer first = null; // we take this apart to steal some val
int size = 0;
for (SubmosaicProducer collector : granuleCollectors) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.fine("Submosaic producer being called: " + collector.toString());
}
final List<MosaicElement> preparedMosaic = collector.createMosaic();
if (preparedMosaic.size() > 0
&& !preparedMosaic.stream().allMatch(p -> p == null)) {
size += preparedMosaic.size();
mosaicInputs.addAll(preparedMosaic);
if (first == null) {
first = collector;
}
}
}
LOGGER.fine("Producing the final mosaic, step 2, final mosaicking");
// optimization
if (size == 1) {
// we don't need to mosaick again
return new MosaicOutput(mosaicInputs.get(0));
}
// no mosaics produced, it might happen, see above
if (size == 0) {
return null;
}
MosaicInputs mosaickingInputs = new MosaicInputs(first.doInputTransparency(),
first.hasAlpha(), mosaicInputs, first.getSourceThreshold());
// normal situation
return new MosaicOutput(
new Mosaicker(RasterLayerResponse.this, mosaickingInputs, mergeBehavior)
.createMosaic());
}
public SubmosaicProducerFactory getGranuleCollectorsFactory() {
return new DefaultSubmosaicProducerFactory();
}
}
/**
* Logger.
*/
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(RasterLayerResponse.class);
/**
* The GridCoverage produced after a {@link #createResponse()} 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 EnhancedImageReadParam baseReadParameters = new EnhancedImageReadParam();
private boolean multithreadingAllowed;
private FootprintBehavior footprintBehavior = FootprintBehavior.None;
private int defaultArtifactsFilterThreshold = Integer.MIN_VALUE;
private double artifactsFilterPTileThreshold = ImageMosaicFormat.DEFAULT_ARTIFACTS_FILTER_PTILE_THRESHOLD;
private boolean oversampledRequest;
private MathTransform baseGridToWorld;
private Interpolation interpolation;
private boolean needsReprojection;
private double[] backgroundValues;
private Hints hints;
private String granulesPaths;
/**
* See {@link GridCoverage2DReader#SOURCE_URL_PROPERTY}.
*/
private URL sourceUrl;
private ROIExcessGranuleRemover excessGranuleRemover;
/**
* 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 rasterManager raster manager being used
* @param collectorsFactory
*/
public RasterLayerResponse(final RasterLayerRequest request, final RasterManager rasterManager,
SubmosaicProducerFactory collectorsFactory) {
this.request = request;
coverageEnvelope = rasterManager.spatialDomainManager.coverageEnvelope;
this.coverageFactory = rasterManager.getCoverageFactory();
this.rasterManager = rasterManager;
this.hints = rasterManager.getHints();
this.submosaicProducerFactory = collectorsFactory;
baseGridToWorld = rasterManager.spatialDomainManager.coverageGridToWorld2D;
finalTransparentColor = request.getOutputTransparentColor();
// are we doing multithreading?
multithreadingAllowed = request.isMultithreadingAllowed();
footprintBehavior = request.getFootprintBehavior();
backgroundValues = request.getBackgroundValues();
interpolation = request.getInterpolation();
needsReprojection = request.spatialRequestHelper.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 #createResponse()} 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.
*
* @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;
}
// add extra parameters to image parameters reader
baseReadParameters.setBands(request.getBands());
// assemble granules
final MosaicOutput mosaic = prepareResponse();
if (mosaic == null || mosaic.image == null) {
this.gridCoverage = null;
return;
}
// postproc
MosaicOutput finalMosaic = postProcessRaster(mosaic);
// create the coverage
gridCoverage = prepareCoverage(finalMosaic);
}
private MosaicOutput postProcessRaster(MosaicOutput mosaickedImage) {
// alpha on the final mosaic
if (finalTransparentColor != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Support for alpha on final mosaic");
}
// Keep ROI as property
ImageWorker imageWorker = new ImageWorker(mosaickedImage.image);
imageWorker.makeColorTransparent(finalTransparentColor);
RenderedOp image = imageWorker.getRenderedOperation();
if (imageWorker.getROI() != null) {
image.setProperty("ROI", imageWorker.getROI());
}
return new MosaicOutput(image, mosaickedImage.pamDataset);
}
return mosaickedImage;
}
/**
* This method loads the granules which overlap the requested {@link GeneralEnvelope} using the provided values for alpha and input ROI.
*
* @return the mosaic output for the request
* @throws DataSourceException
*/
private MosaicOutput prepareResponse() throws DataSourceException {
try {
// === select overview
chooseOverview();
// === extract bbox
initBBOX();
// === init transformations
initTransformations();
// === init raster bounds
initRasterBounds();
// === init excess granule removal if needed
initExcessGranuleRemover();
// === create query and basic BBOX filtering
final Query query = initQuery();
// === manage additional filters
handleAdditionalFilters(query);
// === sort by clause
handleSortByClause(query);
// === collect granules
final MosaicProducer visitor = new MosaicProducer(submosaicProducerFactory
.createProducers(this.getRequest(), this.getRasterManager(), this, false));
rasterManager.getGranuleDescriptors(query, visitor);
// get those granules and create the final mosaic
MosaicOutput returnValue = 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.
//
//
// 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.
//
if (returnValue != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Loaded bbox " + mosaicBBox.toString() + " while crop bbox "
+ request.spatialRequestHelper.getComputedBBox().toString());
}
return returnValue;
}
if (visitor.granulesNumber == 0) {
// Redo the query without filter to check whether we got no granules due
// to a filter. In that case we need to return null
// Notice that we are using a dryRun visitor to make sure we don't
// spawn any loading tasks, we also ensure we get only 1 feature at most
// to make this blazing fast
LOGGER.fine("We got no granules, let's do a dry run with no filters");
List<SubmosaicProducer> collectors = submosaicProducerFactory
.createProducers(this.getRequest(), this.getRasterManager(), this, true);
final MosaicProducer dryRunVisitor = new MosaicProducer(true, collectors);
final Utils.BBOXFilterExtractor bboxExtractor = new Utils.BBOXFilterExtractor();
query.getFilter().accept(bboxExtractor, null);
query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.bbox(
FeatureUtilities.DEFAULT_FILTER_FACTORY.property(rasterManager
.getGranuleCatalog().getType(rasterManager.getTypeName())
.getGeometryDescriptor().getName()),
bboxExtractor.getBBox()));
query.setMaxFeatures(1);
rasterManager.getGranuleDescriptors(query, dryRunVisitor);
if (dryRunVisitor.granulesNumber > 0) {
LOGGER.fine(
"Dry run got a target granule, returning null as the additional filters did filter all the granules out");
// It means the previous lack of granule was due to a filter excluding all the results. Then we return null
return null;
}
}
// prepare a blank response
return createBlankResponse();
} catch (Exception e) {
throw new DataSourceException("Unable to create this mosaic", e);
}
}
private void initExcessGranuleRemover() {
if(request.getExcessGranuleRemovalPolicy() == ExcessGranulePolicy.ROI) {
Dimension tileDimensions = request.getTileDimensions();
int tileWidth, tileHeight;
if(tileDimensions != null) {
tileWidth = (int) tileDimensions.getWidth();
tileHeight = (int) tileDimensions.getHeight();
} else {
tileWidth = tileHeight = ROIExcessGranuleRemover.DEFAULT_TILE_SIZE;
}
excessGranuleRemover = new ROIExcessGranuleRemover(rasterBounds, tileWidth, tileHeight, rasterManager.getConfiguration().getCrs());
}
}
/**
* This method is responsible for computing the raster bounds of the final mosaic.
*
* @throws TransformException In case transformation fails during the process.
*/
private void initRasterBounds() throws TransformException {
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);
}
/**
* This method is responsible for initializing transformations g2w and back
*
* @throws Exception in case we don't manage to instantiate some of them.
*/
private void initTransformations() throws Exception {
// compute final world to grid
// base grid to world for the center of pixels
final AffineTransform g2w;
final SpatialRequestHelper spatialRequestHelper = request.spatialRequestHelper;
if (!request.isHeterogeneousGranules()) {
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 = spatialRequestHelper.getComputedResolution();
BoundingBox computedBBox = spatialRequestHelper.getComputedBBox();
GeneralEnvelope requestedRasterArea = CRS.transform(baseGridToWorld.inverse(), computedBBox);
double minxRaster = Math.round(requestedRasterArea.getMinimum(0));
double minyRaster = Math.round(requestedRasterArea.getMinimum(1));
// rebase the grid to world location to a position close to the requested one to
// avoid JAI playing with very large raster coordinates
// This can be done because the final computation generates the coordinates of the
// output coverage based on the output raster bounds and this very transform
final AffineTransform at = (AffineTransform) baseGridToWorld;
Point2D src = new Point2D.Double(minxRaster, minyRaster);
Point2D dst = new Point2D.Double();
at.transform(src, dst);
g2w = new AffineTransform(at.getScaleX(), at.getShearX(), at.getShearY(), at.getScaleY(), dst.getX(), dst.getY());
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()));
}
} else {
g2w = new AffineTransform(spatialRequestHelper.getComputedGridToWorld());
g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER);
}
// move it to the corner
finalGridToWorldCorner = new AffineTransform2D(g2w);
finalWorldToGridCorner = finalGridToWorldCorner.inverse();// compute raster bounds
}
/**
* This method is responsible for initializing the bbox for the mosaic produced by this response.
*/
private void initBBOX() {
// ok we got something to return, let's load records from the index
final BoundingBox cropBBOX = request.spatialRequestHelper.getComputedBBox();
if (cropBBOX != null) {
mosaicBBox = ReferencedEnvelope.reference(cropBBOX);
} else {
mosaicBBox = new ReferencedEnvelope(coverageEnvelope);
}
}
/**
* This method encloses the standard behavior for the selection of the proper overview level.
* <p>
* See {@link ReadParamsController}
*/
private void chooseOverview() throws IOException, TransformException {
//
// 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.spatialRequestHelper.getComputedBBox() != null
&& request.spatialRequestHelper.getComputedRasterArea() != null
&& !request.isHeterogeneousGranules()) {
imageChoice = ReadParamsController.setReadParams(
request.spatialRequestHelper.getComputedResolution(),
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("Loading level " + imageChoice + " with subsampling factors "
+ baseReadParameters.getSourceXSubsampling() + " "
+ baseReadParameters.getSourceYSubsampling());
}
}
/**
* This method is responsible for initializing the {@link Query} object with the BBOX filter as per the incoming {@link RasterLayerRequest}.
*
* @return a {@link Query} object with the BBOX {@link Filter} in it.
* @throws IOException in case something bad happens
*/
private Query initQuery() throws Exception {
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);
final String typeName = rasterManager.getTypeName();
Filter bbox = null;
if (typeName != null) {
Query query = new Query(typeName);
// max number of elements
if (request.getMaximumNumberOfGranules() > 0) {
query.setMaxFeatures(request.getMaximumNumberOfGranules());
}
bbox = FeatureUtilities.DEFAULT_FILTER_FACTORY
.bbox(FeatureUtilities.DEFAULT_FILTER_FACTORY
.property(rasterManager.getGranuleCatalog().getType(typeName)
.getGeometryDescriptor().getName()),
mosaicBBox);
query.setFilter(bbox);
return query;
} else {
throw new IllegalStateException("GranuleCatalog feature type was null!!!");
}
}
/**
* This method is responsible for creating the filters needed for addtional dimensions like TIME, ELEVATION additional Domains
*
* @param query the {@link Query} to set filters for.
*/
private void handleAdditionalFilters(Query query) {
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);
// 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.elevationDomainManager
.createFilter(GridCoverage2DReader.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.timeDomainManager
.createFilter(GridCoverage2DReader.TIME_DOMAIN, times);
query.setFilter(
FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(), timeFilter));
}
// === Custom Domains Management
if (hasAdditionalDomains) {
final List<Filter> additionalFilter = new ArrayList<>();
for (Entry<String, List> entry : additionalDomains.entrySet()) {
// build a filter for each dimension
final String domainName = entry.getKey() + DomainDescriptor.DOMAIN_SUFFIX;
additionalFilter.add(
rasterManager.domainsManager.createFilter(domainName, entry.getValue()));
}
// merge with existing ones
query.setFilter(FeatureUtilities.DEFAULT_FILTER_FACTORY.and(query.getFilter(),
FeatureUtilities.DEFAULT_FILTER_FACTORY.and(additionalFilter)));
}
}
/**
* Handles the optional {@link SortBy} clause for the query to the catalog
*
* @param query the {@link Query} to set the {@link SortBy} for.
*/
private void handleSortByClause(final Query query) {
Utilities.ensureNonNull("query", query);
LOGGER.fine("Prepping to manage SortBy 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<>(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));
LOGGER.fine("Added clause ASCENDING on attribute:" + attribute);
} 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));
LOGGER.fine("Added clause DESCENDING on attribute:" + attribute);
} else {
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!
this.sortBy = clauses.toArray(new SortBy[] {});
if (rasterManager.getGranuleCatalog()
.getQueryCapabilities(rasterManager.getTypeName()).supportsSorting(sortBy)) {
query.setSortBy(sortBy);
}
} else {
LOGGER.fine("No SortBy Clause");
}
}
}
/**
* This method is responsible for creating a blank image as a reponse to the query as it seems we got a no data area.
*
* @return a blank {@link RenderedImage} initialized using the background values
*/
private MosaicOutput createBlankResponse() {
// 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.
LOGGER.fine("Creating constant image for area with no data");
final ImageLayout2 il = new ImageLayout2();
il.setColorModel(rasterManager.defaultCM);
Dimension tileSize = request.getTileDimensions();
if (tileSize == null) {
tileSize = JAI.getDefaultTileSize();
}
il.setTileGridXOffset(0).setTileGridYOffset(0).setTileWidth((int) tileSize.getWidth())
.setTileHeight((int) tileSize.getHeight());
final RenderingHints renderingHints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, il);
final Number[] values = ImageUtilities.getBackgroundValues(rasterManager.defaultSM,
backgroundValues);
RenderedImage finalImage;
if (ImageUtilities.isMediaLibAvailable()) {
// create a constant image with a proper layout
finalImage = ConstantDescriptor.create((float) rasterBounds.width,
(float) rasterBounds.height, values, renderingHints);
if (rasterBounds.x != 0 || rasterBounds.y != 0) {
ImageWorker w = new ImageWorker(finalImage);
w.translate((float) rasterBounds.x, (float) rasterBounds.y,
Interpolation.getInstance(Interpolation.INTERP_NEAREST));
finalImage = w.getRenderedImage();
}
// impose the color model and samplemodel as the constant operation does not take them
// into account!
if (rasterManager.defaultCM != null) {
il.setColorModel(rasterManager.defaultCM);
il.setSampleModel(rasterManager.defaultCM
.createCompatibleSampleModel(tileSize.width, tileSize.height));
finalImage = new ImageWorker(finalImage).setRenderingHints(renderingHints)
.format(il.getSampleModel(null).getDataType()).getRenderedImage();
}
} else {
il.setWidth(rasterBounds.width).setHeight(rasterBounds.height);
if (rasterBounds.x != 0 || rasterBounds.y != 0) {
il.setMinX(rasterBounds.x).setMinY(rasterBounds.y);
}
// impose the color model and samplemodel as the constant operation does not take them
// into account!
ColorModel cm;
if (rasterManager.defaultCM != null) {
cm = rasterManager.defaultCM;
} else {
byte[] arr = { (byte) 0, (byte) 0xff };
cm = new IndexColorModel(1, 2, arr, arr, arr);
}
il.setColorModel(cm);
il.setSampleModel(cm.createCompatibleSampleModel(tileSize.width, tileSize.height));
final double[] bkgValues = new double[values.length];
for (int i = 0; i < values.length; i++) {
bkgValues[i] = values[i].doubleValue();
}
Assert.isTrue(il.isValid(ImageLayout.WIDTH_MASK | ImageLayout.HEIGHT_MASK
| ImageLayout.SAMPLE_MODEL_MASK));
ImageWorker w = new ImageWorker(renderingHints);
w.setBackground(bkgValues);
w.mosaic(new RenderedImage[0], MosaicDescriptor.MOSAIC_TYPE_OVERLAY, null, null,
new double[][] { { CoverageUtilities
.getMosaicThreshold(il.getSampleModel(null).getDataType()) } },
new Range[] { RangeFactory.create(0, 0) });
finalImage = w.getRenderedImage();
}
//
// TRANSPARENT COLOR MANAGEMENT
//
Color inputTransparentColor = request.getInputTransparentColor();
boolean hasAlpha;
if (inputTransparentColor != null
&& (footprintBehavior == null || !footprintBehavior.handleFootprints())) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Support for alpha on blank image");
}
finalImage = new ImageWorker(finalImage).makeColorTransparent(inputTransparentColor)
.getRenderedImage();
hasAlpha = finalImage.getColorModel().hasAlpha();
if (!hasAlpha) {
// if the resulting image has no transparency (can happen with IndexColorModel then we need to try component
// color model
finalImage = new ImageWorker(finalImage).forceComponentColorModel(true)
.makeColorTransparent(inputTransparentColor).getRenderedImage();
hasAlpha = finalImage.getColorModel().hasAlpha();
}
assert hasAlpha;
} else if (footprintBehavior != null) {
finalImage = footprintBehavior.postProcessBlankResponse(finalImage, renderingHints);
}
return new MosaicOutput(finalImage, null);
}
/**
* This method is responsible for creating a coverage from the supplied {@link RenderedImage}.
*
* @param image
* @return
* @throws IOException
*/
private GridCoverage2D prepareCoverage(MosaicOutput mosaicOutput) throws IOException {
// creating bands
final RenderedImage image = mosaicOutput.image;
final SampleModel sm = image.getSampleModel();
final ColorModel cm = image.getColorModel();
final int numBands = request.getBands() == null ? sm.getNumBands() : request.getBands().length;
// quick check the possible provided bands names are equal the number of bands
if (rasterManager.providedBandsNames != null && rasterManager.providedBandsNames.length != numBands) {
// let's see if bands have been selected
if (request.getBands() == null) {
// no definitively there is something wrong
throw new IllegalArgumentException("The number of provided bands names is different from the number of bands.");
}
}
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;
// checking if bands names are provided, typical case for multiple bands dimensions
if (rasterManager.providedBandsNames != null) {
// we need to take in consideration if bands were selected
if (request.getBands() == null) {
bandName = rasterManager.providedBandsNames[i];
} else {
// using the selected band index to retrieve the correct provided name
bandName = rasterManager.providedBandsNames[request.getBands()[i]];
}
}
if (cm != null) {
// === color interpretation
colorInterpretation = TypeMap.getColorInterpretation(cm, i);
if (colorInterpretation == null) {
throw new IOException("Unrecognized sample dimension type");
}
if (bandName == null) {
bandName = colorInterpretation.name();
if (colorInterpretation == ColorInterpretation.UNDEFINED
|| bandNames.contains(bandName)) {// make sure we create no duplicate band names
bandName = "Band" + (i + 1);
}
}
} else { // no color model
if (bandName == null) {
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;
Double noDataAsProperty = getNoDataProperty(image);
if (noDataAsProperty != null) {
noData = noDataAsProperty.doubleValue();
} else 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);
}
// creating the final coverage by keeping into account the fact that we
Map<String, Object> properties = new HashMap<String, Object>();
if (granulesPaths != null) {
properties.put(GridCoverage2DReader.FILE_SOURCE_PROPERTY, granulesPaths);
}
if (sourceUrl != null) {
properties.put(GridCoverage2DReader.SOURCE_URL_PROPERTY, sourceUrl);
}
if (mosaicOutput.pamDataset != null) {
properties.put(Utils.PAM_DATASET, mosaicOutput.pamDataset);
}
// Setting NoData as the NoData for the first Band
ImageWorker w = new ImageWorker(image);
CoverageUtilities.setNoDataProperty(properties, w.getNoData());
// Setting ROI property
Object property = image.getProperty("ROI");
if (property != null && property instanceof ROI) {
CoverageUtilities.setROIProperty(properties, (ROI) property);
}
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);
}
private Double getNoDataProperty(RenderedImage image) {
if (image != null) {
Object obj = image.getProperty(NoDataContainer.GC_NODATA);
if (obj != null) {
if (obj instanceof NoDataContainer) {
return ((NoDataContainer) obj).getAsSingleValue();
} else if (obj instanceof Double) {
return (Double) obj;
}
}
}
return null;
}
public RasterLayerRequest getRequest() {
return request;
}
public FootprintBehavior getFootprintBehavior() {
return footprintBehavior;
}
public ImageReadParam getBaseReadParameters() {
return baseReadParameters;
}
public MathTransform2D getFinalGridToWorldCorner() {
return finalGridToWorldCorner;
}
public MathTransform2D getFinalWorldToGridCorner() {
return finalWorldToGridCorner;
}
public ReferencedEnvelope getMosaicBBox() {
return mosaicBBox;
}
public Color getFinalTransparentColor() {
return finalTransparentColor;
}
public Rectangle getRasterBounds() {
return rasterBounds;
}
public MathTransform getBaseGridToWorld() {
return baseGridToWorld;
}
public int getImageChoice() {
return imageChoice;
}
public void setImageChoice(int imageChoice) {
this.imageChoice = imageChoice;
}
public boolean isMultithreadingAllowed() {
return multithreadingAllowed;
}
public RasterManager getRasterManager() {
return rasterManager;
}
public Hints getHints() {
return hints;
}
public void setGranulesPaths(String granulesPaths) {
this.granulesPaths = granulesPaths;
}
/**
* See {@link GridCoverage2DReader#SOURCE_URL_PROPERTY}.
*
* @param sourceUrl
*/
public void setSourceUrl(URL sourceUrl) {
this.sourceUrl = sourceUrl;
}
public int getDefaultArtifactsFilterThreshold() {
return defaultArtifactsFilterThreshold;
}
public double getArtifactsFilterPTileThreshold() {
return artifactsFilterPTileThreshold;
}
public SortBy[] getSortBy() {
return sortBy;
}
public double[] getBackgroundValues() {
return backgroundValues;
}
public ROIExcessGranuleRemover getExcessGranuleRemover() {
return excessGranuleRemover;
}
/**
* Builds an alternate view of request/response/manager based on a template descriptor
* @param templateDescriptor
* @return
* @throws Exception
*/
public RasterLayerResponse reprojectTo(GranuleDescriptor templateDescriptor) throws Exception {
// optimization in case the granule CRS and the mosaic CRS correspond
CoordinateReferenceSystem granuleCRS = templateDescriptor.getGranuleEnvelope().getCoordinateReferenceSystem();
if(CRS.equalsIgnoreMetadata(rasterManager.spatialDomainManager.coverageCRS2D, granuleCRS)) {
return this;
}
// rebuild
RasterLayerRequest originalRequest = this.getRequest();
RasterManager originalRasterManager = originalRequest.getRasterManager();
RasterManager manager = originalRasterManager.getForGranuleCRS(templateDescriptor, this.mosaicBBox);
RasterLayerRequest request = new RasterLayerRequest(originalRequest.getParams(), manager);
RasterLayerResponse response = new RasterLayerResponse(request, manager,
this.submosaicProducerFactory);
// initialize enough info without actually running the output computation
response.chooseOverview();
response.initBBOX();
response.initTransformations();
response.initRasterBounds();
return response;
}
}