/*
* DBFExporter.java
*
* Created on April 13, 2007, 2:18 PM
*
*/
package edu.oregonstate.cartography.geometryexport;
import edu.oregonstate.cartography.simplefeatures.Geometry;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* Exporter for the DBF file format.
*
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
*/
public class DBFExporter {
private static final int STRING_LENGTH = 64;
private static final int NUMBER_LENGTH = 20; // F n=1..20
private static final int NUMBER_DECIMALS = 8;
/**
* Creates a new instance of DBFExporter
*/
public DBFExporter() {
}
public void exportTable(OutputStream outputStream, Geometry[] geometries, String attributeName)
throws IOException {
try (BufferedOutputStream bos = new BufferedOutputStream(outputStream)) {
LittleEndianOutputStream dos = new LittleEndianOutputStream(bos);
// dbf flag
dos.write(0x03);
// current date
Calendar cal = GregorianCalendar.getInstance();
int year = cal.get(Calendar.YEAR); // 2002
int month = cal.get(Calendar.MONTH); // 0=Jan, 1=Feb, ...
int day = cal.get(Calendar.DAY_OF_MONTH); // 1...
dos.write(year - 1900);
dos.write(month);
dos.write(day);
// number of records
dos.writeInt(geometries.length);
// header size
int columnsCount = 1; // table.getColumnCount();
if (attributeName != null) {
columnsCount++;
}
dos.writeShort(32 + columnsCount * 32 + 1);
// record size
dos.writeShort(computeRecordSize(null));
// reserved value, fill with 0
dos.writeShort(0);
// transaction byte
dos.write(0);
// encription byte
dos.write(0);
// multi user environment use
for (int i = 0; i < 13; i++) {
dos.write(0);
}
// codepage / language driver
// ESRI shape files use code 0x57 to indicate that
// data is written in ANSI (whatever that means).
// http://www.esricanada.com/english/support/faqs/arcview/avfaq21.asp
dos.write(0x57);
// two reserved bytes
dos.writeShort(0);
this.writeFieldDescriptors(dos, attributeName);
// header record terminator
dos.write(0x0D);
this.writeRecords(dos, geometries, attributeName);
}
}
private int computeRecordSize(String attributeName) throws IOException {
int recordSize = 1; // 1 for deletion flag
int columnsCount = 1; // table.getColumnCount();
if (attributeName != null) {
columnsCount++;
}
for (int i = 0; i < columnsCount; i++) {
/*TableColumn tc = table.getColumn(i);
if (table.isStringColumn(i)) {
recordSize += STRING_LENGTH;
} else if (table.isDoubleColumn(i)) {
recordSize += NUMBER_LENGTH;
} else {
throw new IOException("DBF export: unsupported type");
}*/
recordSize += NUMBER_LENGTH;
}
return recordSize;
}
private void writeString(LittleEndianOutputStream dos, String str, int length)
throws IOException {
byte[] b = str.getBytes("ISO-8859-1");
dos.write(b, 0, Math.min(length, b.length));
for (int c = b.length; c < length; c++) {
dos.write(0);
}
}
private void writeFieldDescriptors(LittleEndianOutputStream dos, String attributeName)
throws IOException {
int columnsCount = 1; // table.getColumnCount();
if (attributeName != null) {
columnsCount++;
}
for (int i = 0; i < columnsCount; i++) {
//TableColumn tc = table.getColumn(i);
// write column title, 10 chars, plus terminating 0.
String title = "attr" + i; // tc.getHeaderValue().toString().trim();
this.writeString(dos, title, 10);
dos.write(0x0);
// write field type
/*if (table.isStringColumn(i)) {
dos.write('C');
dos.writeInt(0); // field address (ignored)
dos.write(STRING_LENGTH); // field length
dos.write(0); // decimal count not used
} else if (table.isDoubleColumn(i)) {*/
dos.write('F');
dos.writeInt(0); // field address (ignored)
dos.write(NUMBER_LENGTH); // field length
dos.write(NUMBER_DECIMALS); // decimal count
/* } else {
throw new IOException("DBF export: unsupported type");
}*/
// 14 reserved or unusued bytes
for (int c = 0; c < 14; c++) {
dos.write(0);
}
}
}
/**
* Convert a double value to a string with a specified width, number of
* digits after the decimal point, and preceeding white spaces.
*
* @param value The value to convert.
* @param width The total width of the string (= the number of characters).
* @param decimals The number of digits after the decimal point.
* @param leadingSpaces The number of leading spaces. Usually 0.
* @return The value converted to a string.
*/
public static String format(double value, int width, int decimals, int leadingSpaces) {
StringBuilder str = new StringBuilder(width);
width += 1 + leadingSpaces;
DecimalFormat formatter = new DecimalFormat();
formatter.setMaximumFractionDigits(decimals);
formatter.setMinimumFractionDigits(decimals);
String s = formatter.format(value); // format the number
int padding = Math.max(leadingSpaces, width - s.length()); // insert leadingSpaces
for (int k = 0; k < padding; k++) {
str.append(' ');
}
str.append(s);
return str.toString();
}
private void writeRecords(LittleEndianOutputStream dos, Geometry[] geometries, String attributeName)
throws IOException {
int rowsCount = geometries.length; // table.getRowCount();
for (int row = 0; row < rowsCount; row++) {
// write deleted flag
dos.write(' ');
String nbrStr = format(row, NUMBER_LENGTH, NUMBER_DECIMALS, 0);
this.writeString(dos, nbrStr, NUMBER_LENGTH);
Number attr = geometries[row].getAttribute(attributeName);
if (attr != null) {
nbrStr = format(attr.doubleValue(), NUMBER_LENGTH, NUMBER_DECIMALS, 0);
} else {
nbrStr = format(0, NUMBER_LENGTH, NUMBER_DECIMALS, 0);
}
this.writeString(dos, nbrStr, NUMBER_LENGTH);
}
}
}