/*
* 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.coverage.io.util;
import it.geosolutions.imageio.stream.input.FileImageInputStreamExt;
import it.geosolutions.imageio.stream.input.FileImageInputStreamExtImpl;
import it.geosolutions.imageio.stream.input.URIImageInputStream;
import it.geosolutions.imageio.stream.input.URIImageInputStreamImpl;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.renderable.ParameterBlock;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.measure.quantity.Quantity;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import org.geotools.coverage.Category;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GeneralGridEnvelope;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.coverage.grid.ViewType;
import org.geotools.coverage.grid.io.AbstractGridCoverage2DReader;
import org.geotools.coverage.grid.io.OverviewPolicy;
import org.geotools.data.DataSourceException;
import org.geotools.factory.Hints;
import org.geotools.feature.NameImpl;
import org.geotools.geometry.Envelope2D;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.imageio.metadata.Band;
import org.geotools.imageio.metadata.RectifiedGrid;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.referencing.CRS;
import org.geotools.referencing.NamedIdentifier;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.referencing.cs.DefaultCartesianCS;
import org.geotools.referencing.datum.DefaultEllipsoid;
import org.geotools.referencing.datum.DefaultGeodeticDatum;
import org.geotools.referencing.datum.DefaultPrimeMeridian;
import org.geotools.referencing.factory.ReferencingFactoryContainer;
import org.geotools.referencing.operation.DefaultMathTransformFactory;
import org.geotools.referencing.operation.builder.GridToEnvelopeMapper;
import org.geotools.referencing.operation.transform.ConcatenatedTransform;
import org.geotools.referencing.operation.transform.IdentityTransform;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.coverage.CoverageUtilities;
import org.geotools.resources.geometry.XRectangle2D;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.temporal.object.DefaultInstant;
import org.geotools.temporal.object.DefaultPeriod;
import org.geotools.util.NumberRange;
import org.opengis.coverage.grid.GridCoverage;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.TransformException;
import org.opengis.temporal.Instant;
import org.opengis.temporal.Period;
import org.opengis.temporal.TemporalGeometricPrimitive;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
*
*
* @author Daniele Romagnoli, GeoSolutions
*
*
* @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/build/maven/javadoc/../../../modules/unsupported/coverage-experiment/coverage-core/src/main/java/org/geotools/coverage/io/util/Utilities.java $
*/
public class Utilities {
private final static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(Utilities.class.toString());
/**
* TODO: Define a contains method which allows to know if the extent of a
* SliceDescriptor contains a predefined extent. This would be useful to
* know which SliceDescriptor to be used to access to a specific
* temporal/vertical extent defined slice.
*/
/** Caches a MathTransformFactory */
private final static MathTransformFactory mtFactory = new DefaultMathTransformFactory();
public static ReferenceIdentifier[] getIdentifiers(
final String nameIdentifier) {
if (nameIdentifier.equalsIgnoreCase("WGS84")) {
final ReferenceIdentifier[] identifiers = {
new NamedIdentifier(Citations.OGC, "WGS84"),
new NamedIdentifier(Citations.ORACLE, "WGS 84"),
new NamedIdentifier(null, "WGS_84"),
new NamedIdentifier(null, "WGS 1984"),
new NamedIdentifier(Citations.EPSG, "WGS_1984"),
new NamedIdentifier(Citations.ESRI, "D_WGS_1984"),
new NamedIdentifier(Citations.EPSG,
"World Geodetic System 1984") };
return identifiers;
}
// TODO: Handle mores
return null;
}
private Utilities() {
}
/**
* Build a {@link DefaultGeodeticDatum} given a set of parameters.
*
* @param name
* the datum name
* @param equatorialRadius
* the equatorial radius parameter
* @param inverseFlattening
* the inverse flattening parameter
* @param unit
* the UoM
* @return a properly built Datum.
*/
public static DefaultGeodeticDatum getDefaultGeodeticDatum(
final String name, final double equatorialRadius,
final double inverseFlattening, Unit unit) {
DefaultEllipsoid ellipsoid = DefaultEllipsoid.createFlattenedSphere(name, equatorialRadius, inverseFlattening, unit);
final ReferenceIdentifier[] identifiers = Utilities.getIdentifiers(name);
// TODO: Should I change this behavior?
if (identifiers == null)
throw new IllegalArgumentException("Reference Identifier not available");
final Map<String, Object> properties = new HashMap<String, Object>(4);
properties.put(DefaultGeodeticDatum.NAME_KEY, identifiers[0]);
properties.put(DefaultGeodeticDatum.ALIAS_KEY, identifiers);
DefaultGeodeticDatum datum = new DefaultGeodeticDatum(properties, ellipsoid, DefaultPrimeMeridian.GREENWICH);
return datum;
}
/**
* Temp utility method which allows to get the real file name from a custom
* input File, where "custom" means a file having a special name structured
* as "originalFileName:imageIndex"
*/
public static File getFileFromCustomInput(Object input) {
final File fileCheck = (File) input;
final String path = fileCheck.getAbsolutePath();
final int imageSpecifierIndex = path.lastIndexOf(":");
final File file;
if (imageSpecifierIndex > 1 && imageSpecifierIndex > path.indexOf(":")) {
file = new File(path.substring(0, imageSpecifierIndex));
} else
file = fileCheck;
return file;
}
/**
* Simple utility method which allows to build a Mercator2SP Projected CRS
* given the set of required parameters. It will be used by several Terascan
* products.
*/
@SuppressWarnings("deprecation")
public static CoordinateReferenceSystem getMercator2SPProjectedCRS(
final double standardParallel, final double centralMeridian,
final double natOriginLat, GeographicCRS sourceCRS, Hints hints)
throws DataSourceException {
CoordinateReferenceSystem projectedCRS = null;
// //
//
// Creating a proper projected CRS
//
// //
final ReferencingFactoryContainer fg = ReferencingFactoryContainer.instance(hints);
ParameterValueGroup params;
try {
params = mtFactory.getDefaultParameters("Mercator_2SP");
params.parameter("standard_parallel_1").setValue(standardParallel);
params.parameter("central_meridian").setValue(centralMeridian);
params.parameter("false_northing").setValue(0);
params.parameter("false_easting").setValue(0);
params.parameter("latitude_of_origin").setValue(natOriginLat);
// //
//
// Setting the CRS
//
// //
final Map<String, String> props = new HashMap<String, String>();
props.put("name", "Mercator CRS");
projectedCRS = fg.createProjectedCRS(props, sourceCRS, null, params, DefaultCartesianCS.PROJECTED);
} catch (FactoryException e) {
throw new DataSourceException(e);
}
return projectedCRS;
}
/**
* Build a base {@link GeographicCRS} given the parameters to specify a
* Geodetic Datum
*/
public static GeographicCRS getBaseCRS(final double equatorialRadius,
final double inverseFlattening) {
final DefaultGeodeticDatum datum = Utilities.getDefaultGeodeticDatum(
"WGS84", equatorialRadius, inverseFlattening, SI.METER);
final GeographicCRS sourceCRS = new DefaultGeographicCRS("WGS-84",
datum, DefaultGeographicCRS.WGS84.getCoordinateSystem());
return sourceCRS;
}
/**
* Simple method returning the value (as {@code String}) of the attribute
* with name {@code attributeName} from the input attributes map.
*
* @param attributes
* the attributes map
* @param attributeName
* the requested attribute
* @return the value of the requested attribute as a {@code String}.
* Returns {@code null} in case of no attribute found.
*
*/
public static String getAttributeValue(NamedNodeMap attributes,
String attributeName) {
String attributeValue = null;
Node attribute = attributes.getNamedItem(attributeName);
if (attribute != null) {
attributeValue = attribute.getNodeValue();
}
return attributeValue;
}
/**
* Check two {@link TemporalGeometricPrimitive} objects. Return {@code true} in case the first argument
* contains the second one. In case of instants, return {@code true} if they are equals.
* In case the first argument is a period and the second one is an instant, check if the instant is
* contained within the period. In case they are Periods, check for an intersection.
*
* @param containing
* @param contained
* @return
*/
public static boolean contains(TemporalGeometricPrimitive containing, TemporalGeometricPrimitive contained) {
// //
//
// Instants should match to be taken
//
// //
if (containing instanceof Instant && contained instanceof Instant)
return containing.equals(contained);
// //
//
// If the first time is a period I will check if the second time (an instant)
// is between the beginning and the ending of the period.
//
// //
else if (containing instanceof Period && contained instanceof Instant) {
final Date position = ((DefaultInstant) contained).getPosition()
.getDate();
final DefaultPeriod period = (DefaultPeriod) containing;
final Date startPeriod = period.getBeginning().getPosition()
.getDate();
final Date endPeriod = period.getEnding().getPosition().getDate();
if (position.compareTo(startPeriod) >= 0
&& position.compareTo(endPeriod) <= 0)
return true;
}
// //
//
// In case both times are periods, I check for an intersection.
//
// //
else if (containing instanceof Period && contained instanceof Period) {
final DefaultPeriod containingPeriod = (DefaultPeriod) containing;
final Date startContaining = containingPeriod.getBeginning().getPosition().getDate();
final Date endContaining = containingPeriod.getEnding().getPosition().getDate();
final DefaultPeriod containedPeriod = (DefaultPeriod) contained;
final Date startContained = containedPeriod.getBeginning().getPosition().getDate();
final Date endContained = containedPeriod.getEnding().getPosition().getDate();
// Return false if the period which should be contained is totally
// outside the containing one.
// Silly example: a period between 3AM and 5AM isn't contained in a
// period between 6AM and 8AM.
// Instead, a period between 5AM and 7AM should be considered
// contained within the period between 6AM and 8AM since part of
// them are intersecting.
if (endContained.compareTo(startContaining) < 0
|| startContained.compareTo(endContaining) > 0)
return false;
else
return true;
}
return false;
}
/**
* Return a {@link Unit} instance for the specified uom String.
* @param uom
* @return
*/
public static Unit<? extends Quantity> parseUnit(final String uom) {
Unit<? extends Quantity> unit = Unit.ONE;
if (uom != null && uom.trim().length() > 0) {
//TODO: Add more well known cases
if (uom.equalsIgnoreCase("temp_deg_c")|| uom.equalsIgnoreCase("Celsius"))
unit = javax.measure.unit.SI.CELSIUS;
else {
try {
unit = Unit.valueOf(uom);
} catch (IllegalArgumentException iae) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE,"Unable to parse the provided unit " + uom);
}
}
}
}
return unit;
}
/**
* Get a WGS84 envelope for the specified envelope. The get2D parameter
* allows to specify if we need the returned coverage as an
* {@code Envelope2D} or a more general {@code GeneralEnvelope} instance.
*
* @param envelope
* @param get2D
* if {@code true}, the requested envelope will be an
* instance of {@link Envelope2D}. If {@code false} it will
* be an instance of {@link GeneralEnvelope
* @return a WGS84 envelope as {@link Envelope2D} in case of request for a
* 2D WGS84 Envelope, or a {@link GeneralEnvelope} otherwise.
* @throws FactoryException
* @throws TransformException
*/
public static Envelope getEnvelopeAsWGS84(final Envelope envelope,
boolean get2D) throws FactoryException, TransformException {
if (envelope == null)
throw new IllegalArgumentException("Specified envelope is null");
Envelope requestedWGS84;
final MathTransform transformToWGS84;
final CoordinateReferenceSystem crs = envelope
.getCoordinateReferenceSystem();
// //
//
// get a math transform to go to WGS84
//
// //
if (!CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84)) {
transformToWGS84 = CRS.findMathTransform(crs,
DefaultGeographicCRS.WGS84, true);
} else {
transformToWGS84 = IdentityTransform.create(2);
}
// do we need to transform the requested envelope?
if (!transformToWGS84.isIdentity()) {
GeneralEnvelope env = CRS.transform(transformToWGS84, envelope);
if (get2D) {
requestedWGS84 = new Envelope2D(env);
((Envelope2D) requestedWGS84)
.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
} else {
requestedWGS84 = env;
((GeneralEnvelope) requestedWGS84)
.setCoordinateReferenceSystem(DefaultGeographicCRS.WGS84);
}
return requestedWGS84;
} else {
if (get2D)
return new Envelope2D(envelope);
else
return new GeneralEnvelope(envelope);
}
}
/**
* Return a 2D version of a requestedEnvelope
*
* @param requestedEnvelope
* the {@code GeneralEnvelope} to be returned as 2D.
* @return the 2D requested envelope
* @throws FactoryException
* @throws TransformException
*/
public static GeneralEnvelope getRequestedEnvelope2D(GeneralEnvelope requestedEnvelope) throws FactoryException,
TransformException {
if (requestedEnvelope == null)
throw new IllegalArgumentException("requested envelope is null");
GeneralEnvelope requestedEnvelope2D = null;
final MathTransform transformTo2D;
CoordinateReferenceSystem requestedEnvelopeCRS2D = requestedEnvelope.getCoordinateReferenceSystem();
// //
//
// Find the transformation to 2D
//
// //
if (requestedEnvelopeCRS2D.getCoordinateSystem().getDimension() != 2) {
transformTo2D = CRS.findMathTransform(requestedEnvelopeCRS2D, CRS
.getHorizontalCRS(requestedEnvelopeCRS2D));
requestedEnvelopeCRS2D = CRS.getHorizontalCRS(requestedEnvelopeCRS2D);
} else
transformTo2D = IdentityTransform.create(2);
if (!transformTo2D.isIdentity()) {
requestedEnvelope2D = CRS.transform(transformTo2D,requestedEnvelope);
requestedEnvelope2D.setCoordinateReferenceSystem(requestedEnvelopeCRS2D);
} else
requestedEnvelope2D = new GeneralEnvelope(requestedEnvelope);
assert requestedEnvelopeCRS2D.getCoordinateSystem().getDimension() == 2;
return requestedEnvelope2D;
}
/**
* Return a crop region from a specified envelope, leveraging on a grid to
* world transformation.
*
* @param envelope
* the crop envelope
* @param gridToWorldTransform
* the grid2world transformation
* @return a {@code Rectangle} representing the crop region.
* @throws TransformException
* in case a problem occurs when going back to raster space.
*/
public static Rectangle getCropRegion(GeneralEnvelope envelope,
final MathTransform gridToWorldTransform) throws TransformException {
if (envelope == null || gridToWorldTransform == null) {
boolean isEnvelope = envelope == null;
boolean isG2W = gridToWorldTransform == null;
boolean twoErrors = isEnvelope && isG2W;
final StringBuilder errorMessage = new StringBuilder();
errorMessage.append("Specified ").append(
isEnvelope ? "envelope" : "").append(twoErrors ? ", " : "")
.append(isG2W ? "grid to world transformation " : "")
.append("is null");
throw new IllegalArgumentException(errorMessage.toString());
}
final MathTransform worldToGridTransform = gridToWorldTransform.inverse();
final GeneralEnvelope rasterArea = CRS.transform(worldToGridTransform,envelope);
final Rectangle2D ordinates = rasterArea.toRectangle2D();
return ordinates.getBounds();
}
/**
* Returns the intersection between the base envelope and the requested
* envelope.
*
* @param baseEnvelope2D
* the base envelope.
*
* @param requestedEnvelope2D
* the requested 2D envelope to be intersected with the base
* envelope.
* @param requestedDim
* is the requested region where to load data of the
* specified envelope.
* @param readGridToWorld
* the Grid to world transformation to be used in read
* @param wgs84BaseEnvelope2D
* a WGS84 version of the baseEnvelope to be used to try
* finding an intersection in wgs84 in case it is impossible
* to compute an intersection of the base envelope with the
* specified requested envelope.
* @return the resulting intersection of envelopes. In case of empty
* intersection, this method is allowed to return {@code null}
* @throws TransformException
* @throws FactoryException
* @todo TODO XXX refactor this method leveraging on the
* coverageSourceCapabilities of reprojection. Moreover add a boolean
* parameter saying if trying to reproject to WGS84 always need to be
* done
*/
public static GeneralEnvelope getIntersection(
final Envelope2D baseEnvelope2D,
final CoordinateReferenceSystem spatialReferenceSystem2D,
GeneralEnvelope requestedEnvelope2D, Rectangle requestedDim,
MathTransform2D readGridToWorld,
final Envelope2D wgs84BaseEnvelope2D) throws TransformException,
FactoryException {
if (baseEnvelope2D == null || spatialReferenceSystem2D == null
|| requestedEnvelope2D == null || requestedDim == null
|| readGridToWorld == null) {
StringBuilder sb = new StringBuilder("Some of the specified parameters are null:")
.append(baseEnvelope2D == null ? "base envelope \n" : "")
.append(spatialReferenceSystem2D == null ? "native spatial reference system\n": "")
.append(requestedEnvelope2D == null ? "requested envelope \n" : "")
.append(requestedDim == null ? "requested dim\n" : "")
.append(readGridToWorld == null ? "requested grid to world transformation \n": "");
throw new IllegalArgumentException(sb.toString());
}
GeneralEnvelope adjustedRequestedEnvelope = new GeneralEnvelope(2);
final CoordinateReferenceSystem requestedEnvelopeCRS2D = requestedEnvelope2D.getCoordinateReferenceSystem();
boolean tryWithWGS84 = false;
try {
// convert the requested envelope 2D to this coverage native crs.
MathTransform transform = null;
if (!CRS.equalsIgnoreMetadata(requestedEnvelopeCRS2D, spatialReferenceSystem2D))
transform = CRS.findMathTransform(requestedEnvelopeCRS2D, spatialReferenceSystem2D, true);
// now transform the requested envelope to source crs
if (transform != null && !transform.isIdentity())
adjustedRequestedEnvelope = CRS.transform(transform, requestedEnvelope2D);
else
adjustedRequestedEnvelope.setEnvelope(requestedEnvelope2D);
// intersect the requested area with the bounds of this
// layer in native crs
if (!adjustedRequestedEnvelope.intersects(baseEnvelope2D, true))
return null;
adjustedRequestedEnvelope.intersect(baseEnvelope2D);
adjustedRequestedEnvelope.setCoordinateReferenceSystem(spatialReferenceSystem2D);
// //
//
// transform the intersection envelope from the destination world
// space to the requested raster space
//
// //
final Envelope requestedEnvelopeCropped = (transform != null && !transform.isIdentity()) ?
CRS.transform(transform.inverse(), adjustedRequestedEnvelope) : adjustedRequestedEnvelope;
final Rectangle2D ordinates = CRS.transform(readGridToWorld.inverse(), requestedEnvelopeCropped).toRectangle2D();
final GeneralGridEnvelope finalRange = new GeneralGridEnvelope(ordinates.getBounds());
final Rectangle tempRect = finalRange.toRectangle();
// check that we stay inside the source rectangle
XRectangle2D.intersect(tempRect, requestedDim, tempRect);
requestedDim.setRect(tempRect);
} catch (TransformException te) {
// something bad happened while trying to transform this
// envelope. let's try with wgs84
tryWithWGS84 = true;
} catch (FactoryException fe) {
// something bad happened while trying to transform this
// envelope. let's try with wgs84
tryWithWGS84 = true;
}
// //
//
// If this does not work, we go back to reproject in the wgs84
// requested envelope
//
// //
if (tryWithWGS84) {
final GeneralEnvelope requestedEnvelopeWGS84 = (GeneralEnvelope) getEnvelopeAsWGS84(requestedEnvelope2D, false);
// checking the intersection in wgs84
if (!requestedEnvelopeWGS84.intersects(wgs84BaseEnvelope2D, true))
return null;
// intersect
adjustedRequestedEnvelope = new GeneralEnvelope(requestedEnvelopeWGS84);
adjustedRequestedEnvelope.intersect(wgs84BaseEnvelope2D);
adjustedRequestedEnvelope = CRS.transform(CRS.findMathTransform(requestedEnvelopeWGS84.getCoordinateReferenceSystem(),
spatialReferenceSystem2D, true), adjustedRequestedEnvelope);
adjustedRequestedEnvelope.setCoordinateReferenceSystem(spatialReferenceSystem2D);
}
return adjustedRequestedEnvelope;
}
public static AffineTransform getAffineTransform(RectifiedGrid rg) {
AffineTransform at = new AffineTransform();
if (rg == null)
throw new IllegalArgumentException("unable to find rectified grid from metadata");
int dimension = rg.getDimension();
int offset = 0;
// if (dimension == 3) {
// offset = 1;
// dimension = 2;
// }
double[][] ov = new double[dimension][];
double[] origin = rg.getCoordinates();
for (int i = 0; i < dimension; i++) {
ov[i] = rg.getOffsetVectorValues(i + offset);
}
at.setTransform(ov[0][0 + offset], 0, 0, ov[1][1 + offset],
origin[0 + offset], origin[1 + offset]);
return at;
}
public static GridEnvelope getGridRange(RectifiedGrid rg) {
if (rg == null)
throw new IllegalArgumentException(
"unable to find rectified grid from metadata");
// int dimension = rg.getDimension();
int offset = 0;
// if (dimension == 3) {
// offset = 1;
// dimension = 2;
// }
int[] high = rg.getHigh();
int[] low = rg.getLow();
final GridEnvelope gridRange = new GeneralGridEnvelope(new int[] {
low[0 + offset], low[1 + offset] }, new int[] {
high[0 + offset], high[1 + offset] }, false);
return gridRange;
}
/**
* Retrieves the original grid to world transformation for this
* {@link AbstractGridCoverage2DReader}.
*
* @param pixInCell
* specifies the datum of the transformation we want.
* @return the original grid to world transformation
*/
public static MathTransform getOriginalGridToWorld(
MathTransform raster2Model, final PixelInCell pixInCell) {
// we do not have to change the pixel datum
if (pixInCell == PixelInCell.CELL_CENTER)
return raster2Model;
// we do have to change the pixel datum
if (raster2Model instanceof AffineTransform) {
final AffineTransform tr = new AffineTransform(
(AffineTransform) raster2Model);
tr.concatenate(AffineTransform.getTranslateInstance(-0.5, -0.5));
return ProjectiveTransform.create(tr);
}
if (raster2Model instanceof IdentityTransform) {
final AffineTransform tr = new AffineTransform(1, 0, 0, 1, 0, 0);
tr.concatenate(AffineTransform.getTranslateInstance(-0.5, -0.5));
return ProjectiveTransform.create(tr);
}
throw new IllegalStateException(
"This grid to world transform is invalud!");
}
/**
* Evaluates the requested envelope and builds a new adjusted version of it
* fitting this coverage envelope.
*
* <p>
* While adjusting the requested envelope this methods also compute the
* source region as a rectangle which is suitable for a successive read
* operation with {@link ImageIO} to do crop-on-read.
*
* @param originalGridToWorld
*
* @param coordinateReferenceSystem
*
*
* @param requestedEnvelope
* is the envelope we are requested to load.
* @param sourceRegion
* represents the area to load in raster space. This
* parameter cannot be null since it gets filled with
* whatever the crop region is depending on the
* <code>requestedEnvelope</code>.
* @param requestedDim
* is the requested region where to load data of the
* specified envelope.
* @param readGridToWorld
* the Grid to world transformation to be used
* @param wgs84BaseEnvelope2D
* @return the adjusted requested envelope, empty if no requestedEnvelope
* has been specified, {@code null} in case the requested envelope
* does not intersect the coverage envelope or in case the adjusted
* requested envelope is covered by a too small raster region (an
* empty region).
*
* @throws DataSourceException
* in case something bad occurs
*/
public static GeneralEnvelope evaluateRequestedParams(
GridEnvelope originalGridRange, Envelope2D baseEnvelope2D,
CoordinateReferenceSystem spatialReferenceSystem2D,
MathTransform originalGridToWorld,
GeneralEnvelope requestedEnvelope, Rectangle sourceRegion,
Rectangle requestedDim, MathTransform2D readGridToWorld,
Envelope2D wgs84BaseEnvelope2D) throws DataSourceException {
GeneralEnvelope adjustedRequestedEnvelope = new GeneralEnvelope(2);
GeneralGridEnvelope baseGridRange = (GeneralGridEnvelope) originalGridRange;
try {
// ////////////////////////////////////////////////////////////////
//
// Check if we have something to load by intersecting the
// requested envelope with the bounds of this data set.
//
// ////////////////////////////////////////////////////////////////
if (requestedEnvelope != null) {
final GeneralEnvelope requestedEnvelope2D = Utilities.getRequestedEnvelope2D(requestedEnvelope);
// ////////////////////////////////////////////////////////////
//
// INTERSECT ENVELOPES AND CROP Destination REGION
//
// ////////////////////////////////////////////////////////////
adjustedRequestedEnvelope = Utilities.getIntersection(baseEnvelope2D, spatialReferenceSystem2D,
requestedEnvelope2D, requestedDim, readGridToWorld, wgs84BaseEnvelope2D);
if (adjustedRequestedEnvelope == null)
return null;
// /////////////////////////////////////////////////////////////////////
//
// CROP SOURCE REGION
//
// /////////////////////////////////////////////////////////////////////
sourceRegion.setRect(Utilities.getCropRegion(adjustedRequestedEnvelope, Utilities
.getOriginalGridToWorld(originalGridToWorld, PixelInCell.CELL_CORNER)));
if (sourceRegion.isEmpty()) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Too small envelope resulting in empty cropped raster region");
}
return null;
// TODO: Future versions may define a 1x1 rectangle starting
// from the lower coordinate
}
if (!sourceRegion.intersects(baseGridRange.toRectangle()) || sourceRegion.isEmpty())
throw new DataSourceException("The crop region is invalid.");
sourceRegion.setRect(sourceRegion.intersection(baseGridRange.toRectangle()));
if (LOGGER.isLoggable(Level.FINE)) {
StringBuilder sb = new StringBuilder(
"Adjusted Requested Envelope = ").append(
adjustedRequestedEnvelope.toString()).append("\n")
.append("Requested raster dimension = ").append(requestedDim.toString()).append("\n")
.append("Corresponding raster source region = ").append(sourceRegion.toString());
LOGGER.log(Level.FINE, sb.toString());
}
} else {
// don't use the source region. Set an empty one
sourceRegion.setBounds(new Rectangle(0, 0, Integer.MIN_VALUE,
Integer.MIN_VALUE));
}
} catch (TransformException e) {
throw new DataSourceException(
"Unable to create a coverage for this source", e);
} catch (FactoryException e) {
throw new DataSourceException(
"Unable to create a coverage for this source", e);
}
return adjustedRequestedEnvelope;
}
/**
* Creates a {@link GridCoverage} for the provided {@link PlanarImage} using
* the {@link #raster2Model} that was provided for this coverage.
*
* <p>
* This method is vital when working with coverages that have a raster to
* model transformation that is not a simple scale and translate.
*
* @param imageIndex
*
* @param image
* contains the data for the coverage to create.
* @param raster2Model
* is the {@link MathTransform} that maps from the raster
* space to the model space.
* @return a {@link GridCoverage}
* @throws IOException
*/
public static GridCoverage createCoverageFromImage(
final GridCoverageFactory coverageFactory,
final String coverageName, int imageIndex, PlanarImage image,
MathTransform raster2Model,
final CoordinateReferenceSystem spatialReferenceSystem2D,
GeneralEnvelope coverageEnvelope2D,
final GridSampleDimension[] sampleDimensions,
final boolean getGeophysics) throws IOException {
final GridSampleDimension[] bands = sampleDimensions;
GridCoverage2D gridCoverage;
// creating coverage
if (raster2Model != null) {
gridCoverage = coverageFactory.create(coverageName, image,
spatialReferenceSystem2D, raster2Model, bands, null, null);
} else
gridCoverage = coverageFactory.create(coverageName, image, coverageEnvelope2D, bands, null, null);
if (getGeophysics)
return gridCoverage.view(ViewType.GEOPHYSICS);
else
return gridCoverage;
}
/**
* This method is responsible for evaluating possible subsampling factors
* once the best resolution level has been found in case we have support for
* overviews, or starting from the original coverage in case there are no
* overviews available.
*
* @param readP
* the imageRead parameter to be set
* @param requestedRes
* the requested resolutions from which to determine the
* decimation parameters.
*/
public static void setDecimationParameters(ImageReadParam readP,
GridEnvelope baseGridRange, double[] requestedRes, double[] highestRes) {
{
if (readP == null || baseGridRange == null)
throw new IllegalArgumentException(
"Specified parameters are null");
final int w = baseGridRange.getSpan(0);
final int h = baseGridRange.getSpan(1);
// ///////////////////////////////////////////////////////////////
// DECIMATION ON READING
// Setting subsampling factors with some checkings
// 1) the subsampling factors cannot be zero
// 2) the subsampling factors cannot be such that the w or h are 0
// ///////////////////////////////////////////////////////////////
if (requestedRes == null) {
readP.setSourceSubsampling(1, 1, 0, 0);
} else {
int subSamplingFactorX = (int) Math.floor(requestedRes[0] / highestRes[0]);
subSamplingFactorX = (subSamplingFactorX == 0) ? 1 : subSamplingFactorX;
while (((w / subSamplingFactorX) <= 0) && (subSamplingFactorX >= 0))
subSamplingFactorX--;
subSamplingFactorX = (subSamplingFactorX == 0) ? 1 : subSamplingFactorX;
int subSamplingFactorY = (int) Math.floor(requestedRes[1] / highestRes[1]);
subSamplingFactorY = (subSamplingFactorY == 0) ? 1 : subSamplingFactorY;
while (((h / subSamplingFactorY) <= 0) && (subSamplingFactorY >= 0))
subSamplingFactorY--;
subSamplingFactorY = (subSamplingFactorY == 0) ? 1 : subSamplingFactorY;
readP.setSourceSubsampling(subSamplingFactorX, subSamplingFactorY, 0, 0);
}
}
}
public static NameImpl buildCoverageName(URL input) {
if (input == null)
throw new IllegalArgumentException("Null URL specified");
String fileName = input.getPath();
final int slashIndex = fileName.lastIndexOf("/");
// TODO: fix if slashIndex == -1
fileName = fileName.substring(slashIndex + 1, fileName.length());
final int dotIndex = fileName.lastIndexOf(".");
final String coverageNameString = (dotIndex == -1) ? fileName
: fileName.substring(0, dotIndex);
return new NameImpl(coverageNameString);
}
/**
* Prepares the read parameters 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.
*
* @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 overviews level upon request.
* @param readParam
* an instance of {@link ImageReadParam} for setting the
* subsampling factors.
* @param requestedEnvelope
* the {@link GeneralEnvelope} we are requesting.
* @param requestedDim
* the requested dimensions.
* @param gridRange
* @throws IOException
* @throws TransformException
*/
public static void setReadParameters(OverviewPolicy overviewPolicy,
ImageReadParam readParam, GeneralEnvelope requestedEnvelope,
Rectangle requestedDim, double[] highestRes, GridEnvelope gridRange,
PixelInCell pixelInCell) throws IOException, TransformException {
double[] requestedRes = null;
// //
//
// Initialize overview policy
//
// //
if (overviewPolicy == null) {
overviewPolicy = OverviewPolicy.NEAREST;
}
// //
//
// default values for subsampling
//
// //
readParam.setSourceSubsampling(1, 1, 0, 0);
// //
//
// requested to ignore overviews
//
// //
if (overviewPolicy.equals(OverviewPolicy.IGNORE)) {
return;
}
// //
//
// Resolution requested. I am here computing the resolution required
// by the user.
//
// //
if (requestedEnvelope != null) {
final GridToEnvelopeMapper geMapper = new GridToEnvelopeMapper();
geMapper.setEnvelope(requestedEnvelope);
geMapper.setGridRange(new GeneralGridEnvelope(requestedDim, 2));
geMapper.setPixelAnchor(pixelInCell);
final AffineTransform transform = geMapper.createAffineTransform();
requestedRes = CoverageUtilities.getResolution(transform);
}
if (requestedRes == null) {
return;
}
// ////////////////////////////////////////////////////////////////////
//
// DECIMATION ON READING
//
// ////////////////////////////////////////////////////////////////////
if (highestRes == null)
throw new IllegalArgumentException("Unspecified highest Resolution");
if ((requestedRes[0] > highestRes[0])
|| (requestedRes[1] > highestRes[1])) {
Utilities.setDecimationParameters(readParam, gridRange,
requestedRes, highestRes);
}
}
/**
* This method creates the GridCoverage2D from the underlying file given a
* specified envelope, and a requested dimension.
*
* @param imageIndex
* @param coordinateReferenceSystem
* @param generalEnvelope
* @param mathTransform
*
* @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 useMultithreading
* specify if the underlying read process should use
* multithreading when a JAI ImageRead operation is requested
* @param overviewPolicy
* the overview policy which need to be adopted
* @return a {@code GridCoverage}
*
* @throws java.io.IOException
*/
public static GridCoverage createCoverage(ImageReaderSpi spi, Object input,
final int imageIndex, ImageReadParam imageReadParam,
final boolean useJAI, final boolean useMultithreading,
final boolean newTransform,
final GridSampleDimension[] sampleDimensions,
final String coverageName, GridCoverageFactory coverageFactory,
MathTransform raster2Model,
CoordinateReferenceSystem coordinateReferenceSystem,
GeneralEnvelope coverageEnvelope2D) throws IOException {
// ////////////////////////////////////////////////////////////////////
//
// Doing an image read for reading the coverage.
//
// ////////////////////////////////////////////////////////////////////
final PlanarImage image = readImage(spi, input, imageIndex, useJAI,
imageReadParam, useMultithreading);
// /////////////////////////////////////////////////////////////////////
//
// Creating the coverage
//
// /////////////////////////////////////////////////////////////////////
if (newTransform) {
// I need to calculate a new transformation (raster2Model)
// between the cropped image and the required envelope
final int ssWidth = image.getWidth();
final int ssHeight = image.getHeight();
// //
//
// setting new coefficients to define a new affineTransformation
// to be applied to the grid to world transformation
// ------------------------------------------------------
//
// With respect to the original envelope, the obtained
// planarImage needs to be rescaled and translated. The scaling
// factors are computed as the ratio between the cropped source
// region sizes and the read image sizes. The translate
// settings are represented by the offsets of the source region.
//
// //
final Rectangle sourceRegion = imageReadParam.getSourceRegion();
final double scaleX = sourceRegion.width / (1.0 * ssWidth);
final double scaleY = sourceRegion.height / (1.0 * ssHeight);
final double translateX = sourceRegion.x;
final double translateY = sourceRegion.y;
return Utilities.createCoverageFromImage(coverageFactory,
coverageName, imageIndex, image, ConcatenatedTransform
.create(ProjectiveTransform
.create(new AffineTransform(scaleX, 0, 0,
scaleY, translateX, translateY)),
raster2Model), coordinateReferenceSystem,
(GeneralEnvelope) null, sampleDimensions, true);
} else {
// In case of no transformation is required (As an instance,
// when reading the whole image)
return Utilities.createCoverageFromImage(coverageFactory,
coverageName, imageIndex, image, (MathTransform) null,
(CoordinateReferenceSystem) null, coverageEnvelope2D,
sampleDimensions, true);
}
}
/**
* Returns a {@code PlanarImage} given a set of parameter specifying the
* type of read operation to be performed.
*
* @param imageIndex
*
* @param new
* FileImageInputStreamExtImplinput the input
* {@code ImageInputStream} to be used for reading the image.
* @param useJAI
* {@code true} if we need to use a JAI ImageRead operation,
* {@code false} if we need a simple direct
* {@code ImageReader.read(...)} call.
* @param imageReadParam
* an {@code ImageReadParam} specifying the read parameters
* @param useMultithreading
* {@code true} if a JAI ImageRead operation is requested
* with support for multithreading. This parameter will be
* ignored if requesting a direct read operation.
* @return the read {@code PlanarImage}
* @throws IOException
*/
public static PlanarImage readImage(final ImageReaderSpi spi,
final Object input, final int imageIndex, final boolean useJAI,
final ImageReadParam imageReadParam, final boolean useMultithreading)
throws IOException {
ImageInputStream paramInput = null;
if (input instanceof File) {
paramInput = new FileImageInputStreamExtImpl((File) input);
} else if (input instanceof FileImageInputStreamExt){
paramInput = (FileImageInputStreamExt) input;
} else if (input instanceof URIImageInputStream){
paramInput = (URIImageInputStream) input;
} else if (input instanceof URL){
final URL tempURL = (URL) input;
String protocol = tempURL.getProtocol();
if (protocol.equalsIgnoreCase("file")) {
try {
File file = it.geosolutions.imageio.utilities.Utilities.urlToFile(tempURL);
paramInput = new FileImageInputStreamExtImpl(file);
} catch (IOException e) {
throw new RuntimeException("Failed to create a valid input stream ", e);
}
} else if (tempURL.getProtocol().toLowerCase().startsWith("http")
|| tempURL.getProtocol().equalsIgnoreCase("dods")) {
try {
paramInput = new URIImageInputStreamImpl(tempURL.toURI());
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to create a valid input stream ", e);
}
}
}
else
throw new IllegalArgumentException("Unsupported Input type:" + input);
PlanarImage planarImage;
ImageReader reader;
ImageReaderSpi readerSpi = spi;
if (useJAI) {
final ParameterBlock pbjImageRead = new ParameterBlock();
pbjImageRead.add(paramInput);
pbjImageRead.add(imageIndex);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(Boolean.FALSE);
pbjImageRead.add(null);
pbjImageRead.add(null);
pbjImageRead.add(imageReadParam);
reader = readerSpi.createReaderInstance();
pbjImageRead.add(reader);
// Check if to use a simple JAI ImageRead operation or a
// multithreaded one
final String jaiOperation = useMultithreading ? "ImageReadMT" : "ImageRead";
// final String jaiOperation = "ImageRead";
/** TODO: SET HINTS */
planarImage = JAI.create(jaiOperation, pbjImageRead, null);
} else {
reader = readerSpi.createReaderInstance();
reader.setInput(paramInput, true, true);
planarImage = PlanarImage.wrapRenderedImage(reader.read(imageIndex, imageReadParam));
reader.dispose();
}
return planarImage;
}
/**
* Compute the coverage request and produce a grid coverage. The produced
* grid coverage may be {@code null} in case of empty request.
*
* @param index
* @param imageReadParam
* @param isEmptyRequest
* @param needTransformation
* @param imageReaderSpi
* @param coverageName
* @param coverageFactory
* @param raster2Model
* @param coordinateReferenceSystem
* @param envelope2D
*
* @throws IOException
* @TODO: handle more input types
*/
public static GridCoverage compute(
Object input,
final int imageIndex,
final boolean needTransformation,
final boolean isEmptyRequest,
final boolean useJAI, ImageReadParam imageReadParam,
final boolean useMultithreading,
final GridSampleDimension[] sampleDimensions,
final ImageReaderSpi imageReaderSpi,
final String coverageName,
final GridCoverageFactory coverageFactory,
final MathTransform raster2Model,
final CoordinateReferenceSystem coordinateReferenceSystem,
final GeneralEnvelope envelope2D) throws IOException {
GridCoverage gridCoverage = null;
if (isEmptyRequest)
gridCoverage = null;
else {
gridCoverage = Utilities.createCoverage(imageReaderSpi, input,
imageIndex, imageReadParam, useJAI, useMultithreading,
needTransformation, sampleDimensions, coverageName,
coverageFactory, raster2Model, coordinateReferenceSystem,
envelope2D);
}
return gridCoverage;
}
public final static boolean ensureValidString(final String... strings){
for (String string : strings){
if (string == null || string.trim().length() <= 0)
return false;
}
return true;
}
public static String buildIso8601Time(String date, String time) {
String iso8601Date = null;
if (ensureValidString(date, time)) {
final String formattedDate = date.replace("/", "-");
final StringBuilder sb = new StringBuilder(formattedDate).append("T").append(time).append("Z");
iso8601Date = sb.toString();
}
return iso8601Date;
}
/**
* Build a suitable {@link GridSampleDimension}
* @param sampleDim
* @param elementName
* @param unit
* @return
*/
public static GridSampleDimension buildBands(final Band sampleDim, final String elementName, Unit unit) {
if (sampleDim == null)
throw new IllegalArgumentException("provided metadata element is null");
// final Unit unit = CRSUtilities.parseUnit(sampleDim.getUoM());
Category[] categories = null;
Category values = null;
Category nan = null;
int cat = 0;
final double scale = sampleDim.getScale();
final double offset = sampleDim.getOffset();
final NumberRange<? extends Number> validRange = sampleDim.getValidRange();
final double[] noDataValues = sampleDim.getNoDataValues();
// Actually, the underlying readers use fillValue as noData
// Is this correct?
Number minO = null;
Number maxO = null;
if (validRange != null) {
minO = validRange.getMinValue();
maxO = validRange.getMaxValue();
}
if (!(scale == 1.0 && offset == 0.0)) {
if (minO != null && maxO != null) {
double min = validRange.getMinimum();
double max = validRange.getMaximum();
if (!Double.isInfinite(min) && !Double.isNaN(min)
&& !Double.isInfinite(max) && !Double.isNaN(max)) {
NumberRange<Double> minMax = NumberRange.create(min, false, max, true);
// double geoMin = (minMax.getMinimum(false)*scale)+offset;
// double geoMax = (minMax.getMaximum(true)*scale)+offset;
values = new Category("values", null, minMax, scale, offset);
// values = new Category("values", null, minMax
// ,NumberRange.create(geoMin, false, geoMax,true));
cat++;
}
}
}
if (noDataValues != null) {
final int size = noDataValues.length;
if (size == 1) {
double noData = noDataValues[0];
double newNoData = noData;
if (minO != null && maxO != null) {
double min = validRange.getMinimum();
double max = validRange.getMaximum();
if (min == noData && minO instanceof Integer) {
newNoData = min - 1;
} else if (max == noData && maxO instanceof Integer) {
newNoData = max + 1;
}
}
if (!Double.isNaN(noData)) {
nan = new Category(Vocabulary
.formatInternational(VocabularyKeys.NODATA),
new Color[] { new Color(0, 0, 0, 0) }, NumberRange
.create(noData, noData), NumberRange
.create(newNoData, newNoData));
}
}
cat++;
}
if (cat > 0) {
categories = new Category[cat];
if (cat == 2) {
categories[0] = nan;
categories[1] = values;
} else
categories[0] = nan == null ? values : nan;
}
final GridSampleDimension band = new GridSampleDimension(elementName+ ":sd", categories, unit);
return band;
}
}