/*
* Copyright 2011 by Mark Coletti, Keith Sullivan, Sean Luke, and
* George Mason University Mason University Licensed under the Academic
* Free License version 3.0
*
* See the file "LICENSE" for more information
*
* $Id$
*/
package sim.io.geo;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
import com.vividsolutions.jts.geom.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import sim.field.geo.GeomVectorField;
import sim.util.Bag;
import sim.util.geo.AttributeValue;
import sim.util.geo.MasonGeometry;
/**
* A native Java importer to read ERSI shapefile data into the GeomVectorField.
* We assume the input file follows the standard ESRI shapefile format.
*/
public class ShapeFileImporter
{
/** Not meant to be instantiated
*/
private ShapeFileImporter()
{
}
// Shape types included in ESRI Shapefiles. Not all of these are currently supported.
final static int NULL_SHAPE = 0;
final static int POINT = 1;
final static int POLYLINE = 3;
final static int POLYGON = 5;
final static int MULTIPOINT = 8;
final static int POINTZ = 11;
final static int POLYLINEZ = 13;
final static int POLYGONZ = 15;
final static int MULTIPOINTZ = 18;
final static int POINTM = 21;
final static int POLYLINEM = 23;
final static int POLYGONM = 25;
final static int MULTIPOINTM = 28;
final static int MULTIPATCH = 31;
public static boolean isSupported(int shapeType)
{
switch (shapeType)
{
case POINT:
case POLYLINE:
case POLYGON:
case POINTZ:
return true;
default:
return false; // no other types are currently supported
}
}
private static String typeToString(int shapeType)
{
switch (shapeType)
{
case NULL_SHAPE:
return "NULL_SHAPE";
case POINT:
return "POINT";
case POLYLINE:
return "POLYLINE";
case POLYGON:
return "POLYGON";
case MULTIPOINT:
return "MULTIPOINT";
case POINTZ:
return "POINTZ";
case POLYLINEZ:
return "POLYLINEZ";
case POLYGONZ:
return "POLYGONZ";
case MULTIPOINTZ:
return "MULTIPOINTZ";
case POINTM:
return "POINTM";
case POLYLINEM:
return "POLYLINEM";
case POLYGONM:
return "POLYGONM";
case MULTIPOINTM:
return "MULTIPOINTM";
case MULTIPATCH:
return "MULTIPATCH";
default:
return "UNKNOWN";
}
}
/** Create a polygon from an array of LinearRings.
*
* If there is only one ring the function will create and return a simple
* polygon. If there are multiple rings, the function checks to see if any
* of them are holes (which are in counter-clockwise order) and if so, it
* creates a polygon with holes. If there are no holes, it creates and
* returns a multi-part polygon.
*
*/
private static Geometry createPolygon(LinearRing[] parts)
{
GeometryFactory geomFactory = new GeometryFactory();
if (parts.length == 1)
{
return geomFactory.createPolygon(parts[0], null);
}
ArrayList<LinearRing> shells = new ArrayList<LinearRing>();
ArrayList<LinearRing> holes = new ArrayList<LinearRing>();
for (int i = 0; i < parts.length; i++)
{
if (CGAlgorithms.isCCW(parts[i].getCoordinates()))
{
holes.add(parts[i]);
} else
{
shells.add(parts[i]);
}
}
// This will contain any holes within a given polygon
LinearRing [] holesArray = null;
if (! holes.isEmpty())
{
holesArray = new LinearRing[holes.size()];
holes.toArray(holesArray);
}
if (shells.size() == 1)
{ // single polygon
// It's ok if holesArray is null
return geomFactory.createPolygon(shells.get(0), holesArray);
}
else
{ // mutipolygon
Polygon[] poly = new Polygon[shells.size()];
for (int i = 0; i < shells.size(); i++)
{
poly[i] = geomFactory.createPolygon(parts[i], holesArray);
}
return geomFactory.createMultiPolygon(poly);
}
}
/**
* Wrapper function which creates a new array of LinearRings and calls
* the other function.
*/
private static Geometry createPolygon(Geometry[] parts)
{
LinearRing[] rings = new LinearRing[parts.length];
for (int i = 0; i < parts.length; i++)
{
rings[i] = (LinearRing) parts[i];
}
return createPolygon(rings);
}
/** Populate field from the shape file given in fileName
*
* @param shpFile to be read from
* @param field to contain read in data
* @throws FileNotFoundException
*/
public static void read(final URL shpFile, GeomVectorField field) throws FileNotFoundException, IOException, Exception
{
read(shpFile, field, null, MasonGeometry.class);
}
/** Populate field from the shape file given in fileName
*
* @param shpFile to be read from
* @param field to contain read in data
* @param masked dictates the subset of attributes we want
* @throws FileNotFoundException
*/
public static void read(final URL shpFile, GeomVectorField field, final Bag masked) throws FileNotFoundException, IOException, Exception
{
read(shpFile, field, masked, MasonGeometry.class);
}
/** Populate field from the shape file given in fileName
*
* @param shpFile to be read from
* @param field to contain read in data
* @param masonGeometryClass allows us to over-ride the default MasonGeometry wrapper
* @throws FileNotFoundException
*/
public static void read(final URL shpFile, GeomVectorField field, Class<?> masonGeometryClass) throws FileNotFoundException, IOException, Exception
{
read(shpFile, field, null, masonGeometryClass);
}
/** Populate field from the shape file given in fileName
*
* @param shpFile to be read from
* @param field is GeomVectorField that will contain the ShapeFile's contents
* @param masked dictates the subset of attributes we want
* @param masonGeometryClass allows us to over-ride the default MasonGeometry wrapper
* @throws FileNotFoundException if unable to open shape file
* @throws IOException if problem reading files
*
*/
public static void read(final URL shpFile, GeomVectorField field, final Bag masked, Class<?> masonGeometryClass) throws FileNotFoundException, IOException, Exception
{
if (shpFile == null)
{
throw new IllegalArgumentException("shpFile is null; likely file not found");
}
if (! MasonGeometry.class.isAssignableFrom(masonGeometryClass))
{
throw new IllegalArgumentException("masonGeometryClass not a MasonGeometry class or subclass");
}
try
{
FileInputStream shpFileInputStream = new FileInputStream(shpFile.getFile());
if (shpFileInputStream == null)
{
throw new FileNotFoundException(shpFile.getFile());
}
FileChannel channel = shpFileInputStream.getChannel();
ByteBuffer byteBuf = channel.map(FileChannel.MapMode.READ_ONLY, 0, (int) channel.size());
channel.close();
// Database file name is same as shape file name, except with '.dbf' extension
String dbfFilename = shpFile.getFile().substring(0, shpFile.getFile().lastIndexOf('.')) + ".dbf";
FileInputStream dbFileInputStream = new FileInputStream(dbfFilename);
FileChannel dbChannel = dbFileInputStream.getChannel();
ByteBuffer dbBuffer = dbChannel.map(FileChannel.MapMode.READ_ONLY, 0, (int) dbChannel.size());
dbChannel.close();
dbBuffer.order(ByteOrder.LITTLE_ENDIAN);
int headerSize = dbBuffer.getShort(8);
int recordSize = dbBuffer.getShort(10);
int fieldCnt = (short) ((headerSize - 1) / 32 - 1);
// Corresponds to a dBase field directory entry
class FieldDirEntry
{
public String name;
public int fieldSize;
}
FieldDirEntry fields[] = new FieldDirEntry[fieldCnt];
RandomAccessFile inFile = new RandomAccessFile(dbfFilename, "r");
if (inFile == null)
{
throw new FileNotFoundException(dbfFilename);
}
inFile.seek(32);
byte c[] = new byte[32];
char type[] = new char[fieldCnt];
int length;
for (int i = 0; i < fieldCnt; i++)
{
inFile.readFully(c, 0, 11);
int j = 0;
for (j = 0; j < 12 && c[j] != 0; j++);
String name = new String(c, 0, j);
type[i] = (char) inFile.readByte();
fields[i] = new FieldDirEntry();
fields[i].name = name;
inFile.read(c, 0, 4); // data address
byte b = inFile.readByte();
if (b > 0)
{
length = (int) b;
} else
{
length = 256 + (int) b;
}
fields[i].fieldSize = length;
inFile.skipBytes(15);
}
inFile.seek(0);
inFile.skipBytes(headerSize);
GeometryFactory geomFactory = new GeometryFactory();
// advance to the first record
byteBuf.position(100);
while (byteBuf.hasRemaining())
{
// advance past two int: recordNumber and recordLength
byteBuf.position(byteBuf.position() + 8);
byteBuf.order(ByteOrder.LITTLE_ENDIAN);
int recordType = byteBuf.getInt();
if (!isSupported(recordType))
{
System.out.println("Error: ShapeFileImporter.ingest(...): ShapeType " + typeToString(recordType) + " not supported.");
return; // all shapes are the same type so don't bother reading any more
}
// Read the attributes
byte r[] = new byte[recordSize];
inFile.read(r);
int start1 = 1;
// Contains all the attribute values keyed by name that will eventually
// be copied over to a corresponding MasonGeometry wrapper.
Map<String, AttributeValue> attributes = new HashMap<String, AttributeValue>(fieldCnt);
//attributeInfo = new ArrayList<AttributeValue>();
for (int k = 0; k < fieldCnt; k++)
{
// It used to be that we'd just flag attributes not in
// the mask Bag as hidden; however, now we just don't
// bother adding it to the MasonGeometry. If the user
// really wanted that attribute, they'd have added it to
// the mask in the first place
// if (masked != null && ! masked.contains(fields[k].name))
// {
// fld.setHidden(true);
// } else
// {
// fld.setHidden(false);
// }
// If the user bothered specifying a mask and the current
// attribute, as indexed by 'k', is NOT in the mask, then
// merrily skip on to the next attribute
if (masked != null && ! masked.contains(fields[k].name))
{
// But before we skip, ensure that we wind the pointer
// to the start of the next attribute value.
start1 += fields[k].fieldSize;
continue;
}
String rawAttributeValue = new String(r, start1, fields[k].fieldSize);
rawAttributeValue = rawAttributeValue.trim();
AttributeValue attributeValue = new AttributeValue();
if ( rawAttributeValue.isEmpty() )
{
// If we've gotten no data for this, then just add the
// empty string.
attributeValue.setString(rawAttributeValue);
}
else if (type[k] == 'N') // Numeric
{
if (rawAttributeValue.length() == 0)
{
attributeValue.setString("0");
}
if (rawAttributeValue.indexOf('.') != -1)
{
attributeValue.setDouble(Double.valueOf(rawAttributeValue));
} else
{
attributeValue.setInteger(Integer.valueOf(rawAttributeValue));
}
} else if (type[k] == 'L') // Logical
{
attributeValue.setValue(Boolean.valueOf(rawAttributeValue));
} else if (type[k] == 'F') // Floating point
{
attributeValue.setValue(Double.valueOf(rawAttributeValue));
}
else
{
attributeValue.setString(rawAttributeValue);
}
attributes.put(fields[k].name, attributeValue);
start1 += fields[k].fieldSize;
}
// Read the shape
Geometry geom = null;
if (recordType == POINT)
{
Coordinate pt = new Coordinate(byteBuf.getDouble(), byteBuf.getDouble());
geom = geomFactory.createPoint(pt);
}
else if (recordType == POINTZ)
{
Coordinate pt = new Coordinate(byteBuf.getDouble(), byteBuf.getDouble(), byteBuf.getDouble());
// Skip over the "measure" which we don't use.
// Actually, this is an optional field that most don't
// implement these days, so no need to skip over that
// which doesn't exist.
// XXX (Is there a way to detect that the M field exists?)
// byteBuf.position(byteBuf.position() + 8);
geom = geomFactory.createPoint(pt);
} else if (recordType == POLYLINE || recordType == POLYGON)
{
// advance past four doubles: minX, minY, maxX, maxY
byteBuf.position(byteBuf.position() + 32);
int numParts = byteBuf.getInt();
int numPoints = byteBuf.getInt();
// get the array of part indices
int partIndicies[] = new int[numParts];
for (int i = 0; i < numParts; i++)
{
partIndicies[i] = byteBuf.getInt();
}
// get the array of points
Coordinate pointsArray[] = new Coordinate[numPoints];
for (int i = 0; i < numPoints; i++)
{
pointsArray[i] = new Coordinate(byteBuf.getDouble(), byteBuf.getDouble());
}
Geometry[] parts = new Geometry[numParts];
for (int i = 0; i < numParts; i++)
{
int start = partIndicies[i];
int end = numPoints;
if (i < numParts - 1)
{
end = partIndicies[i + 1];
}
int size = end - start;
Coordinate coords[] = new Coordinate[size];
for (int j = 0; j < size; j++)
{
coords[j] = new Coordinate(pointsArray[start + j]);
}
if (recordType == POLYLINE)
{
parts[i] = geomFactory.createLineString(coords);
} else
{
parts[i] = geomFactory.createLinearRing(coords);
}
}
if (recordType == POLYLINE)
{
LineString[] ls = new LineString[numParts];
for (int i = 0; i < numParts; i++)
{
ls[i] = (LineString) parts[i];
}
if (numParts == 1)
{
geom = parts[0];
} else
{
geom = geomFactory.createMultiLineString(ls);
}
} else // polygon
{
geom = createPolygon(parts);
}
} else
{
System.err.println("Unknown shape type in " + recordType);
}
if (geom != null)
{
// The user *may* have created their own MasonGeometry
// class, so use the given masonGeometry class; by
// default it's MasonGeometry.
MasonGeometry masonGeometry = (MasonGeometry) masonGeometryClass.newInstance();
masonGeometry.geometry = geom;
if (!attributes.isEmpty())
{
masonGeometry.addAttributes(attributes);
}
field.addGeometry(masonGeometry);
}
}
}
catch (IOException e)
{
System.out.println("Error in ShapeFileImporter!!");
System.out.println("SHP filename: " + shpFile);
// e.printStackTrace();
throw e;
}
}
}