// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/shape/ShapeFile.java,v $
// $RCSfile: ShapeFile.java,v $
// $Revision: 1.4 $
// $Date: 2005/08/09 18:48:03 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.layer.shape;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Vector;
import com.bbn.openmap.dataAccess.shape.ShapeUtils;
import com.bbn.openmap.util.Debug;
/**
* Class representing an ESRI Shape File.
* <p>
* <pre>
* Usage:
*
* To verify a shape file:
* java com.bbn.openmap.layer.shape.ShapeFile -v shapeFile
*
* Append records from srcShapeFile to destShapeFile:
* java com.bbn.openmap.layer.shape.ShapeFile -a destShapeFile srcShapeFile
*
* Print information about the header and the number of records:
* java com.bbn.openmap.layer.shape.ShapeFile shapeFile
*
* </pre>
*
* @author Tom Mitchell
* @author Ray Tomlinson
* @author Geoffrey Knauth
* @version $Revision: 1.4 $ $Date: 2005/08/09 18:48:03 $
*/
public class ShapeFile
extends ShapeUtils {
/** A Shape File's magic number. */
public static final int SHAPE_FILE_CODE = 9994;
/** The currently handled version of Shape Files. */
public static final int SHAPE_FILE_VERSION = 1000;
/** A default record size. Automatically increased on demand. */
public static final int DEFAULT_RECORD_BUFFER_SIZE = 50000;
/** The read/write class for shape files. */
protected RandomAccessFile raf;
/** The buffer that holds the 100 byte header. */
protected byte header[];
/** Holds the length of the file, in bytes. */
protected long fileLength;
/** Holds the version of the file, as an int. */
protected int fileVersion;
/** Holds the shape type of the file. */
protected int fileShapeType;
/** Holds the bounds of the file (four doubles). */
protected ESRIBoundingBox fileBounds;
/** A buffer for current record's header. */
protected byte recHdr[];
/** A buffer for the current record's data. */
protected byte recBuf[];
/**
* Construct a <code>ShapeFile</code> from a file name.
*
* @exception IOException if something goes wrong opening or reading the
* file.
*/
public ShapeFile(String name)
throws IOException {
raf = new RandomAccessFile(name, "rw");
recHdr = new byte[ShapeUtils.SHAPE_FILE_RECORD_HEADER_LENGTH];
recBuf = new byte[DEFAULT_RECORD_BUFFER_SIZE];
initHeader();
}
/**
* Construct a <code>ShapeFile</code> from the given <code>File</code>.
*
* @param file A file object representing an ESRI Shape File
*
* @exception IOException if something goes wrong opening or reading the
* file.
*/
public ShapeFile(File file)
throws IOException {
this(file.getPath());
}
/**
* Reads or writes the header of a Shape file. If the file is empty, a blank
* header is written and then read. If the file is not empty, the header is
* read.
* <p>
* After this function runs, the file pointer is set to byte 100, the first
* byte of the first record in the file.
*
* @exception IOException if something goes wrong reading or writing the
* shape file
*/
protected void initHeader()
throws IOException {
int result = raf.read();
if (result == -1) {
// File is empty, write a new header into the file
writeHeader();
}
readHeader();
}
/**
* Writes a blank header into the shape file.
*
* @exception IOException if something goes wrong writing the shape file
*/
protected void writeHeader()
throws IOException {
header = new byte[SHAPE_FILE_HEADER_LENGTH];
writeBEInt(header, 0, SHAPE_FILE_CODE);
writeBEInt(header, 24, 50); // empty shape file size in 16 bit
// words
writeLEInt(header, 28, SHAPE_FILE_VERSION);
writeLEInt(header, 32, SHAPE_TYPE_NULL);
writeLEDouble(header, 36, 0.0);
writeLEDouble(header, 44, 0.0);
writeLEDouble(header, 52, 0.0);
writeLEDouble(header, 60, 0.0);
raf.seek(0);
raf.write(header, 0, SHAPE_FILE_HEADER_LENGTH);
}
/**
* Reads and parses the header of the file. Values from the header are stored
* in the fields of this class.
*
* @exception IOException if something goes wrong reading the file
* @see #header
* @see #fileVersion
* @see #fileLength
* @see #fileShapeType
* @see #fileBounds
*/
protected void readHeader()
throws IOException {
header = new byte[ShapeUtils.SHAPE_FILE_HEADER_LENGTH];
raf.seek(0); // Make sure we're at the beginning of
// the file
raf.read(header, 0, ShapeUtils.SHAPE_FILE_HEADER_LENGTH);
int fileCode = ShapeUtils.readBEInt(header, 0);
if (fileCode != SHAPE_FILE_CODE) {
throw new IOException("Invalid file code, " + "probably not a shape file");
}
fileVersion = ShapeUtils.readLEInt(header, 28);
if (fileVersion != SHAPE_FILE_VERSION) {
throw new IOException("Unable to read shape files with version " + fileVersion);
}
fileLength = ShapeUtils.readBEInt(header, 24);
fileLength *= 2; // convert from 16-bit words to 8-bit
// bytes
fileShapeType = ShapeUtils.readLEInt(header, 32);
fileBounds = ShapeUtils.readBox(header, 36);
}
/**
* Returns the length of the file in bytes.
*
* @return the file length
*/
public long getFileLength() {
return fileLength;
}
/**
* Returns the version of the file. The only currently supported version is
* 1000 (which represents version 1).
*
* @return the file version
*/
public int getFileVersion() {
return fileVersion;
}
/**
* Returns the shape type of the file. Shape files do not mix shape types;
* all the shapes are of the same type.
*
* @return the file's shape type
*/
public int getShapeType() {
return fileShapeType;
}
/**
* Sets the shape type of the file. If the file has a shape type already, it
* cannot be set. If it does not have a shape type, it is set and written to
* the file in the header.
* <p>
* Shape types are enumerated in the class ShapeUtils.
*
* @param newShapeType the new shape type
* @exception IOException if something goes wrong writing the file
* @exception IllegalArgumentException if file already has a shape type
* @see ShapeUtils
*/
public void setShapeType(int newShapeType)
throws IOException, IllegalArgumentException {
if (fileShapeType == SHAPE_TYPE_NULL) {
fileShapeType = newShapeType;
long filePtr = raf.getFilePointer();
writeLEInt(header, 32, fileShapeType);
raf.seek(0);
raf.write(header, 0, 100);
raf.seek(filePtr);
} else {
throw new IllegalArgumentException("file already has a valid" + " shape type: " + fileShapeType);
}
}
/**
* Returns the bounding box of this shape file. The bounding box is the
* smallest rectangle that encloses all the shapes in the file.
*
* @return the bounding box
*/
public ESRIBoundingBox getBoundingBox() {
return fileBounds;
}
/**
* Returns the next record from the shape file as an <code>ESRIRecord</code>.
* Each successive call gets the next record. There is no way to go back a
* record. When there are no more records, <code>null</code> is returned.
*
* @return a record, or null if there are no more records
* @exception IOException if something goes wrong reading the file
*/
public ESRIRecord getNextRecord()
throws IOException {
// Debug.output("getNextRecord: ptr = " +
// raf.getFilePointer());
int result = raf.read(recHdr, 0, ShapeUtils.SHAPE_FILE_RECORD_HEADER_LENGTH);
if (result == -1) { // EOF
// Debug.output("getNextRecord: EOF");
return null;
}
int contentLength = ShapeUtils.readBEInt(recHdr, 4);
int bytesToRead = contentLength * 2;
int fullRecordSize = bytesToRead + 8;
if (recBuf.length < fullRecordSize) {
if (Debug.debugging("shape")) {
Debug.output("record size: " + fullRecordSize);
}
recBuf = new byte[fullRecordSize];
}
System.arraycopy(recHdr, 0, recBuf, 0, ShapeUtils.SHAPE_FILE_RECORD_HEADER_LENGTH);
raf.read(recBuf, ShapeUtils.SHAPE_FILE_RECORD_HEADER_LENGTH, bytesToRead);
switch (fileShapeType) {
case ShapeUtils.SHAPE_TYPE_NULL:
throw new IOException("Can't parse NULL shape type");
case ShapeUtils.SHAPE_TYPE_POINT:
return new ESRIPointRecord(recBuf, 0);
case ShapeUtils.SHAPE_TYPE_ARC:
// case ShapeUtils.SHAPE_TYPE_POLYLINE:
return new ESRIPolygonRecord(recBuf, 0);
case ShapeUtils.SHAPE_TYPE_POLYGON:
return new ESRIPolygonRecord(recBuf, 0);
case ShapeUtils.SHAPE_TYPE_MULTIPOINT:
throw new IOException("Multipoint shape not yet implemented");
default:
throw new IOException("Unknown shape type: " + fileShapeType);
}
}
/**
* Adds a record to the end of this file. The record is written to the file
* at the end of the last record.
*
* @param r the record to be added
* @exception IOException if something goes wrong writing to the file
*/
public void add(ESRIRecord r)
throws IOException {
if (r.getShapeType() == fileShapeType) {
verifyRecordBuffer(r.getBinaryStoreSize());
int nBytes = r.write(recBuf, 0);
// long len = raf.length();
// Debug.output("seek to " + len);
raf.seek(raf.length());
raf.write(recBuf, 0, nBytes);
} else {
Debug.error("ShapeFile.add(): type=" + r.getShapeType() + " does not match file type=" + fileShapeType);
}
}
/**
* Closes the shape file and disposes of resources.
*
* @exception IOException if something goes wrong closing the file
*/
public void close()
throws IOException {
raf.close();
raf = null;
}
/**
* Verifies the contents of a shape file. The header is verified for file
* length, bounding box, and shape type. The records are verified for shape
* type and record number. The file is verified for proper termination (EOF
* at the end of a record).
*
* @param repair NOT CURRENTLY USED - would signal that the file should be
* repaired if possible
* @param verbose NOT CURRENTLY USED - would cause the verifier to display
* progress and status
* @exception IOException if something goes wrong reading or writing the file
*/
public void verify(boolean repair, boolean verbose)
throws IOException {
// Is file length stored in header correctly?
// Is file bounding box correct?
// Does file have a valid shape type?
// Is each record the correct shape type?
// Does each record header have the correct record number?
// Do we reach EOF at the end of a record?
boolean headerChanged = false;
long fLen = raf.length();
if (verbose) {
Debug.output("Checking file length...");
System.out.flush();
}
if (fileLength == fLen) {
if (verbose) {
Debug.output("correct.");
}
} else {
if (verbose) {
Debug.output("incorrect (got " + fileLength + ", should be " + fLen + ")");
}
if (repair) {
fileLength = fLen;
writeBEInt(header, 24, ((int) fLen / 2));
headerChanged = true;
if (verbose) {
Debug.output("...repaired.");
}
}
}
// loop through file to verify:
// record numbers
// Shape types
// bounding box
// correct EOF
raf.seek(100);
ESRIRecord r;
int nRecords = 0;
Vector<ESRIRecord> v = new Vector<ESRIRecord>();
ESRIBoundingBox bounds = new ESRIBoundingBox();
long recStart = raf.getFilePointer();
byte intBuf[] = new byte[4];
while ((r = getNextRecord()) != null) {
long recEnd = raf.getFilePointer();
// Debug.output("verify - start: " + recStart +
// "; end: " + recEnd);
nRecords++;
v.addElement(r);
if (r.getRecordNumber() != nRecords) {
// Debug.output("updating record number for record "
// + nRecords);
writeBEInt(intBuf, 0, nRecords);
raf.seek(recStart);
raf.write(intBuf, 0, 4);
raf.seek(recEnd);
}
if (fileShapeType == SHAPE_TYPE_NULL) {
Debug.output("updating shape type in header.");
fileShapeType = r.getShapeType();
writeLEInt(header, 32, fileShapeType);
headerChanged = true;
}
if (r.getShapeType() != fileShapeType) {
Debug.output("invalid shape type " + r.getShapeType() + ", expecting " + fileShapeType);
}
bounds.addBounds(r.getBoundingBox());
recStart = recEnd;
}
if (!fileBounds.equals(bounds)) {
Debug.output("adjusting bounds");
Debug.output("from min: " + fileBounds.min);
Debug.output("to min: " + bounds.min);
Debug.output("from max: " + fileBounds.max);
Debug.output("to max: " + bounds.max);
writeBox(header, 36, bounds);
headerChanged = true;
fileBounds = bounds;
}
if (headerChanged) {
Debug.output("writing changed header");
raf.seek(0);
raf.write(header, 0, 100);
}
}
/**
* Verifies that the record buffer is big enough to hold the given number of
* bytes. If it is not big enough a new buffer is created that can hold the
* given number of bytes.
*
* @param size the number of bytes the buffer needs to hold
*/
protected void verifyRecordBuffer(int size) {
if (recBuf.length < size) {
recBuf = new byte[size];
}
}
/**
* The driver for the command line interface. Reads the command line
* arguments and executes appropriate calls.
* <p>
* See the file documentation for usage.
*
* @param args the command line arguments
* @exception IOException if something goes wrong reading or writing the file
*/
public static void main(String args[])
throws IOException {
Debug.init(System.getProperties());
int argc = args.length;
if (argc == 1) {
ShapeFile sf = new ShapeFile(args[0]);
Debug.output("Shape file: " + args[0]);
Debug.output("version: " + sf.getFileVersion());
Debug.output("length: " + sf.getFileLength());
Debug.output("bounds:");
Debug.output("\tmin: " + sf.getBoundingBox().min);
Debug.output("\tmax: " + sf.getBoundingBox().max);
int nRecords = 0;
ESRIRecord record = sf.getNextRecord();
while (record != null) {
if (record instanceof ESRIPointRecord) {
double lat = ((ESRIPointRecord)record).getY();
double lon = ((ESRIPointRecord)record).getX();
Debug.output("record: " + lat + ", " + lon);
} else {
Debug.output("record: " + record.getClass().getName());
}
nRecords++;
record = sf.getNextRecord();
}
Debug.output("records: " + nRecords);
} else if ("-a".equals(args[0])) {
// Append a shape file to another shape file
String destFile = args[1];
String srcFile = args[2];
ShapeFile in = new ShapeFile(srcFile);
ShapeFile out = new ShapeFile(destFile);
if (in.getShapeType() != out.getShapeType()) {
try {
out.setShapeType(in.getShapeType());
} catch (IllegalArgumentException e) {
Debug.error("Incompatible shape types.");
System.exit(1);
}
}
ESRIRecord r;
while ((r = in.getNextRecord()) != null) {
out.add(r);
}
out.verify(true, true);
} else if ("-v".equals(args[0])) {
// Verify a shape file
String shpFile = args[1];
ShapeFile s = new ShapeFile(shpFile);
s.verify(true, true);
} else {
Debug.output("Usage:");
Debug.output("ShapeFile file.shp -- displays information about file.shp");
Debug.output("ShapeFile -a dest.shp src.shp -- appends records from src.shp to dest.shp");
Debug.output("ShapeFile -v file.shp -- verifies file.shp");
}
}
}