All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ucar.nc2.geotiff.GeoTiff Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.nc2.geotiff;

import java.io.Closeable;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;

import ucar.nc2.constants.CDM;
import ucar.nc2.util.CompareNetcdf2;

/**
 * Low level read/write geotiff files.
 *
 * @author John Caron
 * @author Yuan Ho
 */
public class GeoTiff implements Closeable {
  static final private boolean showBytes = false, debugRead = false, debugReadGeoKey = false;
  static final private boolean showHeaderBytes = false;

  private String filename;
  private RandomAccessFile file;
  private FileChannel channel;
  private List tags = new ArrayList<>();
  private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
  private boolean readonly;

  /**
   * Constructor. Does not open or create the file.
   *
   * @param filename pathname of file
   */
  public GeoTiff(String filename) {
    this.filename = filename;
  }

  /**
   * Close the Geotiff file.
   *
   * @throws java.io.IOException on io error
   */
  public void close() throws IOException {
    if (channel != null) {
      if (!readonly) {
        channel.force(true);
        channel.truncate(nextOverflowData);
      }
      channel.close();
    }
    if (file != null)
      file.close();
  }

  /////////////////////////////////////////////////////////////////////////////
  // writing

  private int headerSize = 8;
  private int firstIFD = 0;
  private int lastIFD = 0;
  private int startOverflowData = 0;
  private int nextOverflowData = 0;

  void addTag(IFDEntry ifd) {
    tags.add(ifd);
  }

  List getTags() { return tags; }

  void deleteTag(IFDEntry ifd) {
    tags.remove(ifd);
  }

  void setTransform(double xStart, double yStart, double xInc, double yInc) {
    // tie the raster 0, 0 to xStart, yStart
    addTag(new IFDEntry(Tag.ModelTiepointTag, FieldType.DOUBLE).setValue(new double[]{0.0, 0.0, 0.0, xStart, yStart, 0.0}));

    // define the "affine transformation" : requires grid to be regular (!)
    addTag(new IFDEntry(Tag.ModelPixelScaleTag, FieldType.DOUBLE).setValue(new double[]{xInc, yInc, 0.0}));
  }

  private List geokeys = new ArrayList<>();

  void addGeoKey(GeoKey geokey) {
    geokeys.add(geokey);
  }

  private void writeGeoKeys() {
    if (geokeys.size() == 0) return;

    // count extras
    int extra_chars = 0;
    int extra_ints = 0;
    int extra_doubles = 0;
    for (GeoKey geokey : geokeys) {
      if (geokey.isDouble)
        extra_doubles += geokey.count();
      else if (geokey.isString)
        extra_chars += geokey.valueString().length() + 1;
      else if (geokey.count() > 1)
        extra_ints += geokey.count();
    }

    int n = (geokeys.size() + 1) * 4;
    int[] values = new int[n + extra_ints];
    double[] dvalues = new double[extra_doubles];
    char[] cvalues = new char[extra_chars];
    int icounter = n;
    int dcounter = 0;
    int ccounter = 0;

    values[0] = 1;
    values[1] = 1;
    values[2] = 0;
    values[3] = geokeys.size();
    int count = 4;
    for (GeoKey geokey : geokeys) {
      values[count++] = geokey.tagCode();

      if (geokey.isDouble) {
        values[count++] = Tag.GeoDoubleParamsTag.getCode(); // extra double values here
        values[count++] = geokey.count();
        values[count++] = dcounter;
        for (int k = 0; k < geokey.count(); k++)
          dvalues[dcounter++] = geokey.valueD(k);

      } else if (geokey.isString) {
        String s = geokey.valueString();
        values[count++] = Tag.GeoAsciiParamsTag.getCode(); // extra double values here
        values[count++] = s.length(); // dont include trailing 0 in the count
        values[count++] = ccounter;
        for (int k = 0; k < s.length(); k++)
          cvalues[ccounter++] = s.charAt(k);
        cvalues[ccounter++] = (char) 0;

      } else if (geokey.count() > 1) { // more than one int value
        values[count++] = Tag.GeoKeyDirectoryTag.getCode(); // extra int values here
        values[count++] = geokey.count();
        values[count++] = icounter;
        for (int k = 0; k < geokey.count(); k++)
          values[icounter++] = geokey.value(k);

      } else { // normal case of one int value
        values[count++] = 0;
        values[count++] = 1;
        values[count++] = geokey.value();
      }
    } // loop over geokeys

    addTag(new IFDEntry(Tag.GeoKeyDirectoryTag, FieldType.SHORT).setValue(values));
    if (extra_doubles > 0)
      addTag(new IFDEntry(Tag.GeoDoubleParamsTag, FieldType.DOUBLE).setValue(dvalues));
    if (extra_chars > 0)
      addTag(new IFDEntry(Tag.GeoAsciiParamsTag, FieldType.ASCII).setValue(new String(cvalues)));
  }

