package nl.tudelft.simulation.dsol.animation.gis.esri; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.List; import org.djutils.draw.bounds.Bounds2d; import org.djutils.exceptions.Throw; import nl.tudelft.simulation.dsol.animation.gis.DataSourceInterface; import nl.tudelft.simulation.dsol.animation.gis.FeatureInterface; import nl.tudelft.simulation.dsol.animation.gis.GisMapInterface; import nl.tudelft.simulation.dsol.animation.gis.GisObject; import nl.tudelft.simulation.dsol.animation.gis.SerializablePath; import nl.tudelft.simulation.dsol.animation.gis.SerializableRectangle2D; import nl.tudelft.simulation.dsol.animation.gis.io.Endianness; import nl.tudelft.simulation.dsol.animation.gis.io.ObjectEndianInputStream; import nl.tudelft.simulation.dsol.animation.gis.transform.CoordinateTransform; import nl.tudelft.simulation.language.d2.Shape; /** * This class reads ESRI-shapefiles and returns the shape objects. *
* Copyright (c) 2020-2022 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See * for project information DSOL Manual. The DSOL * project is distributed under a three-clause BSD-style license, which can be found at * DSOL License. *
** The dsol-animation-gis project is based on the gisbeans project that has been part of DSOL since 2002, originally by Peter * Jacobs and Paul Jacobs. *
* @author Alexander Verbraeck */ public class ShapeFileReader implements DataSourceInterface { /** */ private static final long serialVersionUID = 20201223L; /** the URL for the shape file to be read. */ private URL shpFile = null; /** the URL for the shape index file to be read. */ private URL shxFile = null; /** the URL for the dbase-III format file with texts to be read. */ private URL dbfFile = null; /** the type of shape we are working on. */ private int currentType = GisMapInterface.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; /** number of shapes in the current file. */ private final int numShapes; /** an optional transformation of the lat/lon (or other) coordinates. */ private final CoordinateTransform coordinateTransform; /** the features to read by this OpenStreeetMap reader. */ private final Listnl.tudelft.simulation.dsol.animation.gis.GisObject
* @throws IOException on file IO or database connection failure
*/
public synchronized List* A shape type of 0 indicates a null shape, with no geometric data for the shape. Each feature type (point, line, polygon, * etc.) supports nulls¾it is valid to have points and null points in the same shapefile. Often null shapes are place * holders; they are used during shapefile creation and are populated with geometric data soon after they are created. *
* @param input ObjectEndianInputStream; the inputStream * @return null to indicate this is not a valid shape */ private synchronized Object readNullShape(final ObjectEndianInputStream input) { return null; } /** * Read a Point. ** A point consists of a pair of double-precision coordinates in the order X,Y. *
* *
* All byte orders are Little Endian.
* Integer ShapeType // byte 0; Value 1 for Point
* Point
* {
* Double X // byte 4; X coordinate (8 bytes)
* Double Y // byte 12; Y coordinate (8 bytes)
* }
*
*
* @param input ObjectEndianInputStream; the inputStream
* @return Point2D.Double; the point
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPoint(final ObjectEndianInputStream input) throws IOException
{
this.currentType = GisMapInterface.POINT;
input.setEndianness(Endianness.LITTLE_ENDIAN);
double[] point = this.coordinateTransform.doubleTransform(input.readDouble(), input.readDouble());
return new Point2D.Double(point[0], point[1]);
}
/**
* Read a PolyLine.
* * A PolyLine is an ordered set of vertices that consists of one or more parts. A part is a connected sequence of two or * more points. Parts may or may not be connected to one another. Parts may or may not intersect one another. Because this * specification does not forbid consecutive points with identical coordinates, shapefile readers must handle such cases. On * the other hand, the degenerate, zero length parts that might result are not allowed. *
* *
* All byte orders are Little Endian.
* Integer ShapeType // byte 0; Value 8 for PolyLine
* PolyLine
* {
* Double[4] Box // byte 4; Bounding Box, consisting of {Xmin, Ymin, Xmax, Ymax} (32 bytes)
* Integer NumParts // byte 36; Number of Parts in the PolyLine (4 bytes)
* Integer NumPoints // byte 40; Total Number of Points, summed for all parts (4 bytes)
* Integer[NumParts] Parts // byte 44; Index array to first point in in the points array (4 * NumParts)
* Point[NumPoints] Points // Points for all parts; no delimiter between points of different parts (16 * NumPoints)
* }
*
* where a point consists of a pair of double-precision coordinates in the order X,Y.
* Point
* {
* Double X // X coordinate (8 bytes)
* Double Y // Y coordinate (8 bytes)
* }
*
*
* @param input ObjectEndianInputStream; the inputStream
* @param skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the shape as a SerializablePath
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolyLine(final ObjectEndianInputStream input, final boolean skipBoundingBox)
throws IOException
{
this.currentType = GisMapInterface.LINE;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolygon(final ObjectEndianInputStream input, final boolean skipBoundingBox)
throws IOException
{
this.currentType = GisMapInterface.POLYGON;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readMultiPoint(final ObjectEndianInputStream input, final boolean skipBoundingBox)
throws IOException
{
this.currentType = GisMapInterface.POINT;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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 file IO or database connection failure
*/
private synchronized Object readPointZ(final ObjectEndianInputStream input, final int contentLength) throws IOException
{
this.currentType = GisMapInterface.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolyLineZ(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.LINE;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolygonZ(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.POLYGON;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readMultiPointZ(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.POINT;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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 file IO or database connection failure
*/
private synchronized Object readPointM(final ObjectEndianInputStream input, final int contentLength) throws IOException
{
this.currentType = GisMapInterface.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolyLineM(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.LINE;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readPolygonM(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.POLYGON;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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;
SerializablePath result = new SerializablePath(Path2D.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 skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readMultiPointM(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
this.currentType = GisMapInterface.POINT;
if (skipBoundingBox)
{
input.skipBytes(32);
}
input.setEndianness(Endianness.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 readMultiPatch.
* @param input ObjectEndianInputStream; the inputStream
* @param contentLength int; the contentLength
* @param skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
* @return the java2D PointShape
* @throws IOException on file IO or database connection failure
*/
private synchronized Object readMultiPatch(final ObjectEndianInputStream input, final int contentLength,
final boolean skipBoundingBox) throws IOException
{
if (input != null || contentLength != 0 || skipBoundingBox)
{
throw new IOException(
"Please inform support@javel.nl that you need MultiPatch support");
}
return null;
}
/** {@inheritDoc} */
@Override
public boolean isDynamic()
{
return false; // OSM data is static
}
/**
* Return the key names of the attribute data. The attribute values are stored in the GisObject together with the shape.
* @return String[]; the key names of the attribute data
*/
public String[] getAttributeKeyNames()
{
return this.dbfReader.getColumnNames();
}
/** {@inheritDoc} */
@Override
public URL getURL()
{
return this.shpFile;
}
}