ShapeFileReader.java

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.
 * <p>
 * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/dsol/manual/" target="_blank">DSOL Manual</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">DSOL License</a>.
 * </p>
 * <p>
 * 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.
 * </p>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 */
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 List<FeatureInterface> featuresToRead;

    /**
     * Construct a reader for an ESRI ShapeFile.
     * @param shapeUrl URL; URL may or may not end with their extension.
     * @param coordinateTransform CoordinateTransform; the transformation of (x, y) coordinates to (x', y') coordinates.
     * @param featuresToRead the features to read
     * @throws IOException throws an IOException if the shxFile is not accessible
     */
    public ShapeFileReader(final URL shapeUrl, final CoordinateTransform coordinateTransform,
            final List<FeatureInterface> featuresToRead) throws IOException
    {
        this.coordinateTransform = coordinateTransform;
        this.featuresToRead = featuresToRead;
        String fileName = shapeUrl.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());
        }
    }

    /** {@inheritDoc} */
    @Override
    public List<FeatureInterface> getFeatures()
    {
        return this.featuresToRead;
    }

    /** {@inheritDoc} */
    @Override
    public void populateShapes() throws IOException
    {
        Throw.when(this.featuresToRead.size() != 1, IOException.class,
                "Trying to read ESROI shapes, but number of features is not 1");
        List<GisObject> shapes = readAllShapes();
        this.featuresToRead.get(0).getShapes().clear();
        this.featuresToRead.get(0).getShapes().addAll(shapes);
    }

    /**
     * Read a particular shape directly from the shape file, without caching (the cache is stored at the Features).
     * @param index int; the index of the shape to read from the shape file, without using any caching
     * @return GisObject; the shape belonging to the index
     * @throws IOException when there is a problem reading the ESRI files.
     */
    public synchronized GisObject readShape(final int index) throws IOException
    {
        if (index > this.numShapes || index < 0)
        {
            throw new IndexOutOfBoundsException("Index =" + index + ", while number of shapes in layer :" + this.numShapes);
        }

        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));
    }

    /**
     * Read all shapes directly from the shape file, without caching (the cache is stored at the Features).
     * @return List&lt;GisObject&gt;; the shapes that are directly read from the shape file
     * @throws IOException when there is a problem reading the ESRI files.
     */
    public synchronized List<GisObject> readAllShapes() throws IOException
    {
        ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());

        shapeInput.skipBytes(100);
        ArrayList<GisObject> results = new ArrayList<>(this.numShapes);
        String[][] attributes = this.dbfReader.getRows();
        for (int i = 0; i < this.numShapes; i++)
        {
            Object shape = this.readShape(shapeInput);
            if (shape != null) // skip Null Shape type 0
            {
                results.add(new GisObject(shape, attributes[i]));
            }
        }
        shapeInput.close();
        return results;
    }

    /**
     * Read all shapes for a certain extent directly from the shape file, without caching (the cache is stored at the Features).
     * @param extent Bounds2d; the extent for which to read the shapes
     * @return List&lt;GisObject&gt;; the shapes for the given extent that are directly read from the shape file
     * @throws IOException when there is a problem reading the ESRI files.
     */
    public synchronized List<GisObject> readShapes(final Bounds2d extent) throws IOException
    {
        ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());

        shapeInput.skipBytes(100);
        ArrayList<GisObject> results = new ArrayList<>();

        String[][] attributes = this.dbfReader.getRows();
        for (int i = 0; i < this.numShapes; i++)
        {
            shapeInput.setEndianness(Endianness.BIG_ENDIAN);
            int shapeNumber = shapeInput.readInt();
            int contentLength = shapeInput.readInt();
            shapeInput.setEndianness(Endianness.LITTLE_ENDIAN);

            // the null type is properly skipped
            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.toRectangle2D(), 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.toRectangle2D().contains(temp))
                {
                    results.add(new GisObject(temp, attributes[i]));
                }
            }
        }
        shapeInput.close();
        return results;
    }

    /**
     * Return the shapes based on a particular value of the attributes.
     * @param attribute String; the value of the attribute
     * @param columnName String; the columnName
     * @return List the resulting ArrayList of <code>nl.tudelft.simulation.dsol.animation.gis.GisObject</code>
     * @throws IOException on file IO or database connection failure
     */
    public synchronized List<GisObject> getShapes(final String attribute, final String columnName) throws IOException
    {
        List<GisObject> result = new ArrayList<>();
        int[] shapeNumbers = this.dbfReader.getRowNumbers(attribute, columnName);
        for (int i = 0; i < shapeNumbers.length; i++)
        {
            result.add(this.readShape(i));
        }
        return result;
    }

    /**
     * Read a shape.
     * @param input ObjectEndianInputStream; the inputStream
     * @return Object; the shape
     * @throws IOException on file IO or database connection failure
     */
    private Object readShape(final ObjectEndianInputStream input) throws IOException
    {
        return readShape(input, -1, -1, -1, true);
    }

    /**
     * @param input ObjectEndianInputStream; the input stream.
     * @param fixedShapeNumber int; the shape number, if -1, read from input
     * @param fixedContentLength int; the length of the content, if -1, read from input
     * @param fixedType int; shape type; if -1, read from input
     * @param skipBoundingBox boolean; whether to skip the bytes of the bounding box because they have not yet been read
     * @return the shape
     * @throws IOException on I/O error reading from the shape file
     */
    private Object readShape(final ObjectEndianInputStream input, final int fixedShapeNumber, final int fixedContentLength,
            final int fixedType, final boolean skipBoundingBox) throws IOException
    {
        input.setEndianness(Endianness.BIG_ENDIAN);
        @SuppressWarnings("unused")
        int shapeNumber = fixedShapeNumber == -1 ? input.readInt() : fixedShapeNumber;

        int contentLength = fixedContentLength == -1 ? input.readInt() : fixedContentLength;

        input.setEndianness(Endianness.LITTLE_ENDIAN);
        int type = fixedType == -1 ? input.readInt() : fixedType;

        switch (type)
        {
            case 0:
                return readNullShape(input);
            case 1:
                return readPoint(input);
            case 3:
                return readPolyLine(input, skipBoundingBox);
            case 5:
                return readPolygon(input, skipBoundingBox);
            case 8:
                return readMultiPoint(input, skipBoundingBox);
            case 11:
                return readPointZ(input, contentLength);
            case 13:
                return readPolyLineZ(input, contentLength, skipBoundingBox);
            case 15:
                return readPolygonZ(input, contentLength, skipBoundingBox);
            case 18:
                return readMultiPointZ(input, contentLength, skipBoundingBox);
            case 21:
                return readPointM(input, contentLength);
            case 23:
                return readPolyLineM(input, contentLength, skipBoundingBox);
            case 25:
                return readPolygonM(input, contentLength, skipBoundingBox);
            case 28:
                return readMultiPointM(input, contentLength, skipBoundingBox);
            case 31:
                return readMultiPatch(input, contentLength, skipBoundingBox);
            default:
                throw new IOException("Unknown shape type or shape type not supported");
        }
    }

    /**
     * Read a Null Shape.
     * <p>
     * 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.
     * </p>
     * @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.
     * <p>
     * A point consists of a pair of double-precision coordinates in the order X,Y.
     * </p>
     * 
     * <pre>
     *   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)
     *   }
     * </pre>
     * 
     * @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.
     * <p>
     * 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.
     * </p>
     * 
     * <pre>
     *   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)
     *   }
     * </pre>
     * 
     * @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 <a href=\"mailto:support@javel.nl\">support@javel.nl</a> 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;
    }

}