  int writeData(byte[] data, int imageNumber) throws IOException {
    if (file == null)
      init();

    if (imageNumber == 1)
      channel.position(headerSize);
    else
      channel.position(nextOverflowData);

    ByteBuffer buffer = ByteBuffer.wrap(data);
    channel.write(buffer);

    if (imageNumber == 1)
      firstIFD = headerSize + data.length;
    else
      firstIFD = data.length + nextOverflowData;

    return nextOverflowData;
  }

  int writeData(float[] data, int imageNumber) throws IOException {
    if (file == null)
      init();

    if (imageNumber == 1)
      channel.position(headerSize);
    else
      channel.position(nextOverflowData);

    // no way around making a copy
    ByteBuffer direct = ByteBuffer.allocateDirect(4 * data.length);
    FloatBuffer buffer = direct.asFloatBuffer();
    buffer.put(data);
    //buffer.flip();
    channel.write(direct);

    if (imageNumber == 1)
      firstIFD = headerSize + 4 * data.length;
    else
      firstIFD = 4 * data.length + nextOverflowData;

    return nextOverflowData;
  }

  void writeMetadata(int imageNumber) throws IOException {
    if (file == null)
      init();

    // geokeys all get added at once
    writeGeoKeys();

    // tags gotta be in order
    Collections.sort(tags);
    if (imageNumber == 1) {
      writeHeader(channel);
    } else {
      //now this is not the first image we need to fill the Offset of nextIFD
      channel.position(lastIFD);
      ByteBuffer buffer = ByteBuffer.allocate(4);
      if (debugRead)
        System.out.println("position before writing nextIFD= " + channel.position() + " IFD is " + firstIFD);
      buffer.putInt(firstIFD);
      buffer.flip();
      channel.write(buffer);
    }
    writeIFD(channel, firstIFD);
  }

  private int writeHeader(FileChannel channel) throws IOException {
    channel.position(0);

    ByteBuffer buffer = ByteBuffer.allocate(8);
    buffer.put((byte) 'M');
    buffer.put((byte) 'M');
    buffer.putShort((short) 42);
    buffer.putInt(firstIFD);

    buffer.flip();
    channel.write(buffer);

    return firstIFD;
  }

  public void initTags() throws IOException {
    tags = new ArrayList<>();
    geokeys = new ArrayList<>();
  }

  private void init() throws IOException {
    file = new RandomAccessFile(filename, "rw");
    channel = file.getChannel();
    if (debugRead) System.out.println("Opened file to write: '" + filename + "', size=" + channel.size());
    readonly = false;
  }

