/*
* ShapeGeometryImporter.java
*
* Created on June 9, 2006, 1:51 PM
*
*/
package edu.oregonstate.cartography.geometryimport;
import edu.oregonstate.cartography.simplefeatures.GeometryCollection;
import edu.oregonstate.cartography.simplefeatures.LineString;
import edu.oregonstate.cartography.simplefeatures.Point;
import java.io.*;
/**
* An importer for ESRI shape files. This importer only reads geometry from .shp
* files.
*
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
*/
public class ShapeGeometryImporter implements GeometryCollectionImporter {
/**
* Identifiers for different shape types.
*/
private static final int NULLSHAPE = 0;
private static final int POINT = 1;
private static final int POLYLINE = 3;
private static final int POLYGON = 5;
private static final int MULTIPOINT = 8;
private static final int POINTZ = 11;
private static final int POLYLINEZ = 13;
private static final int POLYGONZ = 15;
private static final int MULTIPOINTZ = 18;
private static final int POINTM = 21;
private static final int POLYLINEM = 23;
private static final int POLYGONM = 25;
private static final int MULTIPOINTM = 28;
private static final int MULTIPATCH = 31; // not supported yet
/**
* ESRI shapefile magic code at the beginning of the .shp file.
*/
private static final int FILE_CODE = 9994;
/**
* Creates a new instance of ShapeGeometryImporter
*/
public ShapeGeometryImporter() {
}
protected String findDataFile(String path) {
if (path == null || path.length() < 5) {
return null;
}
String dataFileExtension = this.getLowerCaseDataFileExtension();
String lowerCaseFilePath = path.toLowerCase();
if (lowerCaseFilePath.endsWith("." + dataFileExtension)) {
return path;
}
final boolean is_shp_sibling
= lowerCaseFilePath.endsWith(".dbf")
|| lowerCaseFilePath.endsWith(".prj")
|| lowerCaseFilePath.endsWith(".sbn")
|| lowerCaseFilePath.endsWith(".sbx")
|| lowerCaseFilePath.endsWith(".shx");
if (!is_shp_sibling) {
return null;
}
path = replaceExtension(path, dataFileExtension);
if (new File(path).exists()) {
return path;
}
path = replaceExtension(path, dataFileExtension.toUpperCase());
return new File(path).exists() ? path : null;
}
protected String getLowerCaseDataFileExtension() {
return "shp";
}
private String findSHXFilePath(String path) {
if (path == null || path.length() < 5) {
return null;
}
path = replaceExtension(path, "shx");
if (!new File(path).exists()) {
path = replaceExtension(path, "SHX");
}
return new File(path).exists() ? path : null;
}
/**
* Imports the geometry of a shapefile.
*
* @param path The path to the shapefile
* @return A GeometryCollection with all imported features.
* @throws IOException
*/
@Override
public GeometryCollection importData(String path) throws IOException {
MixedEndianDataInputStream is = null;
try {
path = this.findDataFile(path);
if (path == null) {
return null;
}
GeometryCollection geometryCollection = new GeometryCollection();
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
is = new MixedEndianDataInputStream(bis);
// magic code is 9994
if (is.readInt() != FILE_CODE) {
throw new IOException("File is not an ESRI Shape file.");
}
is.skipBytes(5 * 4);
int fileLength = is.readInt() * 2;
// read version and shape type
int version = is.readLittleEndianInt();
int shapeType = is.readLittleEndianInt();
// skip bounding box and four double values
is.skipBytes(8 * 8);
// Read all features stored in records. The shp file does not contain
// the number of records present in the file. The shx file can be
// used to extract this information.
// If the shx file cannot be found, -1 is returned.
final int recordCount = this.readRecordCountFromSHXFile(path);
final int[] recOffsets = readSHXFile(path);
// Read until all records specified in the shx file are
// imported or until the end of file is reached and an EOFException
// is thrown.
int currentRecord = 0;
int currentPos = 100;
try {
while (true) {
// move to beginning of record
is.skipBytes(recOffsets[currentRecord] - currentPos);
currentPos = recOffsets[currentRecord];
currentPos += readRecord(is, geometryCollection);
if (++currentRecord == recordCount) {
break;
}
}
} catch (EOFException e) {
// EOFException indicates that all records have been read.
}
return geometryCollection;
} finally {
if (is != null) {
is.close();
}
}
}
public String getImporterName() {
return "Shape Importer";
}
private int readRecord(MixedEndianDataInputStream is,
GeometryCollection geometryCollection)
throws IOException {
final int recordNumber = is.readInt();
final int contentLength = is.readInt() * 2;
// content is at least one int (i.e. the ShapeType)
if (contentLength < 4) {
throw new EOFException("Negative record length");
}
final int shapeType = is.readLittleEndianInt();
int recordBytesRead = 8 + 4; // header is 8 bytes, shapeType is 4 bytes
switch (shapeType) {
case NULLSHAPE:
break;
case POINT:
case POINTZ:
case POINTM:
recordBytesRead += readPoint(is, geometryCollection, recordNumber);
break;
case MULTIPOINT:
case MULTIPOINTZ:
case MULTIPOINTM:
recordBytesRead += readMultipoint(is, geometryCollection, recordNumber);
break;
case POLYLINE:
case POLYLINEZ:
case POLYLINEM:
recordBytesRead += readPolyline(is, geometryCollection, recordNumber);
break;
case POLYGON:
case POLYGONZ:
case POLYGONM:
recordBytesRead += readPolygon(is, geometryCollection, recordNumber);
break;
case MULTIPATCH:
throw new IOException("Multipatch Shape files are not supported.");
default:
throw new IOException("Shapefile contains unsupported "
+ "geometry type: " + shapeType);
}
return recordBytesRead;
}
private int readPoint(MixedEndianDataInputStream is,
GeometryCollection geometryCollection, int recordID)
throws IOException {
double x = is.readLittleEndianDouble();
double y = is.readLittleEndianDouble();
Point point = new Point(x, y);
geometryCollection.addGeometry(point);
return 2 * 8;
}
private int readMultipoint(MixedEndianDataInputStream is,
GeometryCollection geometryCollection, int recordID)
throws IOException {
is.skipBytes(4 * 8); // skip bounding box
final int numPoints = is.readLittleEndianInt();
for (int ptID = 0; ptID < numPoints; ptID++) {
readPoint(is, geometryCollection, recordID);
}
return 4 * 8 + 4 + numPoints * 2 * 8;
}
private int readPolyline(MixedEndianDataInputStream is,
GeometryCollection geometryCollection,
int recordID) throws IOException {
is.skipBytes(4 * 8); // skip bounding box
final int numParts = is.readLittleEndianInt();
final int numPoints = is.readLittleEndianInt();
// read indices into point array
int[] pointIds = new int[numParts];
for (int partID = 0; partID < numParts; partID++) {
pointIds[partID] = is.readLittleEndianInt();
}
// read point array
double[] x = new double[numPoints];
double[] y = new double[numPoints];
for (int ptID = 0; ptID < numPoints; ptID++) {
x[ptID] = is.readLittleEndianDouble();
y[ptID] = is.readLittleEndianDouble();
}
for (int partID = 0; partID < 1/* FIXME numParts*/; partID++) {
LineString line = new LineString();
geometryCollection.addGeometry(line);
int firstPtID = pointIds[partID];
int lastPtID = partID + 1 < numParts ? pointIds[partID + 1] : numPoints;
// part must have at least two points
if ((lastPtID - firstPtID) < 2) {
continue;
}
for (int ptID = firstPtID; ptID < lastPtID; ptID++) {
line.addPoint(new Point(x[ptID], y[ptID]));
}
}
return 4 * 8 + 4 + 4 + numParts * 4 + numPoints * 2 * 8;
}
private int readPolygon(MixedEndianDataInputStream is,
GeometryCollection geometryCollection,
int recordID) throws IOException {
is.skipBytes(4 * 8); // skip bounding box
final int numParts = is.readLittleEndianInt();
final int numPoints = is.readLittleEndianInt();
// read indices into point array
int[] pointIds = new int[numParts];
for (int partID = 0; partID < numParts; partID++) {
pointIds[partID] = is.readLittleEndianInt();
}
// read point array
double[] x = new double[numPoints];
double[] y = new double[numPoints];
for (int ptID = 0; ptID < numPoints; ptID++) {
x[ptID] = is.readLittleEndianDouble();
y[ptID] = is.readLittleEndianDouble();
}
LineString line = new LineString();
// add sections
for (int partID = 0; partID < numParts; partID++) {
int firstPtID = pointIds[partID];
int lastPtID = partID + 1 < numParts ? pointIds[partID + 1] : numPoints - 1;
// part must have at least two points
if ((lastPtID - firstPtID) < 2) {
continue;
}
for (int ptID = firstPtID + 1; ptID < lastPtID; ptID++) {
line.addPoint(new Point(x[ptID], y[ptID]));
}
// close polygon when there are more than 2 points
if (line.getNumPoints() > 2) {
// FIXME line.close();
}
}
geometryCollection.addGeometry(line);
return 4 * 8 + 4 + 4 + numParts * 4 + numPoints * 2 * 8;
}
/**
* Reads the number of records from the shx file.
*
* @param path The path of the data shape file.
* @return The number of records or -1 if the shx file cannot be found.
*/
private int readRecordCountFromSHXFile(String path) {
MixedEndianDataInputStream is = null;
try {
String shxPath = findSHXFilePath(path);
if (shxPath == null) {
return -1;
}
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(shxPath));
is = new MixedEndianDataInputStream(bis);
is.skipBytes(24);
final int fileLength = is.readInt() * 2;
final int recordsCount = (fileLength - 100) / 8;
return recordsCount;
} catch (java.io.IOException e) {
return -1;
} finally {
if (is != null) {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
}
private int[] readSHXFile(String path) {
MixedEndianDataInputStream is = null;
try {
String shxPath = findSHXFilePath(path);
if (shxPath == null) {
return null;
}
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(shxPath));
is = new MixedEndianDataInputStream(bis);
is.skipBytes(24);
final int fileLength = is.readInt() * 2;
final int recordsCount = (fileLength - 100) / 8;
is.skipBytes(72);
int[] recOffsets = new int[recordsCount];
for (int i = 0; i < recordsCount; i++) {
recOffsets[i] = is.readInt() * 2;
// skip length
int length = is.readInt() * 2;
//System.out.println("Shape " + i);
//System.out.println("Offset: " + recOffsets[i]);
//System.out.println("Content Length: " + length);
}
return recOffsets;
} catch (java.io.IOException e) {
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (java.io.IOException e) {
}
}
}
}
/**
* Change the extension of a file path. The extension is what follows the
* last dot '.' in the path. If no dot exists in the path, the passed
* extension is simply appended without replacing anything.
*
* @param filePath The path of the file with the extension to replace.
* @param newExtension The new extension for the file, e.g. "tif".
* @return A new path to a file. The file may not actually exist on the hard
* disk.
*/
public static String replaceExtension(String filePath, String newExtension) {
final int dotIndex = filePath.lastIndexOf('.');
if (dotIndex == -1) {
return filePath + "." + newExtension;
}
return filePath.substring(0, dotIndex + 1) + newExtension;
}
}