/* * (c) 2004 Mike Nidel * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * Take, Modify, Distribute freely * Buy, Sell, Pass it off as your own * * Use this code at your own risk, the author makes no guarantee * of performance and retains no liability for the failure of this * software. * * If you feel like it, send any suggestions for improvement or * bug fixes, or modified source code to mike@gelbin.org * * Do not taunt Happy Fun Ball. * */ /* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-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.grid.io.imageio.geotiff; import it.geosolutions.imageio.plugins.tiff.GeoTIFFTagSet; import java.awt.geom.AffineTransform; import java.util.Collection; import java.util.HashMap; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import org.geotools.coverage.grid.io.imageio.geotiff.codes.GeoTiffGCSCodes; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Errors; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * This class provides an abstraction from the details of TIFF data access for * the purpose of retrieving GeoTIFFWritingUtilities metadata from an image. * * <p> * All of the GeoKey values are included here as constants, and the portions of * the GeoTIFFWritingUtilities specification pertaining to each have been copied * for easy access. * </p> * * <p> * The majority of the possible GeoKey values and their meanings are NOT * reproduced here. Only the most important GeoKey code values have been copied, * for others see the specification. * </p> * * <p> * Convenience methods have been included to retrieve the various TIFFFields * that are not part of the GeoKey directory, such as the Model Transformation * and Model TiePoints. Retrieving a GeoKey from the GeoKey directory is a bit * more specialized and requires knowledge of the correct key code. * </p> * * <p> * Making use of the geographic metadata still requires some basic understanding * of the GeoKey values that is not provided here. * </p> * * <p> * For more information see the GeoTIFFWritingUtilities specification at * http://www.remotesensing.org/geotiff/spec/geotiffhome.html * </p> * * @author Mike Nidel * @author Simone Giannecchini, GeoSolutions * * @source $URL: * http://svn.geotools.org/geotools/trunk/gt/plugin/geotiff/src/org/geotools/gce/geotiff/IIOMetadataAdpaters/GeoTiffIIOMetadataDecoder.java $ */ public final class GeoTiffIIOMetadataDecoder { private final IIOMetadata iioMetadata; private final HashMap<Integer, GeoKeyEntry> geoKeys; private int geoKeyDirVersion; private int geoKeyRevision; private int geoKeyMinorRevision; private int geoKeyDirTagsNum; private final PixelScale pixelScale; private final TiePoint[] tiePoints; private final double noData; private final AffineTransform modelTransformation; /** * The constructor builds a metadata adapter for the image metadata root * IIOMetadataNode. * * @param imageMetadata * The image metadata */ public GeoTiffIIOMetadataDecoder(final IIOMetadata imageMetadata) { if (imageMetadata == null){ throw new NullPointerException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"imageMetadata")); } iioMetadata = imageMetadata; // getting the image metadata root node. Node rootNode = getRootNode(); if (rootNode == null) { throw new NullPointerException("Unable to retrieve metadata"); } pixelScale = calculateModelPixelScales(rootNode); tiePoints = calculateTiePoints(rootNode); noData = calculateNoData(rootNode); modelTransformation = calculateModelTransformation(rootNode); // getting the geokey ddirectory IIOMetadataNode geoKeyDir = getTiffField(rootNode, GeoTIFFTagSet.TAG_GEO_KEY_DIRECTORY); geoKeys = new HashMap<Integer, GeoKeyEntry>(); if (geoKeyDir != null) { NodeList geoKeyDirEntries = geoKeyDir.getFirstChild().getChildNodes(); for (int i = 4; i < geoKeyDirEntries.getLength(); i += 4) { int keyID = getIntValueAttribute(geoKeyDirEntries.item(i)); GeoKeyEntry key = new GeoKeyEntry(keyID,// key getIntValueAttribute(geoKeyDirEntries.item(i + 1)),// location getIntValueAttribute(geoKeyDirEntries.item(i + 2)),// count getIntValueAttribute(geoKeyDirEntries.item(i + 3)));// offset if (!geoKeys.containsKey(keyID)) { geoKeys.put( keyID, key); } } // GeoKeyDirVersion and the other parameters geoKeyDirVersion = getTiffShort(geoKeyDir,GeoTiffGCSCodes.GEO_KEY_DIRECTORY_VERSION_INDEX); geoKeyRevision = getTiffShort(geoKeyDir,GeoTiffGCSCodes.GEO_KEY_REVISION_INDEX); if (geoKeyRevision != 1) { geoKeyRevision = 1; // I had to remove this because I did not want to have wrong // revision numbers blocking us. // throw new UnsupportedOperationException("Unsupported revision"); } geoKeyMinorRevision = getTiffShort(geoKeyDir, GeoTiffGCSCodes.GEO_KEY_MINOR_REVISION_INDEX); // loading the number of geokeys inside the geokeydirectory geoKeyDirTagsNum = getTiffShort(geoKeyDir, GeoTiffGCSCodes.GEO_KEY_NUM_KEYS_INDEX); } } /** * Gets the version of the GeoKey directory. This is typically a value of 1 * and can be used to check that the data is of a valid format. * * @return DOCUMENT ME! * * @throws UnsupportedOperationException * DOCUMENT ME! */ public int getGeoKeyDirectoryVersion() { // now get the value from the correct TIFFShort location return geoKeyDirVersion; } /** * Gets the revision number of the GeoKeys in this metadata. * * @return DOCUMENT ME! * * @throws UnsupportedOperationException * DOCUMENT ME! */ public int getGeoKeyRevision() { // Get the value from the correct TIFFShort return geoKeyRevision; } /** * Gets the minor revision number of the GeoKeys in this metadata. * * @return DOCUMENT ME! * * @throws UnsupportedOperationException * DOCUMENT ME! */ public int getGeoKeyMinorRevision() { // Get the value from the correct TIFFShort return geoKeyMinorRevision; } /** * Gets the number of GeoKeys in the geokeys directory. * * @return DOCUMENT ME! * * @throws UnsupportedOperationException * DOCUMENT ME! */ public int getNumGeoKeys() { return geoKeyDirTagsNum; } /** * Gets a GeoKey value as a String. This implementation should be * "quiet" in the sense that it should not throw any exceptions but * only return null in the event that the data organization is not as * expected. * * @param keyID * The numeric ID of the GeoKey * * @return A string representing the value, or null if the key was not * found. */ public String getGeoKey(final int keyID) { final GeoKeyEntry rec = getGeoKeyRecord(keyID); if (rec == null){ return null; } if (rec.getTiffTagLocation() == 0){ // value is stored directly in the GeoKey record return String.valueOf(rec.getValueOffset()); } // value is stored externally // get the TIFF field where the data is actually stored final IIOMetadataNode field = getTiffField(getRootNode(), rec.getTiffTagLocation()); if (field == null){ return null; } final Node sequence = field.getFirstChild(); if (sequence == null){ return null; } return sequence.getNodeName().equals(GeoTiffConstants.GEOTIFF_ASCIIS_TAG) ? getTiffAscii((IIOMetadataNode) sequence, rec.getValueOffset(), rec.getCount()) : getValueAttribute(sequence.getChildNodes().item(rec.getValueOffset())); } /** * Gets a record containing the four TIFFShort values for a geokey entry. * For more information see the GeoTIFFWritingUtilities specification. * * @param keyID * DOCUMENT ME! * * @return the record with the given keyID, or null if none is found * * @throws UnsupportedOperationException * DOCUMENT ME! */ public GeoKeyEntry getGeoKeyRecord(int keyID) { return geoKeys.get(keyID); } /** * Return the GeoKeys. * @return **/ public Collection<GeoKeyEntry> getGeoKeys() { return geoKeys.values(); } private PixelScale calculateModelPixelScales(Node rootNode) { final double[] pixScales = getTiffDoubles(getTiffField(rootNode, GeoTIFFTagSet.TAG_MODEL_PIXEL_SCALE)); if (pixScales == null) { return null; } PixelScale retVal = new PixelScale(); for (int i = 0; i < pixScales.length; i++) switch (i) { case 0: retVal.setScaleX(pixScales[i]); break; case 1: retVal.setScaleY(pixScales[i]); break; case 2: retVal.setScaleZ(pixScales[i]); break; } return retVal; } /** * Gets the model pixel scales from the correct TIFFField */ public PixelScale getModelPixelScales() { return pixelScale; } /** * Gets the model tie points from the appropriate TIFFField * * @return the tie points, or null if not found */ public TiePoint[] getModelTiePoints() { return tiePoints; } private TiePoint[] calculateTiePoints(Node rootNode) { IIOMetadataNode node = getTiffField(rootNode, GeoTIFFTagSet.TAG_MODEL_TIE_POINT); if (node == null) { return null; } final double tiePoints[] = getTiffDoubles(node); if (tiePoints == null || tiePoints.length <= 0) { return null; } final int numTiePoints = tiePoints.length / 6; final TiePoint retVal[] = new TiePoint[numTiePoints]; int initialIndex = 0; for (int i = 0; i < numTiePoints; i++) { initialIndex = i * 6; retVal[i] = new TiePoint(tiePoints[initialIndex], tiePoints[initialIndex + 1], tiePoints[initialIndex + 2], tiePoints[initialIndex + 3], tiePoints[initialIndex + 4], tiePoints[initialIndex + 5]); } return retVal; } /** * Gets the noData from the related TIFFField. Check metadata has noData * using {@link #hasNoData()} method before calling this method. * * @return the noData value or {@link Double#NaN} in case of unable to * get noData. * */ public double getNoData() { return noData; } private double calculateNoData(Node rootNode) { final IIOMetadataNode noDataNode = getTiffField(rootNode, GeoTiffConstants.TIFFTAG_NODATA); if (noDataNode == null) { return Double.NaN; } final String noData = getTiffAscii(noDataNode); if (noData == null) { return Double.NaN; } try { return Double.parseDouble(noData); } catch (NumberFormatException nfe){ //TODO: Log a message. return Double.NaN; } } /** * Tells me if the underlying {@link IIOMetadata} contains ModelTiepointTag * tag for {@link TiePoint}. * * @return true if ModelTiepointTag is present, false otherwise. */ public boolean hasTiePoints() { return tiePoints != null && tiePoints.length > 0; } /** * Tells me if the underlying {@link IIOMetadata} contains ModelTiepointTag * tag for {@link TiePoint}. * * @return true if ModelTiepointTag is present, false otherwise. */ public boolean hasPixelScales() { if (pixelScale == null){ return false; } else { final double[] values = pixelScale.getValues(); for (int i = 0; i < values.length; i++){ if (Double.isInfinite(values[i]) || Double.isNaN(values[i])){ return false; } } return true; } } /** * Tells me if the underlying {@link IIOMetadata} contains NoData Tag. * * @return true if NoData Tag is present, false otherwise. * @see GeoTiffConstants#TIFFTAG_NODATA */ public boolean hasNoData() { return !Double.isNaN(noData); } /** * Returns the value of an ASCII TIFFTag referred by tagID. * * @return the tag value as a String, null if not available. * */ public String getAsciiTIFFTag(final String tagID) { if (GeoTiffConstants.isNumeric(tagID)){ final IIOMetadataNode metadataNode = getTiffField(getRootNode(), Integer.valueOf(tagID)); if (metadataNode != null){ return getTiffAscii(metadataNode); } } return null; } /** * Gets the model tie points from the appropriate TIFFField * * <p> * Attention, for the moment we support only 2D baseline transformations. * * @return the transformation, or null if not found */ public AffineTransform getModelTransformation() { return modelTransformation; } private AffineTransform calculateModelTransformation(Node rootNode) { final IIOMetadataNode node = getTiffField(rootNode, GeoTIFFTagSet.TAG_MODEL_TRANSFORMATION); if (node == null) { return null; } final double[] modelTransformation = getTiffDoubles(node); if (modelTransformation == null) { return null; } AffineTransform transform = null; if (modelTransformation.length == 9) { transform = new AffineTransform(modelTransformation[0], modelTransformation[4], modelTransformation[1], modelTransformation[5], modelTransformation[6], modelTransformation[7]); } else if (modelTransformation.length == 16) { transform = new AffineTransform(modelTransformation[0], modelTransformation[4], modelTransformation[1], modelTransformation[5], modelTransformation[3], modelTransformation[7]); } return transform; } /** * Tells me if the underlying {@link IIOMetadata} contains * ModelTransformationTag tag for {@link AffineTransform} that map from * Raster Space to World Space. * * @return true if ModelTransformationTag is present, false otherwise. * */ public boolean hasModelTrasformation() { return modelTransformation != null; } // private utility methods /** * Gets the value attribute of the given Node. * * @param node * A Node containing a value attribute, for example the node * <TIFFShort value="123"> * * @return A String containing the text from the value attribute. In the * above example, the string would be 123 */ private String getValueAttribute(Node node) { return node.getAttributes().getNamedItem(GeoTiffConstants.VALUE_ATTRIBUTE) .getNodeValue(); } /** * Gets the value attribute's contents and parses it as an int * * @param node * DOCUMENT ME! * * @return DOCUMENT ME! */ private int getIntValueAttribute(Node node) { return Integer.parseInt(getValueAttribute(node)); } /** * Gets a TIFFField node with the given tag number. This is done by searching for a TIFFField * with attribute number whose value is the specified tag value. * * @param tag DOCUMENT ME! * * @return DOCUMENT ME! */ private IIOMetadataNode getTiffField(Node rootNode, final int tag) { Node node = rootNode.getFirstChild(); if (node != null){ node = node.getFirstChild(); for (; node != null; node = node.getNextSibling()) { Node number = node.getAttributes().getNamedItem(GeoTiffConstants.NUMBER_ATTRIBUTE); if (number != null && tag == Integer.parseInt(number.getNodeValue())) { return (IIOMetadataNode) node; } } } return null; } /** * Gets a single TIFFShort value at the given index. * * @param tiffField * An IIOMetadataNode pointing to a TIFFField element that * contains a TIFFShorts element. * @param index * The 0-based index of the desired short value * * @return DOCUMENT ME! */ private int getTiffShort(final IIOMetadataNode tiffField, final int index) { return getIntValueAttribute(((IIOMetadataNode) tiffField .getFirstChild()).getElementsByTagName( GeoTiffConstants.GEOTIFF_SHORT_TAG).item(index)); } /** * Gets an array of double values from a TIFFDoubles TIFFField. * * @param tiffField * An IIOMetadataNode pointing to a TIFFField element that * contains a TIFFDoubles element. * * @return DOCUMENT ME! */ private double[] getTiffDoubles(final IIOMetadataNode tiffField) { if (tiffField == null) { return null; } final NodeList doubles = ((IIOMetadataNode) tiffField.getFirstChild()) .getElementsByTagName(GeoTiffConstants.GEOTIFF_DOUBLE_TAG); final int length = doubles.getLength(); final double[] result = new double[length]; for (int i = 0; i < length; i++) { result[i] = Double.parseDouble(getValueAttribute(doubles.item(i))); } return result; } /** * Gets a portion of a TIFFAscii string with the specified start character * and length; * * @param tiffField * An IIOMetadataNode pointing to a TIFFField element that * contains a TIFFAsciis element. This element should contain a * single TiffAscii element. * @param start * DOCUMENT ME! * @param length * DOCUMENT ME! * * @return A substring of the value contained in the TIFFAscii node, with * the final '|' character removed. */ private String getTiffAscii(final IIOMetadataNode tiffField, int start, int length) { // there should be only one, so get the first // GeoTIFFWritingUtilities specification places a vertical bar '|' in // place of \0 // null delimiters so drop off the vertical bar for Java Strings final String valueAttribute = getValueAttribute( ((IIOMetadataNode) tiffField.getFirstChild()) .getElementsByTagName(GeoTiffConstants.GEOTIFF_ASCII_TAG).item(0)); if (start == -1) start = 0; if (length == -1) length = valueAttribute.length() + 1; return valueAttribute.substring(start, start + length - 1); } /** * Gets the TIFFAscii string * * @param tiffField * An IIOMetadataNode pointing to a TIFFField element that * contains a TIFFAsciis element. This element should contain a * single TIFFAscii element. * * @return The value contained in the TIFFAscii node, with * the final '|' character removed. */ private String getTiffAscii(final IIOMetadataNode tiffField) { return getTiffAscii(tiffField,-1,-1); } public IIOMetadataNode getRootNode() { return (IIOMetadataNode) iioMetadata.getAsTree(iioMetadata.getNativeMetadataFormatName()); } /** * Return <code>true</code> if the geokey directory is present, <code>false</code> otherwise. * In case no geokey dir is present no CRS can be constructed from this set of metadata. * * <p> * A prj can be used otherwise. * @return <code>true</code> if the geokey directory is present, <code>false</code> otherwise. */ public boolean hasGeoKey(){ return !geoKeys.isEmpty(); } } // end of class GeoTiffIIOMetadataDecoder