  private void writeIFD(FileChannel channel, int start) throws IOException {
    channel.position(start);

    ByteBuffer buffer = ByteBuffer.allocate(2);
    int n = tags.size();
    buffer.putShort((short) n);
    buffer.flip();
    channel.write(buffer);

    start += 2;
    startOverflowData = start + 12 * tags.size() + 4;
    nextOverflowData = startOverflowData;

    for (IFDEntry elem : tags) {
      writeIFDEntry(channel, elem, start);
      start += 12;
    }
    // firstIFD = startOverflowData;
    // position to where the "next IFD" goes
    channel.position(startOverflowData - 4);
    lastIFD = startOverflowData - 4;
    if (debugRead) System.out.println("pos before writing nextIFD= " + channel.position());
    buffer = ByteBuffer.allocate(4);
    buffer.putInt(0);
    buffer.flip();
    channel.write(buffer);
  }

  private void writeIFDEntry(FileChannel channel, IFDEntry ifd, int start) throws IOException {
    channel.position(start);
    ByteBuffer buffer = ByteBuffer.allocate(12);

    buffer.putShort((short) ifd.tag.getCode());
    buffer.putShort((short) ifd.type.code);
    buffer.putInt(ifd.count);

    int size = ifd.count * ifd.type.size;
    if (size <= 4) {
      int done = writeValues(buffer, ifd);
      for (int k = 0; k < 4 - done; k++) // fill out to 4 bytes
        buffer.put((byte) 0);
      buffer.flip();
      channel.write(buffer);

    } else { // write offset
      buffer.putInt(nextOverflowData);
      buffer.flip();
      channel.write(buffer);
      // write data
      channel.position(nextOverflowData);
      //System.out.println(" write offset = "+ifd.tag.getName());
      ByteBuffer vbuffer = ByteBuffer.allocate(size);
      writeValues(vbuffer, ifd);
      vbuffer.flip();
      channel.write(vbuffer);
      nextOverflowData += size;
    }
  }

  private int writeValues(ByteBuffer buffer, IFDEntry ifd) {
    int done = 0;

    if (ifd.type == FieldType.ASCII) {
      return writeSValue(buffer, ifd);

    } else if (ifd.type == FieldType.RATIONAL) {
      for (int i = 0; i < ifd.count * 2; i++)
        done += writeIntValue(buffer, ifd, ifd.value[i]);

    } else if (ifd.type == FieldType.FLOAT) {
      for (int i = 0; i < ifd.count; i++)
        buffer.putFloat((float) ifd.valueD[i]);
      done += ifd.count * 4;

    } else if (ifd.type == FieldType.DOUBLE) {
      for (int i = 0; i < ifd.count; i++)
        buffer.putDouble(ifd.valueD[i]);
      done += ifd.count * 8;

    } else {
      for (int i = 0; i < ifd.count; i++)
        done += writeIntValue(buffer, ifd, ifd.value[i]);
    }

    return done;
  }

  private int writeIntValue(ByteBuffer buffer, IFDEntry ifd, int v) {
    switch (ifd.type.code) {
      case 1:
        buffer.put((byte) v);
        return 1;
      case 3:
        buffer.putShort((short) v);
        return 2;
      case 4:
        buffer.putInt(v);
        return 4;
      case 5:
        buffer.putInt(v);
        return 4;
    }
    return 0;
  }

  private int writeSValue(ByteBuffer buffer, IFDEntry ifd) {
    buffer.put(ifd.valueS.getBytes(CDM.utf8Charset));
    int size = ifd.valueS.length();
    if ((size & 1) != 0) size++;  // check if odd
    return size;
  }

  /////////////////////////////////////////////////////////////////////////////
  // reading

  /**
   * Read the geotiff file, using the filename passed in the constructor.
   *
   * @throws IOException on read error
   */
  public void read() throws IOException {
    file = new RandomAccessFile(filename, "r");
    channel = file.getChannel();
    if (debugRead) System.out.println("Opened file to read:'" + filename + "', size=" + channel.size());
    readonly = true;

    int nextOffset = readHeader(channel);
    while (nextOffset > 0) {
      nextOffset = readIFD(channel, nextOffset);
      parseGeoInfo();
    }

    //parseGeoInfo();
  }

