/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001 - 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.resources.coverage;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.DataBuffer;
import java.awt.image.RenderedImage;
import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import javax.imageio.ImageReadParam;
import javax.media.jai.PropertySource;
import javax.media.jai.ROI;
import org.apache.commons.io.IOUtils;
import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridGeometry2D;
import org.geotools.geometry.Envelope2D;
import org.geotools.metadata.iso.spatial.PixelTranslation;
import org.geotools.referencing.CRS;
import org.geotools.referencing.operation.matrix.XAffineTransform;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.resources.CRSUtilities;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.Utilities;
import org.opengis.coverage.Coverage;
import org.opengis.coverage.grid.GridCoverage;
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.TransformException;
import org.opengis.util.InternationalString;
import it.geosolutions.jaiext.range.NoDataContainer;
import it.geosolutions.jaiext.range.Range;
/**
* A set of utilities methods for the Grid Coverage package. Those methods are not really
* rigorous; must of them should be seen as temporary implementations.
*
* @since 2.4
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
* @author Simone Giannecchini, GeoSolutions
*/
public final class CoverageUtilities {
/**
* Public name for standard No Data category.
*/
public static final InternationalString NODATA=Vocabulary.formatInternational(VocabularyKeys.NODATA);
/**
* Axes transposition for swapping Lat and Lon axes.
*/
public static final AffineTransform AXES_SWAP= new AffineTransform2D(0,1,1,0,0,0);
/** Identity affine transformation.*/
public static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform2D(AffineTransform.getRotateInstance(0));
/**
* {@link AffineTransform} that can be used to go from an image datum placed
* at the center of pixels to one that is placed at ULC.
*/
public final static AffineTransform CENTER_TO_CORNER = AffineTransform
.getTranslateInstance(PixelTranslation
.getPixelTranslation(PixelInCell.CELL_CORNER),
PixelTranslation
.getPixelTranslation(PixelInCell.CELL_CORNER));
/**
* {@link AffineTransform} that can be used to go from an image datum placed
* at the ULC corner of pixels to one that is placed at center.
*/
public final static AffineTransform CORNER_TO_CENTER = AffineTransform
.getTranslateInstance(-PixelTranslation
.getPixelTranslation(PixelInCell.CELL_CORNER),
-PixelTranslation
.getPixelTranslation(PixelInCell.CELL_CORNER));
public static final double AFFINE_IDENTITY_EPS = 1E-6;
/**
* Do not allows instantiation of this class.
*/
private CoverageUtilities() {
}
/**
* Returns a two-dimensional CRS for the given coverage. This method performs a
* <cite>best effort</cite>; the returned CRS is not garanteed to be the most
* appropriate one.
*
* @param coverage The coverage for which to obtains a two-dimensional CRS.
* @return The two-dimensional CRS.
* @throws TransformException if the CRS can't be reduced to two dimensions.
*/
public static CoordinateReferenceSystem getCRS2D(final Coverage coverage)
throws TransformException
{
if (coverage instanceof GridCoverage2D) {
return ((GridCoverage2D) coverage).getCoordinateReferenceSystem2D();
}
if (coverage instanceof GridCoverage) {
final GridGeometry2D geometry =
GridGeometry2D.wrap(((GridCoverage) coverage).getGridGeometry());
if (geometry.isDefined(GridGeometry2D.CRS_BITMASK)) {
return geometry.getCoordinateReferenceSystem2D();
} else try {
return geometry.reduce(coverage.getCoordinateReferenceSystem());
} catch (FactoryException exception) {
// Ignore; we will fallback on the code below.
}
}
return CRSUtilities.getCRS2D(coverage.getCoordinateReferenceSystem());
}
/**
* Returns a two-dimensional horizontal CRS for the given coverage. This method performs a
* <cite>best effort</cite>; the returned CRS is not garanteed to succed.
*
* @param coverage The coverage for which to obtains a two-dimensional horizontal CRS.
* @return The two-dimensional horizontal CRS.
* @throws TransformException if the CRS can't be reduced to two dimensions.
*/
public static CoordinateReferenceSystem getHorizontalCRS(final Coverage coverage)
throws TransformException
{
CoordinateReferenceSystem returnedCRS=null;
if (coverage instanceof GridCoverage2D) {
returnedCRS= ((GridCoverage2D) coverage).getCoordinateReferenceSystem2D();
}
if (coverage instanceof GridCoverage) {
final GridGeometry2D geometry =
GridGeometry2D.wrap(((GridCoverage) coverage).getGridGeometry());
if (geometry.isDefined(GridGeometry2D.CRS_BITMASK)) {
returnedCRS= geometry.getCoordinateReferenceSystem2D();
} else try {
returnedCRS= geometry.reduce(coverage.getCoordinateReferenceSystem());
} catch (FactoryException exception) {
// Ignore; we will fallback on the code below.
}
}
if(returnedCRS==null)
returnedCRS= CRS.getHorizontalCRS(coverage.getCoordinateReferenceSystem());
if(returnedCRS==null)
throw new TransformException(Errors.format(
ErrorKeys.CANT_REDUCE_TO_TWO_DIMENSIONS_$1,
returnedCRS));
return returnedCRS;
}
/**
* Returns a two-dimensional envelope for the given coverage. This method performs a
* <cite>best effort</cite>; the returned envelope is not garanteed to be the most
* appropriate one.
*
* @param coverage The coverage for which to obtains a two-dimensional envelope.
* @return The two-dimensional envelope.
* @throws MismatchedDimensionException if the envelope can't be reduced to two dimensions.
*/
public static Envelope2D getEnvelope2D(final Coverage coverage)
throws MismatchedDimensionException
{
if (coverage instanceof GridCoverage2D) {
return ((GridCoverage2D) coverage).getEnvelope2D();
}
if (coverage instanceof GridCoverage) {
final GridGeometry2D geometry =
GridGeometry2D.wrap(((GridCoverage) coverage).getGridGeometry());
if (geometry.isDefined(GridGeometry2D.ENVELOPE_BITMASK)) {
return geometry.getEnvelope2D();
} else {
return geometry.reduce(coverage.getEnvelope());
}
}
// Following may thrown MismatchedDimensionException.
return new Envelope2D(coverage.getEnvelope());
}
/**
* Utility method for extracting NoData property from input {@link GridCoverage2D}.
*
* @param coverage
* @return A {@link NoDataContainer} object containing input NoData definition
*/
public static NoDataContainer getNoDataProperty(GridCoverage2D coverage) {
// Searching for NoData property
final Object noData = coverage.getProperty(NoDataContainer.GC_NODATA);
if (noData != null) {
// Returning a new instance of NoDataContainer
if (noData instanceof NoDataContainer) {
return (NoDataContainer) noData;
} else if (noData instanceof Double) {
return new NoDataContainer((Double) noData);
}
}
return null;
}
/**
* Utility method for extracting ROI property from input {@link GridCoverage2D}.
*
* @param coverage
* @return A {@link ROI} object
*/
public static ROI getROIProperty(GridCoverage2D coverage) {
// Searching for the ROI
final Object roi = coverage.getProperty("GC_ROI");
// Returning it if present
if (roi != null && roi instanceof ROI) {
return (ROI) roi;
}
return null;
}
/**
* Utility method for setting NoData to the input {@link Map}
*
* @param properties {@link Map} where the nodata will be set
* @param noData May be a {@link Range}, double[], double or {@link NoDataContainer}
*/
public static void setNoDataProperty(Map<String, Object> properties, Object noData) {
// If no nodata or no properties are defined, nothing is done
if (noData == null || properties == null) {
return;
}
// Creation of a new NoDataContainer instance and setting it inside the properties
if (noData instanceof Range) {
properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((Range) noData));
} else if (noData instanceof Double) {
properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((Double) noData));
} else if (noData instanceof double[]) {
properties.put(NoDataContainer.GC_NODATA, new NoDataContainer((double[]) noData));
} else if (noData instanceof NoDataContainer) {
properties
.put(NoDataContainer.GC_NODATA, new NoDataContainer((NoDataContainer) noData));
}
}
/**
* Utility method for setting ROI to the input {@link Map}
*
* @param properties {@link Map} where the ROI will be set
* @param roi {@link ROI} instance to set
*/
public static void setROIProperty(Map<String, Object> properties, ROI roi) {
// If no properties is defined, nothing is done
if (properties == null) {
return;
}
// If No ROI is defined we remove ROI property from the property map
if (roi == null) {
properties.remove("GC_ROI");
return;
}
// Otherwise ROI is set
properties.put("GC_ROI", roi);
}
/**
* Retrieves a best guess for the sample value to use for background,
* inspecting the categories of the provided {@link GridCoverage2D}.
*
* @param coverage to use for guessing background values.
* @return an array of double values to use as a background.
*/
public static double[] getBackgroundValues(GridCoverage2D coverage) {
//minimal checks
if(coverage==null){
throw new NullPointerException(Errors.format(ErrorKeys.NULL_PARAMETER_$2, "coverage","GridCoverage2D"));
}
// try to get the GC_NODATA double value from the coverage property
final Object noData=coverage.getProperty(NoDataContainer.GC_NODATA);
if(noData!=null&& noData instanceof NoDataContainer){
return ((NoDataContainer)noData).getAsArray();
//new double[]{((Double)noData).doubleValue()};
}
////
//
// Try to gather no data values from the sample dimensions
// and, if not available, we try to suggest them from the sample
// dimension type
//
////
final GridSampleDimension[] sampleDimensions = coverage.getSampleDimensions();
final double[] background = new double[sampleDimensions.length];
boolean found=false;
final int dataType = coverage.getRenderedImage().getSampleModel().getDataType();
for (int i=0; i<background.length; i++) {
// try to use the no data category if preset
final List<Category> categories = sampleDimensions[i].getCategories();
if(categories!=null&&categories.size()>0){
for(Category category:categories){
if(category.getName().equals(NODATA)){
background[i]=category.getRange().getMinimum();
found=true;
break;
}
}
}
if(!found){
// we don't have a proper no data value, let's try to suggest something
// meaningful fro mthe data type for this coverage
background[i]=suggestNoDataValue(dataType).doubleValue();
}
// SG 25112012, removed this automagic behavior
// final NumberRange<?> range = sampleDimensions[i].getBackground().getRange();
// final double min = range.getMinimum();
// final double max = range.getMaximum();
// if (range.isMinIncluded()) {
// background[i] = min;
// } else if (range.isMaxIncluded()) {
// background[i] = max;
// } else {
// background[i] = 0.5 * (min + max);
// }
}
return background;
}
/**
* Returns {@code true} if the specified grid coverage or any of its source
* uses the following image.
*/
public static boolean uses(final GridCoverage coverage, final RenderedImage image) {
if (coverage != null) {
if ( coverage.getRenderedImage() == image) {
return true;
}
final Collection<GridCoverage> sources = coverage.getSources();
if (sources != null) {
for (final GridCoverage source : sources) {
if (uses(source, image)) {
return true;
}
}
}
}
return false;
}
/**
* Returns the visible band in the specified {@link RenderedImage} or {@link PropertySource}.
* This method fetch the {@code "GC_VisibleBand"} property. If this property is undefined,
* then the visible band default to the first one.
*
* @param image The image for which to fetch the visible band, or {@code null}.
* @return The visible band.
*/
public static int getVisibleBand(final Object image) {
Object candidate = null;
if (image instanceof RenderedImage) {
candidate = ((RenderedImage) image).getProperty("GC_VisibleBand");
} else if (image instanceof PropertySource) {
candidate = ((PropertySource) image).getProperty("GC_VisibleBand");
}
if (candidate instanceof Integer) {
return ((Integer) candidate).intValue();
}
return 0;
}
/**
* Checks if the transformation is a pure scale/translate instance (using the
* provided tolerance factor)
*
* @param transform The {@link MathTransform} to check.
* @param EPS The tolerance factor.
* @return {@code true} if the provided transformation is a simple scale and translate,
* {@code false} otherwise.
*/
public static boolean isScaleTranslate(final MathTransform transform, final double EPS) {
if (!(transform instanceof AffineTransform)) {
return false;
}
final AffineTransform at = (AffineTransform) transform;
final double rotation = Math.abs(XAffineTransform.getRotation(at));
return rotation < EPS; // This is enough for returning 'false' if 'scale' is NaN.
}
/**
* Computes the resolutions for the provided "grid to world" transformation
* The returned resolution array is of length of 2.
*
* @param gridToCRS
* The grid to world transformation.
*/
public static double[] getResolution(final AffineTransform gridToCRS) {
double[] requestedRes = null;
if (gridToCRS != null) {
requestedRes = new double[2];
requestedRes[0] = XAffineTransform.getScaleX0(gridToCRS);
requestedRes[1] = XAffineTransform.getScaleY0(gridToCRS);
}
return requestedRes;
}
/**
* Tries to estimate if the supplied affine transform is either a scale and
* translate transform or if it contains a rotations which is an integer
* multiple of PI/2.
*
* @param gridToCRS an instance of {@link AffineTransform} to check against.
* @param EPS tolerance value for comparisons.
* @return {@code true} if this transform is "simple", {@code false} otherwise.
*/
public static boolean isSimpleGridToWorldTransform(final AffineTransform gridToCRS, double EPS) {
final double rotation = XAffineTransform.getRotation(gridToCRS);
// Checks if there is a valid rotation value (it could be 0). If the result is an integer,
// then there is no rotation and skew or there is a rotation multiple of PI/2. Note that
// there is no need to check explicitly for NaN rotation angle since such value will be
// propagated as NaN by every math functions used here, and (NaN < EPS) returns false.
final double quadrantRotation = Math.abs(rotation / (Math.PI/2));
return Math.abs(quadrantRotation - Math.floor(quadrantRotation)) < EPS;
}
/**
* Checks that the provided {@code dimensions} when intersected with the source region used by
* the provided {@link ImageReadParam} instance does not result in an empty {@link Rectangle}.
* Finally, in case the region intersection is not empty, set it as new source region for the
* provided {@link ImageReadParam}.
* <p>
* Input parameters cannot be null.
*
* @param readParameters an instance of {@link ImageReadParam} for which we want to check
* the source region element.
* @param dimensions an instance of {@link Rectangle} to use for the check.
* @return {@code true} if the intersection is not empty, {@code false} otherwise.
*/
public static boolean checkEmptySourceRegion(final ImageReadParam readParameters,
final Rectangle dimensions) {
Utilities.ensureNonNull("readDimension", dimensions);
Utilities.ensureNonNull("readP", readParameters);
final Rectangle sourceRegion = readParameters.getSourceRegion();
Rectangle.intersect(sourceRegion, dimensions, sourceRegion);
if (sourceRegion.isEmpty()) {
return true;
}
readParameters.setSourceRegion(sourceRegion);
return false;
}
/**
* Returns a suitable threshold depending on the {@link DataBuffer} type.
*
* <p>
* Remember that the threshold works with >=.
*
* @param dataType
* to create a low threshold for.
* @return a minimum threshold value suitable for this data type.
*/
public static double getMosaicThreshold(int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
// this may cause problems and truncations when the native mosaic
// operations is enabled
return 0.0;
case DataBuffer.TYPE_INT:
return Integer.MIN_VALUE;
case DataBuffer.TYPE_SHORT:
return Short.MIN_VALUE;
case DataBuffer.TYPE_DOUBLE:
return -Double.MAX_VALUE;
case DataBuffer.TYPE_FLOAT:
return -Float.MAX_VALUE;
}
return 0;
}
/**
* Returns a suitable no data value depending on the {@link DataBuffer} type.
*
* @param dataType
* to create a low threshold for.
* @return a no data value suitable for this data type.
*/
public static Number suggestNoDataValue(int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
return Byte.valueOf((byte)0);
case DataBuffer.TYPE_USHORT:
return Short.valueOf((short)0);
case DataBuffer.TYPE_INT:
return Integer.valueOf(Integer.MIN_VALUE);
case DataBuffer.TYPE_SHORT:
return Short.valueOf(Short.MIN_VALUE);
case DataBuffer.TYPE_DOUBLE:
return Double.valueOf(Double.NaN);
case DataBuffer.TYPE_FLOAT:
return Float.valueOf(Float.NaN);
default:
throw new IllegalAccessError(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,"dataType",dataType));
}
}
/**
* Unified Code for Units of Measure (UCUM)
*/
public static class UCUM {
/**
* An UCUM Unit instance simply made of name and symbol.
*/
public static class UCUMUnit {
private String name;
private String symbol;
public UCUMUnit(String name, String symbol) {
this.name = name;
this.symbol = symbol;
}
public String getName() {
return name;
}
public String getSymbol() {
return symbol;
}
}
/**
* Commonly used UCUM units. In case this set will grow too much, we may consider importing some UCUM specialized library.
*/
public final static UCUMUnit TIME_UNITS = new UCUMUnit("second", "s");
public final static UCUMUnit ELEVATION_UNITS = new UCUMUnit("meter", "m");
}
/**
* Extract Properties from a specified URL
*/
public static Properties loadPropertiesFromURL(URL propsURL) {
Utilities.ensureNonNull("propsURL", propsURL);
final Properties properties = new Properties();
InputStream stream = null;
InputStream openStream = null;
try {
openStream = propsURL.openStream();
stream = new BufferedInputStream(openStream);
properties.load(stream);
} catch (FileNotFoundException e) {
if (FeatureUtilities.LOGGER.isLoggable(Level.SEVERE))
FeatureUtilities.LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
} catch (IOException e) {
if (FeatureUtilities.LOGGER.isLoggable(Level.SEVERE))
FeatureUtilities.LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
} finally {
if (stream != null) {
IOUtils.closeQuietly(stream);
}
if (openStream != null) {
IOUtils.closeQuietly(openStream);
}
}
return properties;
}
}