/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2016, Geomatys
*
* 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.geotoolkit.metadata.geotiff;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotoolkit.image.io.ImageMetadataException;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.metadata.iso.spatial.PixelTranslation;
import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.crs.DefaultTemporalCRS;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.coverage.grid.RectifiedGrid;
import org.opengis.metadata.spatial.CellGeometry;
import org.opengis.metadata.spatial.Georectified;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.FactoryException;
import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.GridSampleDimension;
import org.geotoolkit.internal.image.io.DimensionAccessor;
import org.geotoolkit.internal.image.io.GridDomainAccessor;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.w3c.dom.Node;
import static org.geotoolkit.metadata.geotiff.GeoTiffConstants.*;
import org.geotoolkit.referencing.ReferencingUtilities;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.Utilities;
/**
*
* @author Johann Sorel (Geomatys)
* @author Marechal Remi (Geomatys)
* @module
*/
public class GeoTiffMetaDataWriter {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.metadata.geotiff");
public GeoTiffMetaDataWriter(){}
/**
* Complete the TIFF metadata tree with geotiff informations.
*/
public void fillMetadata(Node tiffTree, final SpatialMetadata spatialMD) throws ImageMetadataException, IOException, FactoryException, TransformException{
ArgumentChecks.ensureNonNull("tiffTree", tiffTree);
ArgumentChecks.ensureNonNull("spatialMD", spatialMD);
//container for informations which will be written
final GeoTiffMetaDataStack stack = new GeoTiffMetaDataStack(tiffTree);
//fill geotiff crs information
final CoordinateReferenceSystem coverageCRS = spatialMD.getInstanceForType(CoordinateReferenceSystem.class);
final GeoTiffCRSWriter crsWriter = new GeoTiffCRSWriter();
crsWriter.fillCRSMetaDatas(stack, CRSUtilities.getCRS2D(coverageCRS));
//fill the transformation information
final RectifiedGrid domain = spatialMD.getInstanceForType(RectifiedGrid.class);
AffineTransform gridToCrs = MetadataHelper.INSTANCE.getAffineTransform(domain, null);
//readjust gridToCRS to be the pixel corner
final Georectified georect = spatialMD.getInstanceForType(Georectified.class);
final CellGeometry cell = georect.getCellGeometry();
final PixelOrientation orientation = georect.getPointInPixel();
/*
* FAQ GEOTIFF :
* Setting the GTRasterTypeGeoKey value to RasterPixelIsPoint or RasterPixelIsArea
* alters how the raster coordinate space is to be interpreted.
* This is defined in section 2.5.2.2 of the GeoTIFF specification.
* => In the case of PixelIsArea (default) a pixel is treated as an area
* and the raster coordinate (0,0) is the top left corner of the top left pixel.
* => PixelIsPoint treats pixels as point samples with empty space between the "pixel" samples.
* In this case raster (0,0) is the location of the top left raster pixel.
*
* Note : GeoTiff mix the concepts of CellGeometry and PixelOrientation.
*/
if(CellGeometry.POINT.equals(cell)){
stack.addShort(GTRasterTypeGeoKey, RasterPixelIsPoint);
if(!orientation.equals(PixelOrientation.CENTER)){
AffineTransform2D trs = new AffineTransform2D(gridToCrs);
gridToCrs = (AffineTransform)PixelTranslation.translate(trs, orientation, PixelOrientation.CENTER,0,1);
}
}else{ //consider all other as Area
stack.addShort(GTRasterTypeGeoKey, RasterPixelIsArea);
if(!orientation.equals(PixelOrientation.UPPER_LEFT)){
AffineTransform2D trs = new AffineTransform2D(gridToCrs);
gridToCrs = (AffineTransform)PixelTranslation.translate(trs, orientation, PixelOrientation.UPPER_LEFT,0,1);
}
}
//-- find a date from crs
final int tempOrdinate = getTemporalOrdinate(coverageCRS);
if (tempOrdinate >= 0) {
//-- add temporal tag
final GridDomainAccessor gda = new GridDomainAccessor(spatialMD);
final double[] origin = gda.getAttributeAsDoubles("origin", false);
final double date = origin[tempOrdinate];
final Date dat = DefaultTemporalCRS.castOrCopy(CommonCRS.Temporal.JAVA.crs()).toDate(date);
stack.setDate(dat);
}
fillTransform(stack, gridToCrs, domain.getExtent());
//fill NoData values
fillSampleDimensionProperties(stack, spatialMD);
//write in the metadata tree
stack.flush();
}
/**
* Return the temporal ordinate from {@link CoordinateReferenceSystem} or -1 if temporal crs was not found.
*
* @return temporal ordinate if exist else return -1.
*/
private int getTemporalOrdinate(final CoordinateReferenceSystem crs) {
final List<CoordinateReferenceSystem> crss = ReferencingUtilities.decompose(crs);
int o = 0;
for (final CoordinateReferenceSystem c : crss) {
if (Utilities.equalsIgnoreMetadata(c, CommonCRS.Temporal.JAVA.crs())) return o;
o += c.getCoordinateSystem().getDimension();
}
return -1;
}
/**
* Fill metadata tree with {@link GridSampleDimension} properties like noData
* or minimum and maximum sample values.<br><br>
*
* Note : more informations at about tiff specification :<br>
* http://www.awaresystems.be/imaging/tiff/tifftags/minsamplevalue.html<br>
* http://www.awaresystems.be/imaging/tiff/tifftags/maxsamplevalue.html<br>
* http://www.awaresystems.be/imaging/tiff/tifftags/gdal_nodata.html<br>
*
* @param stack
* @param spatialMD metadata tree which will be filled.
* @see GeoTiffMetaDataStack#setMinSampleValue(int...)
* @see GeoTiffMetaDataStack#setMaxSampleValue(int...)
* @see GeoTiffMetaDataStack#setNoData(java.lang.String)
*/
private void fillSampleDimensionProperties(final GeoTiffMetaDataStack stack, final SpatialMetadata spatialMD) {
ArgumentChecks.ensureNonNull("stack", stack);
ArgumentChecks.ensureNonNull("spatialMD", spatialMD);
final DimensionAccessor accessor = new DimensionAccessor(spatialMD);
final List<GridSampleDimension> sampleDimensions = accessor.getGridSampleDimensions();
if (sampleDimensions == null) {
LOGGER.log(Level.FINE, "GeotiffMetadataWriter : no gridSampleDimension setted into spatialMetadata.");
return;
}
final int sampleDimensionNumber = sampleDimensions.size();
double[] noData = null;
final int[] minSV = new int[sampleDimensionNumber];
final int[] maxSV = new int[sampleDimensionNumber];
int minSVId = 0;
int maxSVId = 0;
if (sampleDimensions != null && !sampleDimensions.isEmpty()) {
for (GridSampleDimension dimension : sampleDimensions) {
//-- min samplevalue
final double minSVd = dimension.getMinimumValue();
if (checkDoubleToShort(minSVd)) minSV[minSVId++] = (short) minSVd;
//-- maxSampleValue
final double maxSVd = dimension.getMaximumValue();
if (checkDoubleToShort(maxSVd)) maxSV[maxSVId++] = (short) maxSVd;
final double[] dimNoData = dimension.getNoDataValues();
if (noData == null) {
noData = dimNoData;
} else {
if (!Arrays.equals(noData, dimNoData)) {
LOGGER.warning("Unable to fill Geotiff nodata tag cause : all bands must use the same nodata values."+
"expected : "+Arrays.toString(noData)+" found : "+Arrays.toString(dimNoData));
return;
}
}
}
}
//-- if all bands are stipulate
if (minSVId == sampleDimensionNumber) stack.setMinSampleValue(minSV);
//-- if all bands are stipulate
if (maxSVId == sampleDimensionNumber) stack.setMaxSampleValue(maxSV);
if (noData != null && noData.length > 0)
for (double d : noData)
stack.setNoData(String.valueOf(d));
}
/**
* Returns {@code true} if {@code double} value may be cast to {@code short} and lost nothing.<br><br>
*
* Note : Tiff specification only accept short value to stipulate minimum or maximum sample value for each bands.<br>
* For more explanations see : http://www.awaresystems.be/imaging/tiff/tifftags/minsamplevalue.html <br>
* and http://www.awaresystems.be/imaging/tiff/tifftags/maxsamplevalue.html
*
* @param value double which will be cast.
* @return {@code true} if {@code double} value may be cast to {@code short}
* @see GeoTiffMetaDataStack#setMinSampleValue(int...)
* @see GeoTiffMetaDataStack#setMaxSampleValue(int...)
*/
private boolean checkDoubleToShort(final double value) {
final short st = (short) value;
return ((double) st) == value;
}
private void fillTransform(final GeoTiffMetaDataStack stack,
final AffineTransform gridToCRS, final GridEnvelope range) {
stack.setModelTransformation(gridToCRS);
// /////////////////////////////////////////////////////////////////////
// We have to set an affine transformation which is going to be 2D
// since we support baseline GeoTiff.
// /////////////////////////////////////////////////////////////////////
final AffineTransform modifiedRasterToModel;
final int minx = range.getLow(0);
final int miny = range.getLow(1);
if (minx != 0 || miny != 0) {
// //
// Preconcatenate a transform to have raster space beginning at (0,0)
// //
modifiedRasterToModel = new AffineTransform(gridToCRS);
modifiedRasterToModel.concatenate(AffineTransform.getTranslateInstance(minx, miny));
} else {
modifiedRasterToModel = gridToCRS;
}
// /////////////////////////////////////////////////////////////////////
// AXES DIRECTION
// we need to understand how the axes of this gridcoverage are
// specified, trying to understand the direction of the first axis in
// order to correctly use transformations.
//
// Note that here wew assume that in case of a Flip the flip is on the Y
// axis.
// /////////////////////////////////////////////////////////////////////
final boolean lonFirst = AffineTransforms2D.getSwapXY(modifiedRasterToModel) != -1;
// /////////////////////////////////////////////////////////////////////
// ROTATION
// If fthere is not rotation or shearing or flipping we have a simple
// scale and translate hence we can simply set the tie points.
// /////////////////////////////////////////////////////////////////////
final double rotation = AffineTransforms2D.getRotation(modifiedRasterToModel);
// /////////////////////////////////////////////////////////////////////
// Deciding how to save the georef with respect to the CRS.
// /////////////////////////////////////////////////////////////////////
// tie points
if (!(Double.isInfinite(rotation) || Double.isNaN(rotation) || Math.abs(rotation) > 1E-6)) {
final double tiePointLongitude = (lonFirst) ? modifiedRasterToModel.getTranslateX() : modifiedRasterToModel.getTranslateY();
final double tiePointLatitude = (lonFirst) ? modifiedRasterToModel.getTranslateY() : modifiedRasterToModel.getTranslateX();
stack.addModelTiePoint(new TiePoint(0, 0, 0, tiePointLongitude, tiePointLatitude, 0));
// scale
final double scaleModelToRasterLongitude = (lonFirst) ? Math.abs(modifiedRasterToModel.getScaleX()) : Math.abs(modifiedRasterToModel.getShearY());
final double scaleModelToRasterLatitude = (lonFirst) ? Math.abs(modifiedRasterToModel.getScaleY()) : Math.abs(modifiedRasterToModel.getShearX());
stack.setModelPixelScale(scaleModelToRasterLongitude, scaleModelToRasterLatitude, 0);
// Alternative code, not yet enabled in order to avoid breaking
// code.
// The following code is insensitive to axis order and rotations in
// the 'coord' space (not in the 'grid' space, otherwise we would
// not take the inverse of the matrix).
/*
* final AffineTransform coordToGrid = gridToCoord.createInverse();
* final double scaleModelToRasterLongitude = 1 /
* AffineTransforms2D.getScaleX0(coordToGrid); final double
* scaleModelToRasterLatitude = 1 /
* AffineTransforms2D.getScaleY0(coordToGrid);
*/
} else {
stack.setModelTransformation(modifiedRasterToModel);
}
}
}