  IFDEntry findTag(Tag tag) {
    if (tag == null) return null;
    for (IFDEntry ifd : tags) {
      if (ifd.tag == tag)
        return ifd;
    }
    return null;
  }

  private int readHeader(FileChannel channel) throws IOException {
    channel.position(0);

    ByteBuffer buffer = ByteBuffer.allocate(8);
    int n = channel.read(buffer);
    assert n == 8;
    buffer.flip();
    if (showHeaderBytes) {
      printBytes(System.out, "header", buffer, 4);
      buffer.rewind();
    }

    byte b = buffer.get();
    if (b == 73)
      byteOrder = ByteOrder.LITTLE_ENDIAN;
    buffer.order(byteOrder);
    buffer.position(4);
    int firstIFD = buffer.getInt();
    if (debugRead) System.out.println(" firstIFD == " + firstIFD);

    return firstIFD;
  }

  private int readIFD(FileChannel channel, int start) throws IOException {
    channel.position(start);

    ByteBuffer buffer = ByteBuffer.allocate(2);
    buffer.order(byteOrder);

    int n = channel.read(buffer);
    assert n == 2;
    buffer.flip();
    if (showBytes) {
      printBytes(System.out, "IFD", buffer, 2);
      buffer.rewind();
    }
    short nentries = buffer.getShort();
    if (debugRead) System.out.println(" nentries = " + nentries);

    start += 2;
    for (int i = 0; i < nentries; i++) {
      IFDEntry ifd = readIFDEntry(channel, start);
      if (debugRead) System.out.println(i + " == " + ifd);

      tags.add(ifd);
      start += 12;
    }

    if (debugRead) System.out.println(" looking for nextIFD at pos == " + channel.position() + " start = " + start);
    channel.position(start);
    buffer = ByteBuffer.allocate(4);
    buffer.order(byteOrder);
    assert 4 == channel.read(buffer);
    buffer.flip();
    int nextIFD = buffer.getInt();
    if (debugRead) System.out.println(" nextIFD == " + nextIFD);
    return nextIFD;
  }

  private IFDEntry readIFDEntry(FileChannel channel, int start) throws IOException {
    if (debugRead) System.out.println("readIFDEntry starting position to " + start);

    channel.position(start);
    ByteBuffer buffer = ByteBuffer.allocate(12);
    buffer.order(byteOrder);
    int n = channel.read(buffer);
    assert n == 12;
    buffer.flip();
    if (showBytes) printBytes(System.out, "IFDEntry bytes", buffer, 12);

    IFDEntry ifd;
    buffer.position(0);
    int code = readUShortValue(buffer);
    Tag tag = Tag.get(code);
    if (tag == null) tag = new Tag(code);
    FieldType type = FieldType.get(readUShortValue(buffer));
    int count = buffer.getInt();

    ifd = new IFDEntry(tag, type, count);

    if (ifd.count * ifd.type.size <= 4) {
      readValues(buffer, ifd);
    } else {
      int offset = buffer.getInt();
      if (debugRead) System.out.println("position to " + offset);
      channel.position(offset);
      ByteBuffer vbuffer = ByteBuffer.allocate(ifd.count * ifd.type.size);
      vbuffer.order(byteOrder);
      assert ifd.count * ifd.type.size == channel.read(vbuffer);
      vbuffer.flip();
      readValues(vbuffer, ifd);
    }

    return ifd;
  }

  /*
     * Construct a GeoKey from an IFDEntry.
     * @param id GeoKey.Tag number
     * @param v value
     *
    GeoKey(int id, IFDEntry data, int vcount, int offset) {
      this.id = id;
      this.geoTag = GeoKey.Tag.get(id);
      this.count = vcount;

      if (data.type == FieldType.SHORT) {

        if (vcount == 1)
          geoValue = TagValue.get(geoTag, offset);
        else {
          value = new int[vcount];
          for (int i=0; i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy