/*
* 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.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import javax.media.jai.Interpolation;
import javax.media.jai.InterpolationNearest;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.processing.Operations;
import org.geotools.factory.Hints;
import org.geotools.gce.imagemosaic.GranuleDescriptor;
import org.geotools.gce.imagemosaic.MergeBehavior;
import org.geotools.gce.imagemosaic.MosaicElement;
import org.geotools.gce.imagemosaic.Mosaicker;
import org.geotools.gce.imagemosaic.RasterLayerRequest;
import org.geotools.gce.imagemosaic.RasterLayerResponse;
import org.geotools.gce.imagemosaic.RasterManager;
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.image.ImageUtilities;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import it.geosolutions.jaiext.range.NoDataContainer;
/**
* SubmosaicProducer that can handle reprojecting its contents into the target mosaic CRS. This
* works by grouping together everything with a like CRS (and like SortBy property if supplied) and
* mosaicking them separately before forming a final mosaic.
*
* This relies on the SortBy including CRS as a final SortBy clause
*/
class ReprojectingSubmosaicProducer extends BaseSubmosaicProducer {
private final boolean dryRun;
private final RenderingHints renderingHints;
// operations factory to use for resampling
private final Operations operations;
private CoordinateReferenceSystem targetCRS;
private List<CRSBoundMosaicProducer> perMosaicProducers = new ArrayList<>();
private CRSBoundMosaicProducer currentSubmosaicProducer;
ReprojectingSubmosaicProducer(RasterLayerRequest request, RasterLayerResponse response,
RasterManager rasterManager, boolean dryRun) {
super(response, dryRun);
this.targetCRS = rasterManager.getConfiguration().getCrs();
this.dryRun = dryRun;
Hints hints = rasterManager.getHints();
this.renderingHints = createRenderingHints(hints, request);
this.operations = new Operations(renderingHints);
}
private static RenderingHints createRenderingHints(Hints hints, RasterLayerRequest request) {
RenderingHints renderHints = new RenderingHints(null);
if (request.getInterpolation() != null) {
renderHints.put(JAI.KEY_INTERPOLATION, request.getInterpolation());
}
return renderHints;
}
@Override
public boolean accept(GranuleDescriptor granuleDescriptor) {
// we have a current CRS group, either it matches or we need to create a new one
boolean accepted = currentSubmosaicProducer != null
&& currentSubmosaicProducer.accept(granuleDescriptor);
if (!accepted) {
// either we have no producer, or the granule was rejected by the current one,
// presumably because its CRS didn't match, we need to create a new one because we've moved on to the next
CoordinateReferenceSystem targetCRS = granuleDescriptor.getGranuleEnvelope()
.getCoordinateReferenceSystem();
try {
RasterLayerResponse transformedResponse = rasterLayerResponse
.reprojectTo(granuleDescriptor);
this.currentSubmosaicProducer = new CRSBoundMosaicProducer(transformedResponse,
dryRun, targetCRS, granuleDescriptor);
perMosaicProducers.add(currentSubmosaicProducer);
accepted = currentSubmosaicProducer.acceptGranule(granuleDescriptor);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to setup CRS specific sub-mosaic", e);
}
}
return accepted;
}
protected static CoordinateReferenceSystem getCRS(String granuleCRSCode) throws FactoryException {
return CRS.decode(granuleCRSCode);
}
@Override
public List<MosaicElement> createMosaic() throws IOException {
List<MosaicElement> mosaicInputs = new ArrayList<>();
for (CRSBoundMosaicProducer mosaicProducer : this.perMosaicProducers) {
List<MosaicElement> mosaicElement = mosaicProducer.createMosaic();
this.hasAlpha = mosaicProducer.hasAlpha();
try {
for (MosaicElement e : mosaicElement) {
MosaicElement reprojectedMosaicElement = this.reprojectMosaicElement(e,
mosaicProducer);
mosaicInputs.add(reprojectedMosaicElement);
}
} catch (FactoryException e) {
throw new IllegalStateException(e);
}
}
return mosaicInputs;
}
private MosaicElement reprojectMosaicElement(MosaicElement mosaicElement,
CRSBoundMosaicProducer mosaicProducer) throws FactoryException {
if (!CRS.equalsIgnoreMetadata(targetCRS, mosaicProducer.getCrs())) {
GridCoverageFactory factory = new GridCoverageFactory(null);
ReferencedEnvelope submosaicBBOX = computeSubmosaicBoundingBox(mosaicProducer,
mosaicElement);
GridCoverage2D submosaicCoverage = factory.create("submosaic",
mosaicElement.getSource(), submosaicBBOX);
GridCoverage2D resampledCoverage = (GridCoverage2D) operations
.resample(submosaicCoverage, targetCRS);
RenderedImage resampledImage = positionInOutputMosaic(resampledCoverage);
PlanarImage alphaBand = resampledImage.getColorModel().hasAlpha()
? new ImageWorker(resampledImage).retainLastBand().getPlanarImage() : null;
Object property = resampledImage.getProperty("ROI");
ROI overallROI = (property instanceof ROI) ? (ROI) property : null;
return new MosaicElement(alphaBand, overallROI, resampledImage,
mosaicElement.getPamDataset());
} else {
return mosaicElement;
}
}
/**
* Computes the sub-mosaic spatial extend based on the image size and the target grid to world transformation
*
* @param mosaicProducer
* @param image
* @return
* @throws FactoryException
*/
private ReferencedEnvelope computeSubmosaicBoundingBox(CRSBoundMosaicProducer mosaicProducer,
MosaicElement mosaicElement) throws FactoryException {
RenderedImage image = mosaicElement.getSource();
MathTransform2D tx = mosaicProducer.rasterLayerResponse.getFinalGridToWorldCorner();
double[] mosaicked = new double[] { image.getMinX(), image.getMinY(),
image.getMinX() + image.getWidth(), image.getMinY() + image.getHeight() };
try {
tx.transform(mosaicked, 0, mosaicked, 0, 2);
} catch (TransformException e) {
throw new FactoryException(e);
}
ReferencedEnvelope submosaicBBOX = new ReferencedEnvelope(mosaicked[0], mosaicked[2],
mosaicked[1], mosaicked[3], mosaicProducer.getCrs());
return submosaicBBOX;
}
/**
* Given a coverage in the mosaic target CRS generates an RenderedImage properly positioned
* in the mosaic output raster space
*
* @param resampledCoverage
* @return
*/
private RenderedImage positionInOutputMosaic(GridCoverage2D resampledCoverage) {
RenderedImage image = resampledCoverage.getRenderedImage();
// now create the overall transform
final AffineTransform finalRaster2Model = new AffineTransform((AffineTransform2D) resampledCoverage.getGridGeometry().getGridToCRS());
finalRaster2Model.concatenate(CoverageUtilities.CENTER_TO_CORNER);
// keep into account translation factors to place this tile
AffineTransform finalWorldToGridCorner = (AffineTransform) rasterLayerResponse.getFinalWorldToGridCorner();
finalRaster2Model.preConcatenate(finalWorldToGridCorner);
RasterLayerRequest request = rasterLayerResponse.getRequest();
final Interpolation interpolation = request.getInterpolation();
// paranoiac check to avoid that JAI freaks out when computing its internal layouT on images that are too small
Rectangle2D finalLayout = ImageUtilities.layoutHelper(image,
(float) finalRaster2Model.getScaleX(), (float) finalRaster2Model.getScaleY(),
(float) finalRaster2Model.getTranslateX(),
(float) finalRaster2Model.getTranslateY(), interpolation);
if (finalLayout.isEmpty()) {
if (LOGGER.isLoggable(java.util.logging.Level.INFO))
LOGGER.info("Unable to create a granuleDescriptor " + this.toString()
+ " due to jai scale bug creating a null source area");
return null;
}
// apply the affine transform conserving indexed color model
final RenderingHints localHints = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL,
interpolation instanceof InterpolationNearest ? Boolean.FALSE : Boolean.TRUE);
if (XAffineTransform.isIdentity(finalRaster2Model,
CoverageUtilities.AFFINE_IDENTITY_EPS)) {
return image;
} else {
ImageWorker iw = new ImageWorker(image);
iw.setRenderingHints(localHints);
iw.affine(finalRaster2Model, interpolation, request.getBackgroundValues());
RenderedImage renderedImage = iw.getRenderedImage();
// Propagate NoData
if (iw.getNoData() != null) {
PlanarImage t = PlanarImage.wrapRenderedImage(renderedImage);
t.setProperty(NoDataContainer.GC_NODATA, new NoDataContainer(iw.getNoData()));
renderedImage = t;
}
return renderedImage;
}
}
/**
* This submosaic producer takes a CRS and then only accepts granules that match that CRS.
*
*/
private static class CRSBoundMosaicProducer extends BaseSubmosaicProducer {
private final CoordinateReferenceSystem crs;
public CRSBoundMosaicProducer(RasterLayerResponse rasterLayerResponse, boolean dryRun, CoordinateReferenceSystem targetCRS,
GranuleDescriptor templateDescriptor) {
super(rasterLayerResponse, dryRun);
this.crs = targetCRS;
// always accept the template granule descriptor
super.accept(templateDescriptor);
}
@Override
public List<MosaicElement> createMosaic() throws IOException {
final MosaicElement mosaic = (new Mosaicker(this.rasterLayerResponse,
collectGranules(), MergeBehavior.FLAT)).createMosaic(false, true);
if (mosaic == null) {
return Collections.emptyList();
} else {
return Collections.singletonList(mosaic);
}
}
@Override
public boolean accept(GranuleDescriptor granuleDescriptor) {
//make sure the CRSs match
boolean shouldAccept = false;
//need to check that the granule matches CRS
CoordinateReferenceSystem granuleCRS = granuleDescriptor.getGranuleEnvelope().getCoordinateReferenceSystem();
shouldAccept = CRS.equalsIgnoreMetadata(granuleCRS, this.crs);
return shouldAccept && super.accept(granuleDescriptor);
}
public CoordinateReferenceSystem getCrs() {
return crs;
}
}
}