/*
* Shapefile.java
*
* Created on October 11, 2002 Last edited on October 11, 2002
*/
package nl.javel.gisbeans.io.esri;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.djutils.logger.CategoryLogger;
import nl.javel.gisbeans.geom.GisObject;
import nl.javel.gisbeans.geom.SerializableGeneralPath;
import nl.javel.gisbeans.geom.SerializableRectangle2D;
import nl.javel.gisbeans.io.DataSourceInterface;
import nl.javel.gisbeans.io.EndianInterface;
import nl.javel.gisbeans.io.ObjectEndianInputStream;
import nl.javel.gisbeans.map.MapInterface;
import nl.tudelft.simulation.language.d2.Shape;
/**
* This class reads ESRI-shapefiles and returns the shape object
* @author Peter Jacobs
* Paul Jacobs
* @since JDK 1.2
*/
public class ShapeFile implements DataSourceInterface
{
/** the URLS */
private URL shpFile = null, shxFile = null, dbfFile = null;
/** the number of shapes. */
private int numShapes = 0;
/** the type. */
private int type = MapInterface.POLYGON;
/** our DBF reader. */
private DbfReader dbfReader;
/** the NULLSHAPE as defined by ESRI */
public static final int NULLSHAPE = 0;
/** the POINT as defined by ESRI */
public static final int POINT = 1;
/** the POLYLINE as defined by ESRI */
public static final int POLYLINE = 3;
/** the POLYGON as defined by ESRI */
public static final int POLYGON = 5;
/** the MULTIPOINT as defined by ESRI */
public static final int MULTIPOINT = 8;
/** the POINTZ as defined by ESRI */
public static final int POINTZ = 11;
/** the POLYLINEZ as defined by ESRI */
public static final int POLYLINEZ = 13;
/** the POLYGONZ as defined by ESRI */
public static final int POLYGONZ = 15;
/** the MULTIPOINTZ as defined by ESRI */
public static final int MULTIPOINTZ = 18;
/** the POINM as defined by ESRI */
public static final int POINTM = 21;
/** the POLYLINEM as defined by ESRI */
public static final int POLYLINEM = 23;
/** the POLYGONM as defined by ESRI */
public static final int POLYGONM = 25;
/** the MULTIPOINTM as defined by ESRI */
public static final int MULTIPOINTM = 28;
/** the MULTIPATCH as defined by ESRI */
public static final int MULTIPATCH = 31;
/** may we cache parsed data?.. */
private boolean cache = true;
/** the cachedContent. */
private ArrayList cachedContent = null;
/** an optional transformation of the lat/lon (or other) coordinates. */
private final CoordinateTransform coordinateTransform;
/**
* constructs a new ESRI ShapeFile.
* @param url java.net.URL; URL may or may not end with their extension.
* @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
* @throws IOException throws an IOException if the shxFile is not accessable
*/
public ShapeFile(final java.net.URL url, final CoordinateTransform coordinateTransform) throws IOException
{
this.coordinateTransform = coordinateTransform;
String fileName = url.toString();
if (fileName.endsWith(".shp") || fileName.endsWith(".shx") || fileName.endsWith(".dbf"))
{
fileName = fileName.substring(0, fileName.length() - 4);
}
this.shpFile = new URL(fileName + ".shp");
this.shxFile = new URL(fileName + ".shx");
this.dbfFile = new URL(fileName + ".dbf");
try
{
URLConnection connection = this.shxFile.openConnection();
connection.connect();
this.numShapes = (connection.getContentLength() - 100) / 8;
this.dbfReader = new DbfReader(this.dbfFile);
}
catch (IOException exception)
{
throw new IOException("Can't read " + this.shxFile.toString());
}
}
/**
* @return Returns the cache.
*/
public boolean isCache()
{
return this.cache;
}
/**
* @param cache boolean; The cache to set.
*/
public void setCache(boolean cache)
{
this.cache = cache;
this.dbfReader.setCache(cache);
}
/** {@inheritDoc} */
@Override
public String[] getColumnNames()
{
return this.dbfReader.getColumnNames();
}
/** {@inheritDoc} */
@Override
public String[][] getAttributes() throws IOException
{
return this.dbfReader.getRows();
}
/** {@inheritDoc} */
@Override
public URL getDataSource()
{
return this.shpFile;
}
/** {@inheritDoc} */
@Override
public int getNumShapes()
{
return this.numShapes;
}
/**
* getter for a specific shape at a certain index point in shapefile.
* @param index int; the index of the shape
* @return Object shape
* @throws IOException on IOfailure
*/
public synchronized GisObject getShape(int index) throws IOException
{
if (index > this.numShapes || index < 0)
{
throw new IndexOutOfBoundsException("Index =" + index + " number of shapes in layer :" + this.numShapes);
}
// May we use the cache?
if (this.cache && this.cachedContent != null)
{
return (GisObject) this.cachedContent.get(index);
}
ObjectEndianInputStream indexInput = new ObjectEndianInputStream(this.shxFile.openStream());
indexInput.skipBytes(8 * index + 100);
int offset = 2 * indexInput.readInt();
indexInput.close();
ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());
shapeInput.skipBytes(offset);
Object shape = this.readShape(shapeInput);
shapeInput.close();
return new GisObject(shape, this.dbfReader.getRow(index));
}
/**
* getter for all shapes in a shapefile.
* @return HashMap (Object shape)
* @throws IOException on IOfailure
*/
public synchronized List getShapes() throws IOException
{
// May we use the cache?
if (this.cache && this.cachedContent != null)
{
return this.cachedContent;
}
ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());
shapeInput.skipBytes(100);
ArrayList results = new ArrayList(this.numShapes);
String[][] attributes = this.dbfReader.getRows();
for (int i = 0; i < this.numShapes; i++)
{
results.add(new GisObject(this.readShape(shapeInput), attributes[i]));
}
shapeInput.close();
// May we use the cache?
if (this.cache)
{
this.cachedContent = results;
}
return results;
}
/**
* getter for all shapes intersecting with a certain extent
* @param extent SerializableRectangle2D; the extent to get
* @return HashMap (Object shape)
* @throws IOException on IOfailure
*/
public synchronized List getShapes(SerializableRectangle2D extent) throws IOException
{
// May we use the cache?
if (this.cache)
{
if (this.cachedContent == null)
{
this.getShapes();
}
List result = new ArrayList();
for (Iterator i = this.cachedContent.iterator(); i.hasNext();)
{
GisObject shape = (GisObject) i.next();
if (shape.getShape() instanceof SerializableGeneralPath)
{
if (Shape.overlaps(extent, ((SerializableGeneralPath) shape.getShape()).getBounds2D()))
{
result.add(shape);
}
}
else if (shape.getShape() instanceof Point2D)
{
if (extent.contains((Point2D) shape.getShape()))
{
result.add(shape);
}
}
else
{
CategoryLogger.always().error("unknown shape in cached content " + shape);
}
}
return result;
}
ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());
shapeInput.skipBytes(100);
ArrayList results = new ArrayList();
String[][] attributes = this.dbfReader.getRows();
for (int i = 0; i < this.numShapes; i++)
{
shapeInput.setEncode(EndianInterface.BIG_ENDIAN);
int shapeNumber = shapeInput.readInt();
int contentLength = shapeInput.readInt();
shapeInput.setEncode(EndianInterface.LITTLE_ENDIAN);
int type = shapeInput.readInt();
if (type != 0 && type != 1 && type != 11 && type != 21)
{
double[] min = this.coordinateTransform.doubleTransform(shapeInput.readDouble(), shapeInput.readDouble());
double[] max = this.coordinateTransform.doubleTransform(shapeInput.readDouble(), shapeInput.readDouble());
double minX = Math.min(min[0], max[0]);
double minY = Math.min(min[1], max[1]);
double width = Math.max(min[0], max[0]) - minX;
double height = Math.max(min[1], max[1]) - minY;
SerializableRectangle2D bounds = new SerializableRectangle2D.Double(minX, minY, width, height);
if (Shape.overlaps(extent, bounds))
{
results.add(
new GisObject(this.readShape(shapeInput, shapeNumber, contentLength, type, false), attributes[i]));
}
else
{
shapeInput.skipBytes((2 * contentLength) - 36);
}
}
else if (type != 0)
{
Point2D temp = (Point2D) this.readShape(shapeInput, shapeNumber, contentLength, type, false);
if (extent.contains(temp))
{
results.add(new GisObject(temp, attributes[i]));
}
}
}
shapeInput.close();
return results;
}
/**
* getter for all shapes intersecting with a certain extent
* @param attribute String; the attribute
* @param columnName String; the name of the dbfColumn
* @throws IOException on IO exception
* @return the list of shapes
*/
public synchronized List getShapes(String attribute, String columnName) throws IOException
{
ArrayList result = new ArrayList();
int[] shapeNumbers = this.dbfReader.getRowNumbers(attribute, columnName);
for (int i = 0; i < shapeNumbers.length; i++)
{
result.add(this.getShape(i));
}
return result;
}
/**
* getter for the type
* @return int
*/
public int getType()
{
return this.type;
}
/**
* reads a shape
* @param input ObjectEndianInputStream; the inputStream
* @return the shape
* @throws IOException on IOException
*/
private Object readShape(ObjectEndianInputStream input) throws IOException
{
return readShape(input, -1, -1, -1, true);
}
/**
* @param input ObjectEndianInputStream; the inputstream
* @param shapeNumber int; the number
* @param contentLength int; the length of the content
* @param type int;
* @param skipBox boolean;
* @return the shape
* @throws IOException
*/
private Object readShape(ObjectEndianInputStream input, int shapeNumber, int contentLength, int type, boolean skipBox)
throws IOException
{
input.setEncode(EndianInterface.BIG_ENDIAN);
if (shapeNumber == -1)
shapeNumber = input.readInt();
if (contentLength == -1)
contentLength = input.readInt();
input.setEncode(EndianInterface.LITTLE_ENDIAN);
if (type == -1)
type = input.readInt();
switch (type)
{
case 0:
return readNullShape(input);
case 1:
return readPoint(input);
case 3:
return readPolyLine(input, skipBox);
case 5:
return readPolygon(input, skipBox);
case 8:
return readMultiPoint(input, skipBox);
case 11:
return readPointZ(input, contentLength);
case 13:
return readPolyLineZ(input, contentLength, skipBox);
case 15:
return readPolygonZ(input, contentLength, skipBox);
case 18:
return readMultiPointZ(input, contentLength, skipBox);
case 21:
return readPointM(input, contentLength);
case 23:
return readPolyLineM(input, contentLength, skipBox);
case 25:
return readPolygonM(input, contentLength, skipBox);
case 28:
return readMultiPointM(input, contentLength, skipBox);
case 31:
return readMultiPatch(input, contentLength, skipBox);
default:
throw new IOException("Unknown shape type or shape type not supported");
}
}
/**
* reads a nullshape
* @param input ObjectEndianInputStream; the inputStream
* @return a nullobject
* @throws IOException on IOException
*/
private synchronized Object readNullShape(ObjectEndianInputStream input) throws IOException
{
if (input != null)
throw new IOException("readNullShape inputStream is is not null");
return null;
}
/**
* reads a Point
* @param input ObjectEndianInputStream; the inputStream
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPoint(ObjectEndianInputStream input) throws IOException
{
this.type = MapInterface.POINT;
input.setEncode(EndianInterface.LITTLE_ENDIAN);
double[] point = this.coordinateTransform.doubleTransform(input.readDouble(), input.readDouble());
return new Point2D.Double(point[0], point[1]);
}
/**
* reads a PolyLine
* @param input ObjectEndianInputStream; the inputStream
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolyLine(ObjectEndianInputStream input, boolean skipBox) throws IOException
{
this.type = MapInterface.LINE;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
}
}
return result;
}
/**
* reads a Polygon
* @param input ObjectEndianInputStream; the inputStream
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolygon(ObjectEndianInputStream input, boolean skipBox) throws IOException
{
this.type = MapInterface.POLYGON;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
}
}
return result;
}
/**
* reads a readMultiPoint
* @param input ObjectEndianInputStream; the inputStream
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readMultiPoint(ObjectEndianInputStream input, boolean skipBox) throws IOException
{
this.type = MapInterface.POINT;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
Point2D[] result = new Point2D.Double[input.readInt()];
for (int i = 0; i < result.length; i++)
{
result[i] = (Point2D) readPoint(input);
}
return result;
}
/**
* reads a readPointZ
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPointZ(ObjectEndianInputStream input, int contentLength) throws IOException
{
this.type = MapInterface.POINT;
Object point = this.readPoint(input);
input.skipBytes((contentLength * 2) - 20);
return point;
}
/**
* reads a readPolyLineZ
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolyLineZ(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.LINE;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int byteCounter = 44;
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
byteCounter += 4;
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
byteCounter += 16;
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
byteCounter += 16;
}
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readPolygonZ
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolygonZ(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.POLYGON;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int byteCounter = 44;
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
byteCounter += 4;
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
byteCounter += 16;
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
byteCounter += 16;
}
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readMultiPointZ
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readMultiPointZ(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.POINT;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
Point2D[] result = new Point2D.Double[input.readInt()];
int byteCounter = 40;
for (int i = 0; i < result.length; i++)
{
result[i] = (Point2D) readPoint(input);
byteCounter += 16;
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readPointM
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPointM(ObjectEndianInputStream input, int contentLength) throws IOException
{
this.type = MapInterface.POINT;
Object point = this.readPoint(input);
input.skipBytes((contentLength * 2) - 20);
return point;
}
/**
* reads a readPolyLineM
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolyLineM(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.LINE;
if (skipBox == true)
{
input.skipBytes(32);
}
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int byteCounter = 44;
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
byteCounter += 4;
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
byteCounter += 16;
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
byteCounter += 16;
}
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readPolyLineM
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readPolygonM(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.POLYGON;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
int numParts = input.readInt();
int numPoints = input.readInt();
int byteCounter = 44;
int[] partBegin = new int[numParts + 1];
for (int i = 0; i < partBegin.length - 1; i++)
{
partBegin[i] = input.readInt();
byteCounter += 4;
}
partBegin[partBegin.length - 1] = numPoints;
SerializableGeneralPath result = new SerializableGeneralPath(GeneralPath.WIND_NON_ZERO, numPoints);
for (int i = 0; i < numParts; i++)
{
float[] mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.moveTo(mf[0], mf[1]);
byteCounter += 16;
for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
{
float[] lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
result.lineTo(lf[0], lf[1]);
byteCounter += 16;
}
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readMultiPointM
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readMultiPointM(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
this.type = MapInterface.POINT;
if (skipBox == true)
input.skipBytes(32);
input.setEncode(EndianInterface.LITTLE_ENDIAN);
Point2D[] result = new Point2D.Double[input.readInt()];
int byteCounter = 40;
for (int i = 0; i < result.length; i++)
{
result[i] = (Point2D) readPoint(input);
byteCounter += 16;
}
input.skipBytes((contentLength * 2) - byteCounter);
return result;
}
/**
* reads a readMultiPointM
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBox boolean; whether to skip the box
* @return the java2D PointShape
* @throws IOException on IOException
*/
private synchronized Object readMultiPatch(ObjectEndianInputStream input, int contentLength, boolean skipBox)
throws IOException
{
if (input != null || contentLength != 0 || skipBox != false)
{
throw new IOException(
"Please inform support@javel.nl that you need MultiPatch support");
}
return null;
}
}