/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-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.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.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.renderable.ParameterBlock;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
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.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
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.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.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.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.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import sun.awt.OSInfo;
import sun.awt.OSInfo.OSType;
/**
* @author Daniele Romagnoli, GeoSolutions
*
* @source $URL$
*/
public class Utilities {
private final static Logger LOGGER = org.geotools.util.logging.Logging
.getLogger(Utilities.class.toString());
private final static OSType OSTYPE = OSInfo.getOSType();
/**
* TODO: Define a contains method which allows to know if the extent of a CoverageSlice contains a predefined extent. This would be useful to
* know which CoverageSlice 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 (OSTYPE.equals(OSType.WINDOWS)) {
if (imageSpecifierIndex > 1 && imageSpecifierIndex > path.indexOf(":")) {
file = new File(path.substring(0, imageSpecifierIndex));
} else {
file = fileCheck;
}
} else {
if (imageSpecifierIndex > 0) {
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;
}
/**
* 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 CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
// do we need to transform the requested envelope?
if (!CRS.equalsIgnoreMetadata(crs, DefaultGeographicCRS.WGS84)) {
GeneralEnvelope env = CRS.transform(envelope, DefaultGeographicCRS.WGS84);
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.
if (!CRS.equalsIgnoreMetadata(requestedEnvelopeCRS2D, spatialReferenceSystem2D)) {
adjustedRequestedEnvelope = CRS.transform(requestedEnvelope2D,
spatialReferenceSystem2D);
} 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 = CRS.transform(adjustedRequestedEnvelope,
requestedEnvelopeCRS2D);
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;
}
// //
//
// 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(adjustedRequestedEnvelope,
spatialReferenceSystem2D);
adjustedRequestedEnvelope.setCoordinateReferenceSystem(spatialReferenceSystem2D);
}
return adjustedRequestedEnvelope;
}
/**
* 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) 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);
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);
} 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);
}
}
/**
* 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 {
if (isEmptyRequest) {
return null;
} else {
return Utilities.createCoverage(imageReaderSpi, input, imageIndex,
imageReadParam, useJAI, useMultithreading, needTransformation,
sampleDimensions, coverageName, coverageFactory, raster2Model,
coordinateReferenceSystem, envelope2D);
}
}
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;
// }
// /**
// * 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;
// }
//
// /**
// * Move these methods to an Utility Class and improve the logic.
// *
// * @param first
// * @param second
// * @return
// */
// public static boolean isTimeAccepted( TemporalGeometricPrimitive first, TemporalGeometricPrimitive second ) {
// boolean takeThis = Utilities.contains(first, second);
// if (!takeThis)
// takeThis = Utilities.contains(second, first);
// return takeThis;
// }
/**
* Checks that a {@link File} is a real file, exists and is readable.
*
* @param file the {@link File} instance to check. Must not be null.
*
* @return <code>true</code> in case the file is a real file, exists and is readable; <code>false </code> otherwise.
*/
public static boolean checkFileReadable(final File file) {
if (LOGGER.isLoggable(Level.FINE)) {
final String message = getFileInfo(file);
LOGGER.fine(message);
}
if (!file.exists() || !file.canRead() || !file.isFile())
return false;
return true;
}
/**
* Creates a human readable message that describe the provided {@link File} object in terms of its properties.
*
* <p>
* Useful for creating meaningful log messages.
*
* @param file the {@link File} object to create a descriptive message for
* @return a {@link String} containing a descriptive message about the provided {@link File}.
*
*/
public static String getFileInfo(final File file) {
final StringBuilder builder = new StringBuilder();
builder.append("Checking file:").append(FilenameUtils.getFullPath(file.getAbsolutePath()))
.append("\n");
builder.append("isHidden:").append(file.isHidden()).append("\n");
builder.append("exists:").append(file.exists()).append("\n");
builder.append("isFile").append(file.isFile()).append("\n");
builder.append("canRead:").append(file.canRead()).append("\n");
builder.append("canWrite").append(file.canWrite()).append("\n");
builder.append("canExecute:").append(file.canExecute()).append("\n");
builder.append("isAbsolute:").append(file.isAbsolute()).append("\n");
builder.append("lastModified:").append(file.lastModified()).append("\n");
builder.append("length:").append(file.length());
final String message = builder.toString();
return message;
}
public static Properties loadPropertiesFromURL(URL 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 (LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
} catch (IOException e) {
if (LOGGER.isLoggable(Level.SEVERE))
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
return null;
} finally {
if (stream != null) {
IOUtils.closeQuietly(stream);
}
if (openStream != null) {
IOUtils.closeQuietly(openStream);
}
}
return properties;
}
}