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