/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2015, 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.renderer.lite.gridcoverage2d; import java.awt.Color; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.awt.image.RenderedImage; import java.util.Collections; import java.util.Comparator; import java.util.List; import javax.media.jai.Interpolation; 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.processing.CoverageProcessor; import org.geotools.coverage.processing.operation.Crop; import org.geotools.coverage.processing.operation.Mosaic; import org.geotools.coverage.processing.operation.Resample; import org.geotools.factory.Hints; import org.geotools.geometry.Envelope2D; import org.geotools.geometry.GeneralEnvelope; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.geotools.renderer.i18n.ErrorKeys; import org.geotools.renderer.i18n.Errors; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import com.sun.media.jai.util.Rational; /** * @author Simone Giannecchini, GeoSolutions * */ final class GridCoverageRendererUtilities { private static final CoverageProcessor processor = CoverageProcessor.getInstance(new Hints(Hints.LENIENT_DATUM_SHIFT, Boolean.TRUE)); static { // /////////////////////////////////////////////////////////////////// // // Caching parameters for performing the various operations. // // /////////////////////////////////////////////////////////////////// } // FORMULAE FOR FORWARD MAP are derived as follows // Nearest // Minimum: // srcMin = floor ((dstMin + 0.5 - trans) / scale) // srcMin <= (dstMin + 0.5 - trans) / scale < srcMin + 1 // srcMin*scale <= dstMin + 0.5 - trans < (srcMin + 1)*scale // srcMin*scale - 0.5 + trans // <= dstMin < (srcMin + 1)*scale - 0.5 + trans // Let A = srcMin*scale - 0.5 + trans, // Let B = (srcMin + 1)*scale - 0.5 + trans // // dstMin = ceil(A) // // Maximum: // Note that srcMax is defined to be srcMin + dimension - 1 // srcMax = floor ((dstMax + 0.5 - trans) / scale) // srcMax <= (dstMax + 0.5 - trans) / scale < srcMax + 1 // srcMax*scale <= dstMax + 0.5 - trans < (srcMax + 1)*scale // srcMax*scale - 0.5 + trans // <= dstMax < (srcMax+1) * scale - 0.5 + trans // Let float A = (srcMax + 1) * scale - 0.5 + trans // // dstMax = floor(A), if floor(A) < A, else // dstMax = floor(A) - 1 // OR dstMax = ceil(A - 1) // // Other interpolations // // First the source should be shrunk by the padding that is // required for the particular interpolation. Then the // shrunk source should be forward mapped as follows: // // Minimum: // srcMin = floor (((dstMin + 0.5 - trans)/scale) - 0.5) // srcMin <= ((dstMin + 0.5 - trans)/scale) - 0.5 < srcMin+1 // (srcMin+0.5)*scale <= dstMin+0.5-trans < // (srcMin+1.5)*scale // (srcMin+0.5)*scale - 0.5 + trans // <= dstMin < (srcMin+1.5)*scale - 0.5 + trans // Let A = (srcMin+0.5)*scale - 0.5 + trans, // Let B = (srcMin+1.5)*scale - 0.5 + trans // // dstMin = ceil(A) // // Maximum: // srcMax is defined as srcMin + dimension - 1 // srcMax = floor (((dstMax + 0.5 - trans) / scale) - 0.5) // srcMax <= ((dstMax + 0.5 - trans)/scale) - 0.5 < srcMax+1 // (srcMax+0.5)*scale <= dstMax + 0.5 - trans < // (srcMax+1.5)*scale // (srcMax+0.5)*scale - 0.5 + trans // <= dstMax < (srcMax+1.5)*scale - 0.5 + trans // Let float A = (srcMax+1.5)*scale - 0.5 + trans // // dstMax = floor(A), if floor(A) < A, else // dstMax = floor(A) - 1 // OR dstMax = ceil(A - 1) // private static float RATIONAL_TOLERANCE = 0.000001F; /** * Checks whether the provided object is null or not. If it is null it * throws an {@link IllegalArgumentException} exception. * * @param source * the object to check. * @param node * the operation we are trying to run. */ static void ensureNotNull(final Object source, final String name) { if (source == null) throw new IllegalArgumentException(Errors.format( ErrorKeys.NULL_ARGUMENT_$1, name)); } /** * Checks whether the provided source object is null or not. If it is null * it throws an {@link IllegalArgumentException} exception. * * @param source * the object to check. * @param node * the operation we are trying to run. */ static void ensureSourceNotNull(final Object source, final String name) { if (source == null) throw new IllegalArgumentException(Errors.format( ErrorKeys.SOURCE_CANT_BE_NULL_$1, name)); } static Rectangle2D layoutHelper(RenderedImage source, float scaleX, float scaleY, float transX, float transY, Interpolation interp) { // Represent the scale factors as Rational numbers. // Since a value of 1.2 is represented as 1.200001 which // throws the forward/backward mapping in certain situations. // Convert the scale and translation factors to Rational numbers Rational scaleXRational = Rational.approximate(scaleX,RATIONAL_TOLERANCE); Rational scaleYRational = Rational.approximate(scaleY,RATIONAL_TOLERANCE); long scaleXRationalNum = scaleXRational.num; long scaleXRationalDenom = scaleXRational.denom; long scaleYRationalNum = scaleYRational.num; long scaleYRationalDenom = scaleYRational.denom; Rational transXRational = Rational.approximate(transX,RATIONAL_TOLERANCE); Rational transYRational = Rational.approximate(transY,RATIONAL_TOLERANCE); long transXRationalNum = transXRational.num; long transXRationalDenom = transXRational.denom; long transYRationalNum = transYRational.num; long transYRationalDenom = transYRational.denom; int x0 = source.getMinX(); int y0 = source.getMinY(); int w = source.getWidth(); int h = source.getHeight(); // Variables to store the calculated destination upper left coordinate long dx0Num, dx0Denom, dy0Num, dy0Denom; // Variables to store the calculated destination bottom right // coordinate long dx1Num, dx1Denom, dy1Num, dy1Denom; // Start calculations for destination dx0Num = x0; dx0Denom = 1; dy0Num = y0; dy0Denom = 1; // Formula requires srcMaxX + 1 = (x0 + w - 1) + 1 = x0 + w dx1Num = x0 + w; dx1Denom = 1; // Formula requires srcMaxY + 1 = (y0 + h - 1) + 1 = y0 + h dy1Num = y0 + h; dy1Denom = 1; dx0Num *= scaleXRationalNum; dx0Denom *= scaleXRationalDenom; dy0Num *= scaleYRationalNum; dy0Denom *= scaleYRationalDenom; dx1Num *= scaleXRationalNum; dx1Denom *= scaleXRationalDenom; dy1Num *= scaleYRationalNum; dy1Denom *= scaleYRationalDenom; // Equivalent to subtracting 0.5 dx0Num = 2 * dx0Num - dx0Denom; dx0Denom *= 2; dy0Num = 2 * dy0Num - dy0Denom; dy0Denom *= 2; // Equivalent to subtracting 1.5 dx1Num = 2 * dx1Num - 3 * dx1Denom; dx1Denom *= 2; dy1Num = 2 * dy1Num - 3 * dy1Denom; dy1Denom *= 2; // Adding translation factors // Equivalent to float dx0 += transX dx0Num = dx0Num * transXRationalDenom + transXRationalNum * dx0Denom; dx0Denom *= transXRationalDenom; // Equivalent to float dy0 += transY dy0Num = dy0Num * transYRationalDenom + transYRationalNum * dy0Denom; dy0Denom *= transYRationalDenom; // Equivalent to float dx1 += transX dx1Num = dx1Num * transXRationalDenom + transXRationalNum * dx1Denom; dx1Denom *= transXRationalDenom; // Equivalent to float dy1 += transY dy1Num = dy1Num * transYRationalDenom + transYRationalNum * dy1Denom; dy1Denom *= transYRationalDenom; // Get the integral coordinates int l_x0, l_y0, l_x1, l_y1; l_x0 = Rational.ceil(dx0Num, dx0Denom); l_y0 = Rational.ceil(dy0Num, dy0Denom); l_x1 = Rational.ceil(dx1Num, dx1Denom); l_y1 = Rational.ceil(dy1Num, dy1Denom); // Set the top left coordinate of the destination final Rectangle2D retValue= new Rectangle2D.Double(); retValue.setFrame(l_x0, l_y0, l_x1 - l_x0 + 1, l_y1 - l_y0 + 1); return retValue; } /** * Reprojecting the input coverage using the provided parameters. * * @param gc * @param crs * @param interpolation * @return * @throws FactoryException */ static GridCoverage2D reproject( final GridCoverage2D gc, CoordinateReferenceSystem crs, final Interpolation interpolation, final GeneralEnvelope destinationEnvelope, final double bkgValues[], final Hints hints) throws FactoryException { // paranoiac check assert CRS.equalsIgnoreMetadata(destinationEnvelope.getCoordinateReferenceSystem(), crs) || CRS.findMathTransform(destinationEnvelope.getCoordinateReferenceSystem(), crs) .isIdentity(); final ParameterValueGroup param = processor.getOperation("Resample").getParameters().clone(); param.parameter("source").setValue(gc); param.parameter("CoordinateReferenceSystem").setValue(crs); param.parameter("InterpolationType").setValue(interpolation); if(bkgValues!=null){ param.parameter("BackgroundValues").setValue(bkgValues); } return (GridCoverage2D) ((Resample)processor.getOperation("Resample")).doOperation(param, hints); } /** * Cropping the provided coverage to the requested geographic area. * * @param gc * @param envelope * @param crs * @return */ static GridCoverage2D crop( GridCoverage2D gc, GeneralEnvelope envelope, double[] background, final Hints hints) { final GeneralEnvelope oldEnvelope = (GeneralEnvelope) gc.getEnvelope(); // intersect the envelopes in order to prepare for cropping the coverage // down to the neded resolution final GeneralEnvelope intersectionEnvelope = new GeneralEnvelope(envelope); intersectionEnvelope.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem()); intersectionEnvelope.intersect(oldEnvelope); // Do we have something to show? After the crop I could get a null // coverage which would mean nothing to show. if (intersectionEnvelope.isEmpty()){ return null; } // crop final ParameterValueGroup param = processor.getOperation("CoverageCrop").getParameters().clone(); param.parameter("source").setValue(gc); param.parameter("Envelope").setValue(intersectionEnvelope); return (GridCoverage2D) ((Crop)processor.getOperation("CoverageCrop")).doOperation(param, hints); } static GridCoverage2D displace(GridCoverage2D coverage, double tx, double ty, GridCoverageFactory gridCoverageFactory) { // let's compute the new grid geometry GridGeometry2D originalGG = coverage.getGridGeometry(); GridEnvelope gridRange = originalGG.getGridRange(); Envelope2D envelope = originalGG.getEnvelope2D(); double minx = envelope.getMinX() + tx; double miny = envelope.getMinY() + ty; double maxx = envelope.getMaxX() + tx; double maxy = envelope.getMaxY() + ty; ReferencedEnvelope translatedEnvelope = new ReferencedEnvelope(minx, maxx, miny, maxy, envelope.getCoordinateReferenceSystem()); GridGeometry2D translatedGG = new GridGeometry2D(gridRange, translatedEnvelope); GridCoverage2D translatedCoverage = gridCoverageFactory.create(coverage.getName(), coverage.getRenderedImage(), translatedGG, coverage.getSampleDimensions(), new GridCoverage2D[] { coverage }, coverage.getProperties()); return translatedCoverage; } /** * Mosaicking the provided coverages to the requested geographic area. * @param alphas * @param background * * @param gc * @param envelope * @param crs * @return */ static GridCoverage2D mosaic(List<GridCoverage2D> coverages, List<GridCoverage2D> alphas, GeneralEnvelope renderingEnvelope, final Hints hints, double[] background) { Comparator<GridCoverage2D> c = new Comparator<GridCoverage2D>() { @Override public int compare(GridCoverage2D o1, GridCoverage2D o2) { double minx1 = o1.getEnvelope().getMinimum(0); double minx2 = o2.getEnvelope().getMinimum(0); if (minx1 == minx2) { double maxy1 = o1.getEnvelope().getMaximum(1); double maxy2 = o2.getEnvelope().getMaximum(1); return compareDoubles(maxy1, maxy2); } else { return compareDoubles(minx1, minx2); } } private int compareDoubles(double maxy1, double maxy2) { if (maxy1 == maxy2) { return 0; } else { return (int) Math.signum(maxy1 - maxy2); } } }; Collections.sort(coverages, c); Collections.sort(alphas, c); // setup the grid geometry try { // find the intersection between the target envelope and the coverages one ReferencedEnvelope targetEnvelope = ReferencedEnvelope.reference(renderingEnvelope); ReferencedEnvelope coveragesEnvelope = null; for (GridCoverage2D coverage : coverages) { ReferencedEnvelope re = ReferencedEnvelope.reference(coverage.getEnvelope2D()); if (coveragesEnvelope == null) { coveragesEnvelope = re; } else { coveragesEnvelope.expandToInclude(re); } } targetEnvelope = new ReferencedEnvelope(targetEnvelope.intersection(coveragesEnvelope), renderingEnvelope.getCoordinateReferenceSystem()); if (targetEnvelope.isEmpty() || targetEnvelope.isNull()) { return null; } MathTransform2D mt = coverages.get(0).getGridGeometry().getCRSToGrid2D(); Rectangle rasterSpaceEnvelope; rasterSpaceEnvelope = CRS.transform(mt, targetEnvelope).toRectangle2D().getBounds(); GridEnvelope2D gridRange = new GridEnvelope2D(rasterSpaceEnvelope); GridGeometry2D gridGeometry = new GridGeometry2D(gridRange, targetEnvelope); // mosaic final ParameterValueGroup param = processor.getOperation("Mosaic").getParameters().clone(); param.parameter("sources").setValue(coverages); param.parameter("geometry").setValue(gridGeometry); if(background != null){ param.parameter(Mosaic.OUTNODATA_NAME).setValue(background); } if(!alphas.isEmpty()){ param.parameter(Mosaic.ALPHA_NAME).setValue(alphas); } return (GridCoverage2D) ((Mosaic)processor.getOperation("Mosaic")).doOperation(param, hints); } catch (Exception e) { throw new RuntimeException("Failed to mosaic the input coverages", e); } } /** * @param inputEnvelope * @return * @throws Exception */ static GeneralEnvelope reprojectEnvelope(GeneralEnvelope inputEnvelope, final CoordinateReferenceSystem outputCRS) throws Exception { GeneralEnvelope outputEnvelope=null;; CoordinateReferenceSystem inputCRS=inputEnvelope.getCoordinateReferenceSystem(); if (!CRS.equalsIgnoreMetadata(inputCRS, outputCRS)) { outputEnvelope = CRS.transform(inputEnvelope, outputCRS); outputEnvelope.setCoordinateReferenceSystem(outputCRS); } // simple copy if(outputEnvelope==null){ outputEnvelope = new GeneralEnvelope(inputEnvelope); outputEnvelope.setCoordinateReferenceSystem(inputCRS); } return null; } /** * @param inputEnvelope * @param targetCRS * @return * @throws Exception */ static GeneralEnvelope reprojectEnvelopeWithWGS84Pivot(GeneralEnvelope inputEnvelope, CoordinateReferenceSystem targetCRS) throws Exception { GridCoverageRendererUtilities.ensureNotNull(inputEnvelope, "destinationEnvelope"); GridCoverageRendererUtilities.ensureNotNull(targetCRS, "coverageCRS"); final CoordinateReferenceSystem destinationCRS=inputEnvelope.getCoordinateReferenceSystem(); // // // // Try to convert the destination envelope in the source crs. If // this fails we pass through WGS84 as an intermediate step // // // try { // convert the destination envelope to the source coverage // native crs in order to try and crop it. If we get an error we // try to // do this in two steps using WGS84 as a pivot. This introduces // some erros (it usually // increases the envelope we want to check) but it is still // useful. CoordinateOperation operation = CRS.getCoordinateOperationFactory(true) .createOperation(destinationCRS, targetCRS); GeneralEnvelope output = CRS.transform(operation, inputEnvelope); output.setCoordinateReferenceSystem(targetCRS); return output; } catch (TransformException te) { // // // // Convert the destination envelope to WGS84 if needed for safer // comparisons later on with the original crs of this coverage. // // // final GeneralEnvelope destinationEnvelopeWGS84=GridCoverageRendererUtilities.reprojectEnvelope(inputEnvelope,DefaultGeographicCRS.WGS84); // // // // Convert the destination envelope from WGS84 to the source crs // for cropping the provided coverage. // // // return GridCoverageRendererUtilities.reprojectEnvelope(destinationEnvelopeWGS84,targetCRS); } } /** * @param color * @return */ public static double[] colorToArray(Color color) { if(color==null){ return null; } return new double[]{ color.getRed(), color.getGreen(), color.getBlue() }; } }