/* * (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.gce.geotiff.adapters; import it.geosolutions.imageio.plugins.tiff.GeoTIFFTagSet; import java.awt.geom.AffineTransform; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import org.geotools.gce.geotiff.codes.GeoTiffGCSCodes; 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 $ * @todo we can improve a little bt this class caching the pixel scale, the transformation, etc... */ public final class GeoTiffIIOMetadataDecoder { /** The root of the metadata DOM tree */ private IIOMetadataNode rootNode = null; private IIOMetadataNode geoKeyDir = null; private NodeList geoKeyDirEntries = null; private int geoKeyDirEntriesNum = 0; private IIOMetadataNode tiffTagsEntries; private int numTiffTasEntries; private int geoKeyDirVersion; private int geoKeyRevision; private int geoKeyMinorRevision; private int geoKeyDirTagsNum; private IIOMetadataNode geoKeyDoubleParams; private IIOMetadataNode geoKeyAsciiParams; /** * The constructor builds a metadata adapter for the image metadata root * IIOMetadataNode. * * @param imageMetadata * The image metadata */ public GeoTiffIIOMetadataDecoder(final IIOMetadata imageMetadata) { // getting the image metadata root node. rootNode = (IIOMetadataNode) imageMetadata.getAsTree(imageMetadata.getNativeMetadataFormatName()); if (rootNode == null) { throw new IllegalArgumentException( "Unable to retrieve metadata"); } tiffTagsEntries = (IIOMetadataNode) rootNode.getFirstChild().getChildNodes(); if (rootNode == null) { throw new IllegalArgumentException( "Unable to retrieve metadata"); } numTiffTasEntries = tiffTagsEntries.getLength(); // getting the geokey ddirectory geoKeyDir = getTiffField(GeoTIFFTagSet.TAG_GEO_KEY_DIRECTORY); if (geoKeyDir == null) { throw new IllegalArgumentException( "GeoKey directory does not exist"); } // getting all the entries and its nunber geoKeyDirEntries = geoKeyDir.getFirstChild().getChildNodes(); // 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); // each geokey has 4 entries geoKeyDirEntriesNum = geoKeyDirEntries.getLength(); geoKeyDoubleParams = getTiffField(GeoTIFFTagSet.TAG_GEO_DOUBLE_PARAMS); geoKeyAsciiParams = getTiffField(GeoTIFFTagSet.TAG_GEO_ASCII_PARAMS); } /** * 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(rec.getTiffTagLocation()); if (field != null) { final Node sequence = field.getFirstChild(); if (sequence != null) { if (sequence.getNodeName().equals( GeoTiffConstants.GEOTIFF_ASCIIS_TAG)) { // TIFFAscii values are handled specially return getTiffAscii((IIOMetadataNode) sequence, rec .getValueOffset(), rec.getCount()); } else { // value is numeric return getValueAttribute(sequence.getChildNodes().item( rec.getValueOffset())); } } } return null; } /** * 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) { int thisKeyID = 0; // embed the exit condition in the for loop for (int i = 4; i < geoKeyDirEntriesNum; i += 4) { thisKeyID = getIntValueAttribute(geoKeyDirEntries.item(i));// key if (thisKeyID == keyID) { // we've found the right GeoKey, now build it return new GeoKeyEntry(thisKeyID, getIntValueAttribute(geoKeyDirEntries.item(i + 1)),// location getIntValueAttribute(geoKeyDirEntries.item(i + 2)),// count getIntValueAttribute(geoKeyDirEntries.item(i + 3)));// offset } } return null; } /** * Gets a record containing the four TIFFShort values for a geokey entry. * For more information see the GeoTIFFWritingUtilities specification. * * @param index * DOCUMENT ME! * * @return the record with the given keyID, or null if none is found * * @throws UnsupportedOperationException * DOCUMENT ME! */ public GeoKeyEntry getGeoKeyRecordByIndex(int index) { index *= 4; return new GeoKeyEntry(getIntValueAttribute(geoKeyDirEntries .item(index)), getIntValueAttribute(geoKeyDirEntries .item(index + 1)), getIntValueAttribute(geoKeyDirEntries .item(index + 2)), getIntValueAttribute(geoKeyDirEntries .item(index + 3))); } /** * Gets the model pixel scales from the correct TIFFField * */ public PixelScale getModelPixelScales() { final double[] pixScales = getTiffDoubles(getTiffField(GeoTIFFTagSet.TAG_MODEL_PIXEL_SCALE)); if (pixScales == null) return null; final int length = pixScales.length; final PixelScale retVal = new PixelScale(); for (int i = 0; i < 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 tie points from the appropriate TIFFField * * @return the tie points, or null if not found */ public TiePoint[] getModelTiePoints() { IIOMetadataNode node = getTiffField(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() { final IIOMetadataNode noDataNode = getTiffField(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() { IIOMetadataNode node = getTiffField(GeoTIFFTagSet.TAG_MODEL_TIE_POINT); if (node == null) return false; final double tiePoints[] = getTiffDoubles(node); if (tiePoints == null || tiePoints.length <= 0) return false; return true; } /** * Tells me if the underlying {@link IIOMetadata} contains ModelTiepointTag * tag for {@link TiePoint}. * * @return true if ModelTiepointTag is present, false otherwise. */ public boolean hasPixelScales() { final double[] pixScales = getTiffDoubles(getTiffField(GeoTIFFTagSet.TAG_MODEL_PIXEL_SCALE)); if (pixScales == null) return false; final int length = pixScales.length; double tempVal; for (int i = 0; i < length; i++) { tempVal = pixScales[i]; if (Double.isInfinite(tempVal) || Double.isNaN(tempVal)) 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() { final IIOMetadataNode noDataNode = getTiffField(GeoTiffConstants.TIFFTAG_NODATA); if (noDataNode == null) return false; final String noData = getTiffAscii(noDataNode); if (noData == null || noData.trim().length() == 0) return false; return true; } /** * 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() { final IIOMetadataNode node = getTiffField(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() { final IIOMetadataNode node = getTiffField(GeoTIFFTagSet.TAG_MODEL_TRANSFORMATION); if (node == null) return false; final double[] modelTransformation = getTiffDoubles(node); if (modelTransformation == null) return false; return true; } // 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(final int tag) { if (tag == GeoTIFFTagSet.TAG_GEO_ASCII_PARAMS && this.geoKeyAsciiParams != null) return this.geoKeyAsciiParams; if (tag == GeoTIFFTagSet.TAG_GEO_DOUBLE_PARAMS && this.geoKeyDoubleParams != null) return this.geoKeyDoubleParams; if (tag == GeoTIFFTagSet.TAG_GEO_KEY_DIRECTORY && this.geoKeyDir != null) return this.geoKeyDir; // embed the exit condition in the for loop Node child = null; Node number = null; for (int i = 0; i < numTiffTasEntries; i++) { // search through all the TIFF fields to find the one with the // given tag value child = tiffTagsEntries.item(i); number = child.getAttributes().getNamedItem(GeoTiffConstants.NUMBER_ATTRIBUTE); if (number != null) { if (tag == Integer.parseInt(number.getNodeValue())) return (IIOMetadataNode) child; } } 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 rootNode; } } // end of class GeoTiffIIOMetadataDecoder