/* * Geotools 2 - OpenSource mapping toolkit * (C) 2003, Geotools Project Management Committee (PMC) * (C) 2002, Institut de Recherche pour le Développement * * 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; either * version 2.1 of the License, or (at your option) any later version. * * 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. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.gp; // J2SE dependencies import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.RenderedImage; import java.awt.image.renderable.ParameterBlock; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import javax.media.jai.ImageLayout; import javax.media.jai.IntegerSequence; import javax.media.jai.Interpolation; import javax.media.jai.InterpolationNearest; import javax.media.jai.JAI; import javax.media.jai.ParameterList; import javax.media.jai.ParameterListDescriptor; import javax.media.jai.ParameterListDescriptorImpl; import javax.media.jai.PlanarImage; import javax.media.jai.RenderedOp; import org.geotools.cs.CoordinateSystem; import org.geotools.ct.CoordinateTransformationFactory; import org.geotools.ct.MathTransform; import org.geotools.ct.MathTransform2D; import org.geotools.ct.MathTransformFactory; import org.geotools.cv.SampleDimension; import org.geotools.gc.GridCoverage; import org.geotools.gc.GridGeometry; import org.geotools.gc.GridRange; import org.geotools.gc.InvalidGridGeometryException; import org.geotools.pt.Envelope; import org.geotools.pt.Matrix; import org.geotools.resources.CTSUtilities; import org.geotools.resources.LegacyGCSUtilities; import org.geotools.resources.XArray; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.geotools.resources.image.ImageUtilities; import org.geotools.resources.image.JAIUtilities; import org.geotools.util.NumberRange; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.TransformException; /** * Resample a grid coverage using a different grid geometry. * This operation provides the following functionality:
*
* Resampling
* The grid coverage can be resampled at a different cell resolution. Some implementations * may be able to do resampling efficiently at any resolution. This can be determined from * the {@link GridCoverageProcessor} metadata HasArbitraryResolutions keyword. * Also a non-rectilinear grid coverage can be accessed as rectilinear grid coverage with * this operation.
*
* Reprojecting
* The new grid geometry can have a different coordinate system than the underlying grid * geometry. For example, a grid coverage can be reprojected from a geodetic coordinate * system to Universal Transverse Mercator coordinate system.
*
* Subsetting
* A subset of a grid can be viewed as a separate coverage by using this operation with a * grid geometry which as the same geoferencing and a region. Grid range in the grid geometry * defines the region to subset in the grid coverage.
* * @source $URL$ * @version $Id$ * @author Martin Desruisseaux * * @deprecated Replaced by {@link org.geotools.coverage.operation.Resampler2D}. */ final class Resampler extends GridCoverage { /** * Disable the native acceleration for the "Affine" operation. In JAI 1.1.2, the "Affine" * operation on TYPE_FLOAT datatype with INTERP_BILINEAR interpolation cause an exception * in the native code of medialib, which halt the Java Virtual Machine. Using the pure Java * implementation instead resolve the problem. * * @task TODO: Remove this hack when Sun will fix the medialib bug. */ static { ImageUtilities.allowNativeAcceleration("Affine", false); } /** * The logger for emmiting detail about resampling performed. */ private static final Logger LOGGER = Logger.getLogger("org.geotools.gp"); /** * Construct a new grid coverage for the specified grid geometry. * * @param source The source for this grid coverage. * @param image The image. * @param cs The coordinate system. * @param envelope The grid geometry. */ private Resampler(final GridCoverage source, final RenderedImage image, final CoordinateSystem cs, final GridGeometry geometry) { /* * If the grid geometry has more than 2 dimensions, then the current implementation * of GridCoverage assumes that grid range for all dimensions above 2 is [n..n+1]. * TODO: Should we accept the grid geometry as is? (open question...) */ super(source.getName(null), image, cs, geometry.getGridToCoordinateSystem(), source.getSampleDimensions(), new GridCoverage[]{source}, null); } /** * Construct a new grid coverage for the specified envelope. * * @param source The source for this grid coverage. * @param image The image. * @param cs The coordinate system. * @param envelope The grid coverage cordinates. The two first dimensions describe * the image location along x and y axis. The * other dimensions are optional and may be used to locate the image * on a vertical axis or on the time axis. */ private Resampler(final GridCoverage source, final RenderedImage image, final CoordinateSystem cs, final Envelope envelope) { super(source.getName(null), image, cs, envelope, source.getSampleDimensions(), new GridCoverage[]{source}, null); } /** * Create a new coverage with a different coordinate reference system. * * @param sourceCoverage The source grid coverage. * @param targetCS Coordinate system for the new grid coverage, or null. * @param targetGG The target grid geometry, or null for default. * @param interpolation The interpolation to use. * @param The rendering hingts. This is usually provided by a {@link GridCoverageProcessor}. * This method will looks for {@link Hints#COORDINATE_TRANSFORMATION_FACTORY} * and {@link Hints#JAI_INSTANCE} keys. * @return The new grid coverage, or sourceCoverage if no resampling was needed. * @throws FactoryException is a transformation step can't be created. * @throws TransformException if a transformation failed. */ public static GridCoverage reproject( GridCoverage sourceCoverage, final CoordinateSystem targetCS, GridGeometry targetGG, final Interpolation interpolation, final RenderingHints hints) throws FactoryException, TransformException { /* * Gets the {@link JAI} instance to use from the rendering hints. */ Object property = (hints!=null) ? hints.get(Hints.JAI_INSTANCE) : null; final JAI processor; if (property instanceof JAI) { processor = (JAI) property; } else { processor = JAI.getDefaultInstance(); } /* * Gets the {@link CoordinateTransformationFactory} to use from the rendering hints. */ property = (hints!=null) ? hints.get(Hints.COORDINATE_TRANSFORMATION_FACTORY) : null; final CoordinateTransformationFactory factory; if (property instanceof CoordinateTransformationFactory) { factory = (CoordinateTransformationFactory) property; } else { factory = CoordinateTransformationFactory.getDefault(); } final MathTransformFactory mtFactory = factory.getMathTransformFactory(); /* * If the source coverage is already the result of a "Resample" operation, * go up in the chain and check if a previously computed image could fits. */ GridGeometry sourceGG; boolean sameGG; CoordinateSystem sourceCS; boolean sameCS; while (true) { sourceGG = sourceCoverage.getGridGeometry(); sourceCS = sourceCoverage.getCoordinateSystem(); sameGG = (targetGG==null || equivalent(targetGG, sourceGG)); sameCS = (targetCS==null || targetCS.equals(sourceCS, false)); if (sameGG && sameCS) { return sourceCoverage; } if (sourceCoverage instanceof Resampler) { final GridCoverage[] sources = sourceCoverage.getSources(); if (sources.length != 1) { // Should not happen, but test anyway. throw new AssertionError(sources.length); } sourceCoverage = sources[0]; continue; } break; } /* * We use the next two lines mostly as an argument check. If a coordinate system * can't be reduced to a two-dimensional one, then an IllegalArgumentException * will be thrown. Note that a less rigourous check is performed later (compare * envelopes), so we could comment out this block in order to accept a wider * range of (possibly incorrect) coordinate systems. */ if (true) { CTSUtilities.getCoordinateSystem2D(sourceCS); CTSUtilities.getCoordinateSystem2D(targetCS); } /* * The projection are usually applied on floating-point values, in order * to gets maximal precision and to handle correctly the special case of * NaN values. However, we can apply the projection on integer values if * the interpolation type is "nearest neighbor", since this is not really * an interpolation. * * If this condition is meets, then we verify if an "integer version" of the image * is available as a source of the source coverage (i.e. the floating-point image * is derived from the integer image, not the converse). */ Boolean targetGeophysics = null; if (interpolation instanceof InterpolationNearest) { final GridCoverage candidate = sourceCoverage.geophysics(false); if (candidate != sourceCoverage) { final List sources = sourceCoverage.getRenderedImage().getSources(); if (sources != null) { if (sources.contains(candidate.getRenderedImage())) { sourceCoverage = candidate; targetGeophysics = Boolean.TRUE; } } } } /* * Gets the target image as a {@link RenderedOp}. The source image is * initially wrapped into in a "Null" operation. Later, we will change * this "Null" operation into a "Warp" operation. We can't use "Warp" * now because we will know the envelope only after creating the GridCoverage. * Note: RenderingHints contain mostly indications about tiles layout. */ final PlanarImage sourceImage = PlanarImage.wrapRenderedImage(sourceCoverage.getRenderedImage()); final ParameterBlock paramBlk = new ParameterBlock().addSource(sourceImage); RenderingHints targetHints = ImageUtilities.getRenderingHints(sourceImage); if (targetHints == null) { targetHints = hints; } else if (hints != null) { targetHints.add(hints); } final RenderedOp targetImage = processor.createNS("Null", paramBlk, targetHints); GridCoverage targetCoverage = null; /* * Compute the INVERSE of the math transform from [Source Grid] to [Target Grid]. * The transform will be computed using the inverse of the following paths: * * Source Grid --> Source CS --> Target CS --> Target Grid * * If source and target CS are equals, a shorter path is used. This special case * is needed because 'targetCS' may be null (which means "same CS than source"). * Note that 'sourceCS' may be null as well. * * Source Grid --> Common CS --> Target Grid */ final MathTransform transform, step1, step2, step3, step2x; if (sameCS) { step2x = null; step2 = MathTransform2D.IDENTITY; if (!LegacyGCSUtilities.hasTransform(targetGG)) { // TargetGG should not be null, otherwise the code above should // have already detected that this resample is not doing anything. targetGG = new GridGeometry(targetGG.getGridRange(), sourceGG.getGridToCoordinateSystem()); } } else { if (sourceCS==null || targetCS==null) { throw new CannotReprojectException(Errors.format(ErrorKeys.UNSPECIFIED_CRS)); } final MathTransform step2r; step2x = factory.createFromCoordinateSystems(sourceCS, targetCS).getMathTransform(); step2r = getMathTransform2D(step2x, mtFactory); /* * Gets the source and target envelope. It is difficult to check if the first * two dimensions are really independent from other dimensions. However, if * we get the same 2-dimensional envelope no matter if we took in account the * extra dimensions or not, then we will assume that projecting the image with * a MathTransform2D is safe enough. */ final Envelope sourceEnvelope = sourceCoverage.getEnvelope(); final Envelope sourceEnvelope2D = sourceEnvelope.getSubEnvelope(0,2); final Envelope targetEnvelope = CTSUtilities.transform(step2x, sourceEnvelope ); final Envelope targetEnvelope2D = CTSUtilities.transform(step2r, sourceEnvelope2D); if (!targetEnvelope.getSubEnvelope(0,2).equals(targetEnvelope2D)) { throw new TransformException(Errors.format( ErrorKeys.NO_TRANSFORM2D_AVAILABLE)); } /* * If the target GridGeometry is incomplete, provides default * values for the missing fields. Three cases may occur: * * - User provided no GridGeometry at all. Then, constructs an image of the same size * than the source image and set an envelope big enough to contains the projected * coordinates. The transform will derivate from the grid range and the envelope. * * - User provided only a grid range. Then, set an envelope big enough to contains * the projected coordinates. The transform will derivate from the grid range and * the envelope. * * - User provided only a "grid to coordinate system" transform. Then, transform the * projected envelope to "grid units" using the specified transform, and create a * grid range big enough to hold the result. */ if (targetGG == null) { targetCoverage=new Resampler(sourceCoverage, targetImage, targetCS, targetEnvelope); targetGG = targetCoverage.getGridGeometry(); } else if (!LegacyGCSUtilities.hasTransform(targetGG)) { targetGG = new GridGeometry(targetGG.getGridRange(), targetEnvelope, null); } else if (!LegacyGCSUtilities.hasGridRange(targetGG)) { final MathTransform step3x = targetGG.getGridToCoordinateSystem(); final Envelope gridRange = CTSUtilities.transform(step3x.inverse(), targetEnvelope); for (int i=gridRange.getDimension(); --i>=0;) { // According OpenGIS specification, GridGeometry maps pixel's center. But // the bounding box was for all pixels, not pixel's centers. Offset by // 0.5 (use +0.5 for maximum too, not -0.5, since maximum is exclusive). gridRange.setRange(i, gridRange.getMinimum(i)+0.5, gridRange.getMaximum(i)+0.5); } targetGG = new GridGeometry(LegacyGCSUtilities.toGridRange(gridRange), step3x); } step2 = step2r.inverse(); } /* * Complete the transformation from [Target Grid] to [Source Grid]. If the target grid * range was not explicitely specified, a grid range will be automatically computed in * such a way that it will map to the same envelope (at least approximatively). We use * the inverse of 'transform' for this purpose, except that the transform to be used for * grid range may have more than 2 dimensions. */ step1 = targetGG.getGridToCoordinateSystem2D(); step3 = sourceGG.getGridToCoordinateSystem2D().inverse(); if (step1.equals(step3.inverse())) { transform = step2; } else { transform = mtFactory.createConcatenatedTransform( mtFactory.createConcatenatedTransform(step1, step2), step3); } if (!(transform instanceof MathTransform2D)) { // Should not happen with Geotools implementations. May happen // with some external implementations, but should stay unusual. throw new TransformException(Errors.format( ErrorKeys.NO_TRANSFORM2D_AVAILABLE)); } if (!LegacyGCSUtilities.hasGridRange(targetGG)) { final MathTransform xtr; final MathTransform step1x = targetGG.getGridToCoordinateSystem(); final MathTransform step3x = sourceGG.getGridToCoordinateSystem().inverse(); if (step1x.equals(step3x.inverse())) { xtr = step2x; } else if (step2x==null) { xtr = mtFactory.createConcatenatedTransform(step1x, step3x); } else { xtr = mtFactory.createConcatenatedTransform( mtFactory.createConcatenatedTransform(step1x, step2x), step3x); } if (xtr != null) { assert getMathTransform2D(xtr, mtFactory).equals(transform) : xtr; Envelope envelope = LegacyGCSUtilities.toEnvelope(sourceGG.getGridRange()); envelope = CTSUtilities.transform(xtr.inverse(), envelope); targetGG = new GridGeometry(LegacyGCSUtilities.toGridRange(envelope), step1x); } else { assert transform.isIdentity() : transform; targetGG = sourceGG; } } /* * If the target coverage has not been created yet, change the image bounding box in * order to matches the grid range. If the target coverage has already been created, * just make sure that the image bounding box will stay constant even when we will * change the "Null" operation into a "Warp" one later. */ if (true) { final GridRange gridRange = (targetCoverage!=null ? targetCoverage.getGridGeometry() : targetGG).getGridRange(); int layoutMask = 0; ImageLayout layout; if (hints != null) { layout = (ImageLayout)hints.get(JAI.KEY_IMAGE_LAYOUT); if (layout != null) { layoutMask = layout.getValidMask(); } } layout = (ImageLayout)targetImage.getRenderingHint(JAI.KEY_IMAGE_LAYOUT); if (layout != null) { layout = (ImageLayout) layout.clone(); } else { layout = new ImageLayout(); } if (0==(layoutMask & (ImageLayout.MIN_X_MASK | ImageLayout.MIN_Y_MASK | ImageLayout.WIDTH_MASK | ImageLayout.HEIGHT_MASK))) { layout.setMinX (gridRange.getLower (0)); layout.setMinY (gridRange.getLower (1)); layout.setWidth (gridRange.getLength(0)); layout.setHeight(gridRange.getLength(1)); targetImage.setRenderingHint(JAI.KEY_IMAGE_LAYOUT, layout); } if (0==(layoutMask & (ImageLayout.TILE_WIDTH_MASK | ImageLayout.TILE_HEIGHT_MASK | ImageLayout.TILE_GRID_X_OFFSET_MASK | ImageLayout.TILE_GRID_Y_OFFSET_MASK))) { layout.setTileGridXOffset(layout.getMinX(targetImage)); layout.setTileGridYOffset(layout.getMinY(targetImage)); final int width = layout.getWidth (targetImage); final int height = layout.getHeight(targetImage); if (layout.getTileWidth (targetImage) > width ) layout.setTileWidth (width); if (layout.getTileHeight(targetImage) > height) layout.setTileHeight(height); targetImage.setRenderingHint(JAI.KEY_IMAGE_LAYOUT, layout); } } /* * If the user request a new grid geometry with the same coordinate system, and if * the grid geometry is equivalents to a simple extraction of a sub-area, delegate * the work to a "Crop" operation. */ if (transform.isIdentity()) { final GridRange sourceGR = sourceGG.getGridRange(); final GridRange targetGR = targetGG.getGridRange(); final int xmin = targetGR.getLower(0); final int xmax = targetGR.getUpper(0); final int ymin = targetGR.getLower(1); final int ymax = targetGR.getUpper(1); if (xmin >= sourceGR.getLower(0) && xmax <= sourceGR.getUpper(0) && ymin >= sourceGR.getLower(1) && ymax <= sourceGR.getUpper(1)) { paramBlk.add((float) (xmin)); paramBlk.add((float) (ymin)); paramBlk.add((float) (xmax-xmin)); paramBlk.add((float) (ymax-ymin)); targetImage.setParameterBlock(paramBlk); targetImage.setOperationName("Crop"); } } /* * Get the sample value to use for background. We will try to fetch this value from one * of "no data" categories. For geophysics image, it is usually NaN. For non-geophysics * image, it is usually 0. */ final SampleDimension[] sampleDimensions = sourceCoverage.getSampleDimensions(); final double[] background = new double[sampleDimensions.length]; for (int i=0; itransform. * @throws FactoryException if transform is not separable. */ private static MathTransform2D getMathTransform2D(MathTransform transform, final MathTransformFactory mtFactory) throws FactoryException { final IntegerSequence inputDimensions = JAIUtilities.createSequence(0, 1); final IntegerSequence outputDimensions = new IntegerSequence(); transform = mtFactory.createSubTransform(transform, inputDimensions, outputDimensions); if (JAIUtilities.containsAll(outputDimensions, 0, 2)) { transform = mtFactory.createFilterTransform(transform, inputDimensions); return (MathTransform2D) transform; } throw new FactoryException(Errors.format(ErrorKeys.NO_TRANSFORM2D_AVAILABLE)); } /** * Check if two geometries are equal, ignoring unspecified fields. If one or both geometries * has no "gridToCoordinateSystem" transform, then this properties is not taken in account. * Same apply for the grid range. * * * @param range1 The first range. * @param range2 The second range. * @return true if the two geometries are equal, ignoring unspecified fields. */ private static boolean equivalent(final GridGeometry geom1, final GridGeometry geom2) { if (geom1.equals(geom2)) { return true; } try { if (!geom1.getGridRange().equals(geom2.getGridRange())) { return false; } } catch (InvalidGridGeometryException exception) { // One geometry doesn't have a grid range. // Do not compare this properties. } try { if (!geom1.getGridToCoordinateSystem().equals(geom2.getGridToCoordinateSystem())) { return false; } } catch (InvalidGridGeometryException exception) { // One geometry doesn't have a transform. // Do not compare this properties. } return true; } /** * Log a message. */ private static void log(final LogRecord record) { record.setSourceClassName("GridCoverageProcessor"); record.setSourceMethodName("doOperation(\"Resample\")"); LOGGER.log(record); } /** * Returns the coverage name, localized for the supplied locale. * Default implementation fallback to the first source coverage. */ public String getName(final Locale locale) { final GridCoverage[] sources = getSources(); if (sources!=null && sources.length!=0) { return sources[0].getName(locale); } return super.getName(locale); } /** * The "Resample" operation. See package description for more details. * * @version $Id$ * @author Martin Desruisseaux */ static final class Operation extends org.geotools.gp.Operation { /** * Construct a "Resample" operation. */ public Operation() { super("Resample", new ParameterListDescriptorImpl( null, // the object to be reflected upon for enumerated values. new String[] // the names of each parameter. { "Source", "InterpolationType", "CoordinateSystem", "GridGeometry" }, new Class[] // the class of each parameter. { GridCoverage.class, Object.class, CoordinateSystem.class, GridGeometry.class }, new Object[] // The default values for each parameter. { ParameterListDescriptor.NO_PARAMETER_DEFAULT, "NearestNeighbor", null, // Same as source grid coverage null // Automatic }, null // Defines the valid values for each parameter. )); } /** * Resample a grid coverage. This method is invoked by * {@link GridCoverageProcessor} for the "Resample" operation. */ protected GridCoverage doOperation(final ParameterList parameters, final RenderingHints hints) { GridCoverage source = (GridCoverage) parameters.getObjectParameter("Source"); Interpolation interp = toInterpolation (parameters.getObjectParameter("InterpolationType")); CoordinateSystem cs = (CoordinateSystem) parameters.getObjectParameter("CoordinateSystem"); GridGeometry gridGeom = (GridGeometry) parameters.getObjectParameter("GridGeometry"); GridCoverage coverage; // The result to be computed below. if (cs == null) { cs = source.getCoordinateSystem(); } try { coverage = reproject(source, cs, gridGeom, interp, hints); } catch (FactoryException exception) { throw new CannotReprojectException(Errors.format( ErrorKeys.CANT_REPROJECT_$1, source.getName(null)), exception); } catch (TransformException exception) { throw new CannotReprojectException(Errors.format( ErrorKeys.CANT_REPROJECT_$1, source.getName(null)), exception); } /* * Check if we have been able to respect the user request. We may have failed to * respect the user specified grid geometry because JAI may not have honored the * 'minX', 'minY', 'width' and 'height' image layout setting (see documentation * for AffineDescriptor). Of course, we may have failed because of a bug in our * implementation as well. */ if (gridGeom != null) { boolean mismatche = false; final GridGeometry actualGeom = coverage.getGridGeometry(); if (LegacyGCSUtilities.hasGridRange(gridGeom)) { mismatche |= !gridGeom.getGridRange().equals( actualGeom.getGridRange()); } if (LegacyGCSUtilities.hasTransform(gridGeom)) { mismatche |= !gridGeom.getGridToCoordinateSystem().equals( actualGeom.getGridToCoordinateSystem()); } } return coverage; } } }