/* * 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; import it.geosolutions.imageio.imageioimpl.EnhancedImageReadParam; import java.awt.Color; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.ColorModel; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.measure.unit.Unit; import javax.media.jai.JAI; import javax.media.jai.PlanarImage; import javax.media.jai.operator.ConstantDescriptor; import javax.media.jai.operator.FormatDescriptor; import javax.media.jai.util.ImagingException; 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.OverviewPolicy; import org.geotools.data.DataSourceException; import org.geotools.factory.Hints; import org.geotools.gce.OverviewsController.OverviewLevel; import org.geotools.gce.RasterDescriptor.RasterLoadingResult; import org.geotools.gce.geotiff.GeoTiffUtils; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.operation.transform.AffineTransform2D; import org.geotools.resources.coverage.CoverageUtilities; 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.opengis.coverage.ColorInterpretation; import org.opengis.coverage.SampleDimension; import org.opengis.coverage.SampleDimensionType; import org.opengis.coverage.grid.GridCoverage; 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.referencing.operation.TransformException; import org.opengis.util.InternationalString; /** * A RasterLayerResponse. An instance of this class is produced everytime a * requestCoverage is called to a reader. * * @author Daniele Romagnoli, GeoSolutions * @author Simone Giannecchini, GeoSolutions * */ 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", GeoTiffUtils.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")}; } } /** 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 transparentColor; private ReferencedEnvelope finalBBox; private Rectangle rasterBounds; private MathTransform2D finalGridToWorldCorner; private MathTransform2D finalWorldToGridCorner; private int overviewsLevel = 0; private EnhancedImageReadParam baseReadParameters = new EnhancedImageReadParam(); private MathTransform baseGridToWorld; private double[] backgroundValues; private Hints hints; private boolean oversampledRequest = false; /** * 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; hints = rasterManager.getHints(); coverageEnvelope = rasterManager.getCoverageEnvelope(); baseGridToWorld = rasterManager.getRaster2Model(); coverageFactory = rasterManager.getGridCoverageFactory(); this.rasterManager = rasterManager; backgroundValues = request.getBackgroundValues(); transparentColor = request.getInputTransparentColor(); } /** * 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(); //postproc RenderedImage finalRaster = postProcessRaster(mosaic); //create the coverage gridCoverage = prepareCoverage(finalRaster); } /** * This method loads the granules which overlap the requested {@link GeneralEnvelope} using * the provided values for alpha and input ROI. */ private RenderedImage prepareResponse() throws DataSourceException { try { // 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 levels, 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 rasterDescriptor 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 knowning the scale factors. if (request.getRequestedBBox() != null && request.getRequestedRasterArea() != null) { overviewsLevel = setReadParams(request.getOverviewPolicy(), baseReadParameters, request); } else { overviewsLevel = 0; } assert overviewsLevel >= 0; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Loading level " + overviewsLevel + " with subsampling factors " + baseReadParameters.getSourceXSubsampling() + " " + baseReadParameters.getSourceYSubsampling()); } final BoundingBox cropBBOX = request.getCropBBox(); if (cropBBOX != null) { finalBBox = ReferencedEnvelope.reference(cropBBOX); } else { finalBBox = new ReferencedEnvelope(coverageEnvelope); } // compute final world to grid base grid to world for the center of pixels final AffineTransform g2w = new AffineTransform((AffineTransform) baseGridToWorld); // move it to the corner g2w.concatenate(CoverageUtilities.CENTER_TO_CORNER); // keep into account levels and subsampling final OverviewLevel level = rasterManager.overviewsController.resolutionsLevels.get(overviewsLevel); final OverviewLevel baseLevel = rasterManager.overviewsController.resolutionsLevels.get(0); final AffineTransform2D adjustments = new AffineTransform2D( (level.resolutionX / baseLevel.resolutionX) * baseReadParameters.getSourceXSubsampling(), 0, 0, (level.resolutionY / baseLevel.resolutionY) * baseReadParameters.getSourceYSubsampling(), 0, 0); g2w.concatenate(adjustments); // move it to the corner finalGridToWorldCorner = new AffineTransform2D(g2w); finalWorldToGridCorner = finalGridToWorldCorner.inverse(); final GeneralEnvelope tempRasterBounds = CRS.transform(finalWorldToGridCorner, finalBBox); rasterBounds=tempRasterBounds.toRectangle2D().getBounds(); if (rasterBounds.width == 0) rasterBounds.width++; if (rasterBounds.height == 0) rasterBounds.height++; final double[] requestRes = request.getRequestedResolution(); final double resX = baseLevel.resolutionX; final double resY = baseLevel.resolutionY; if ((requestRes[0] < resX || requestRes[1] < resY) ) { // Using the best available resolution oversampledRequest = true; } if(oversampledRequest) rasterBounds.grow(2, 2); RenderedImage theImage=null; try { RasterLoadingResult result = rasterManager.rasterDescriptor.loadRaster(baseReadParameters, overviewsLevel, finalBBox, finalWorldToGridCorner, request, request.getTileDimensions()); theImage =result.getRaster(); if (theImage == null) { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Unable to load the raster with request " + request.toString()); } } // // Set final transformation // RasterLayerResponse.this.finalGridToWorldCorner=new AffineTransform2D(result.gridToWorld); } catch (ImagingException e) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.fine("Unable to load the raster with request " + request); } theImage = null; } catch (Throwable e) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.fine("Unable to load the raster with request " + request); } theImage = null; } if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Processing loaded raster data "); } // // Did we actually load anything?? Notice that it might happen that either we have // wholes inside the definition area for the image or we had some problem with // missing tiles, therefore it might happen that for some bboxes we don't have // anything to load. // if (theImage != null) { // // 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 (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Loaded finalBBox " + finalBBox.toString() + " while crop finalBBox " + request.getCropBBox()); } return theImage; } else { // 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.baseImageType.getSampleModel(), backgroundValues); // create a constant image with a proper layout final RenderedImage finalImage = ConstantDescriptor.create( Float.valueOf(rasterBounds.width), Float.valueOf(rasterBounds.height), values, null); if(rasterManager.baseImageType!=null&&rasterManager.baseImageType.getColorModel()!=null){ final ImageLayout2 il= new ImageLayout2(); il.setColorModel(rasterManager.baseImageType.getColorModel()); Dimension tileSize= request.getTileDimensions(); if(tileSize==null){ tileSize=JAI.getDefaultTileSize(); } il.setSampleModel(rasterManager.baseImageType.getColorModel().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 (IOException e) { throw new DataSourceException("Unable to create this mosaic", e); } catch (TransformException e) { throw new DataSourceException("Unable to create this mosaic", e); } } /** * 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]; // setting bands names. for (int i = 0; i < numBands; i++) { // color interpretation final ColorInterpretation colorInterpretation=TypeMap.getColorInterpretation(cm, i); if(colorInterpretation==null) throw new IOException("Unrecognized sample dimension type"); // 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( colorInterpretation.name(), st, colorInterpretation, noData, min, max, 1, //no scale 0, //no offset null ).geophysics(true); } return coverageFactory.create( rasterManager.getCoverageIdentifier(), image, new GridGeometry2D( new GridEnvelope2D(PlanarImage.wrapRenderedImage(image).getBounds()), PixelInCell.CELL_CORNER, finalGridToWorldCorner, this.rasterManager.getCoverageCRS(), hints), bands, null, null); } /** * This method is responsible for preparing the read param for doing an * {@link ImageReader#read(int, ImageReadParam)}. * * * <p> * This method is responsible for preparing the read param for doing an * {@link ImageReader#read(int, ImageReadParam)}. It sets the passed * {@link ImageReadParam} in terms of decimation on reading using the * provided requestedEnvelope and requestedDim to evaluate the needed * resolution. It also returns and {@link Integer} representing the index of * the raster to be read when dealing with multipage raster. * * @param overviewPolicy * it can be one of {@link Hints#VALUE_OVERVIEW_POLICY_IGNORE}, * {@link Hints#VALUE_OVERVIEW_POLICY_NEAREST}, * {@link Hints#VALUE_OVERVIEW_POLICY_QUALITY} or * {@link Hints#VALUE_OVERVIEW_POLICY_SPEED}. It specifies the * policy to compute the levels level upon request. * @param readParams * an instance of {@link ImageReadParam} for setting the * subsampling factors. * @param requestedEnvelope * the {@link GeneralEnvelope} we are requesting. * @param requestedDim * the requested dimensions. * @return the index of the raster to read in the underlying data sourceFile. * @throws IOException * @throws TransformException */ private int setReadParams(final OverviewPolicy overviewPolicy, final ImageReadParam readParams, final RasterLayerRequest request) throws IOException, TransformException { // Default image index 0 int imageChoice = 0; // default values for subsampling readParams.setSourceSubsampling(1, 1, 0, 0); // // Init overview policy // // // // when policy is explictly provided it overrides the policy provided // using hints. final OverviewPolicy policy; if (overviewPolicy == null) { policy = rasterManager.overviewPolicy; } else { policy = overviewPolicy; } // requested to ignore levels if (policy.equals(OverviewPolicy.IGNORE)) { return imageChoice; } // levels and decimation imageChoice = ReadParamsController.setReadParams( request.getRequestedResolution(), request.getOverviewPolicy(), request.getDecimationPolicy(), baseReadParameters, request.rasterManager, request.rasterManager.overviewsController); // use general levels controller return imageChoice; } private RenderedImage postProcessRaster(RenderedImage image) { // alpha on the final mosaic if (transparentColor != null) { // // TRANSPARENT COLOR MANAGEMENT // // if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Support for alpha on input image "); } return ImageUtilities.maskColor(transparentColor, image); } // if (!needsReprojection){ // try { // // // creating sourceFile 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, GeoTiffUtils.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); // } // } // ImageWorker iw = new ImageWorker(image); // iw.setRenderingHints(localHints); // iw.affine(targetWorldToGrid, new InterpolationNearest(), 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; } }