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.List;

import org.djutils.exceptions.Throw;

import nl.tudelft.simulation.dsol.animation.gis.DataSourceInterface;
import nl.tudelft.simulation.dsol.animation.gis.DoubleXY;
import nl.tudelft.simulation.dsol.animation.gis.FeatureInterface;
import nl.tudelft.simulation.dsol.animation.gis.FloatXY;
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;

/**
 * This class reads ESRI-shapefiles and returns the shape objects.
 * <p>
 * Copyright (c) 2020-2025 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://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://github.com/averbraeck">Alexander Verbraeck</a>
 */
public class ShapeFileReader implements DataSourceInterface
{
    /** 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;

    /** 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 may or may not end with their extension.
     * @param 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());
        }
    }

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

    @Override
    public void populateShapes() throws IOException
    {
        Throw.when(this.featuresToRead.size() != 1, IOException.class,
                "Trying to read ESRI shapes, but number of features is not 1");
        this.featuresToRead.get(0).clearShapes();
        readAllShapes(this.featuresToRead.get(0));
    }

    /**
     * Read a particular shape directly from the shape file, without caching (the cache is stored at the Features).
     * @param index the index of the shape to read from the shape file, without using any caching
     * @return the shape belonging to the index
     * @throws IOException when there is a problem reading the ESRI files.
     */
    public synchronized Object 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 shape;
    }

    /**
     * Read all shapes directly from the shape file, without caching (the cache is stored at the Features).
     * @param feature the feature to read the shapes for
     * @throws IOException when there is a problem reading the ESRI files.
     */
    public synchronized void readAllShapes(final FeatureInterface feature) throws IOException
    {
        ObjectEndianInputStream shapeInput = new ObjectEndianInputStream(this.shpFile.openStream());

        shapeInput.skipBytes(100);
        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
            {
                if (shape instanceof Path2D path2D)
                {
                    feature.addShape(path2D);
                    // TODO feature.addShapeAttributes(attributes[i]);
                }
                else if (shape instanceof Point2D point2D)
                {
                    feature.addPoint(point2D);
                    // TODO feature.addPointAttributes(attributes[i]);
                }
                else if (shape instanceof Point2D[] point2Darray)
                {
                    for (Point2D point : point2Darray)
                    {
                        feature.addPoint(point);
                        // TODO feature.addPointAttributes(attributes[i]); // same for all points
                    }
                }
            }
        }
        shapeInput.close();
    }

    // /**
    // * Read all shapes for a certain extent directly from the shape file, without caching (the cache is stored at the
    // Features).
    // * @param extent the extent for which to read the shapes
    // * @return 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)
    // {
    // DoubleXY min = this.coordinateTransform.doubleTransform(shapeInput.readDouble(), shapeInput.readDouble());
    // DoubleXY max = this.coordinateTransform.doubleTransform(shapeInput.readDouble(), shapeInput.readDouble());
    // double minX = Math.min(min.x(), max.x());
    // double minY = Math.min(min.y(), max.y());
    // double width = Math.max(min.x(), max.x()) - minX;
    // double height = Math.max(min.y(), max.y()) - 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 the value of the attribute
    // * @param columnName 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 the inputStream
     * @return 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 the input stream.
     * @param fixedShapeNumber the shape number, if -1, read from input
     * @param fixedContentLength the length of the content, if -1, read from input
     * @param fixedType shape type; if -1, read from input
     * @param skipBoundingBox 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 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 the inputStream
     * @return Point2D.Double; the point
     * @throws IOException on file IO or database connection failure
     */
    private synchronized Point2D.Float readPoint(final ObjectEndianInputStream input) throws IOException
    {
        input.setEndianness(Endianness.LITTLE_ENDIAN);
        DoubleXY point = this.coordinateTransform.doubleTransform(input.readDouble(), input.readDouble());
        return new Point2D.Float((float) point.x(), (float) point.y());
    }

    /**
     * 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 the inputStream
     * @param skipBoundingBox 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 Path2D.Float readPolyLine(final ObjectEndianInputStream input, final boolean skipBoundingBox)
            throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO);
        for (int i = 0; i < numParts; i++)
        {
            Path2D path = new Path2D.Float();
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            path.moveTo(mf.x(), mf.y());
            FloatXY lf = null;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                path.lineTo(lf.x(), lf.y());
            }
            if (mf.equals(lf))
                path.closePath();
            result.append(path, false);
        }
        return result;
    }

    /**
     * reads a Polygon.
     * @param input the inputStream
     * @param skipBoundingBox 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 Path2D.Float readPolygon(final ObjectEndianInputStream input, final boolean skipBoundingBox)
            throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO);
        for (int i = 0; i < numParts; i++)
        {
            Path2D path = new Path2D.Float();
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            path.moveTo(mf.x(), mf.y());
            FloatXY lf = null;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                path.lineTo(lf.x(), lf.y());
            }
            if (mf.equals(lf))
                path.closePath();
            result.append(path, false);
        }

        return result;
    }

    /**
     * reads a readMultiPoint.
     * @param input the inputStream
     * @param skipBoundingBox 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 Point2D[] readMultiPoint(final ObjectEndianInputStream input, final boolean skipBoundingBox)
            throws IOException
    {
        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 the inputStream
     * @param contentLength the contentLength
     * @return the java2D PointShape
     * @throws IOException on file IO or database connection failure
     */
    private synchronized Point2D.Float readPointZ(final ObjectEndianInputStream input, final int contentLength)
            throws IOException
    {
        Point2D.Float point = this.readPoint(input);
        input.skipBytes((contentLength * 2) - 20);

        return point;
    }

    /**
     * reads a readPolyLineZ.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Path2D.Float readPolyLineZ(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO, numPoints);
        for (int i = 0; i < numParts; i++)
        {
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            result.moveTo(mf.x(), mf.y());
            byteCounter += 16;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                FloatXY lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                result.lineTo(lf.x(), lf.y());
                byteCounter += 16;
            }
        }
        input.skipBytes((contentLength * 2) - byteCounter);

        return result;
    }

    /**
     * reads a readPolygonZ.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Path2D.Float readPolygonZ(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO, numPoints);
        for (int i = 0; i < numParts; i++)
        {
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            result.moveTo(mf.x(), mf.y());
            byteCounter += 16;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                FloatXY lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                result.lineTo(lf.x(), lf.y());
                byteCounter += 16;
            }
        }
        input.skipBytes((contentLength * 2) - byteCounter);

        return result;
    }

    /**
     * reads a readMultiPointZ.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Point2D[] readMultiPointZ(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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 the inputStream
     * @param contentLength the contentLength
     * @return the java2D PointShape
     * @throws IOException on file IO or database connection failure
     */
    private synchronized Point2D readPointM(final ObjectEndianInputStream input, final int contentLength) throws IOException
    {
        Point2D point = this.readPoint(input);
        input.skipBytes((contentLength * 2) - 20);
        return point;
    }

    /**
     * reads a readPolyLineM.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Path2D.Float readPolyLineM(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO, numPoints);
        for (int i = 0; i < numParts; i++)
        {
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            result.moveTo(mf.x(), mf.y());
            byteCounter += 16;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                FloatXY lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                result.lineTo(lf.x(), lf.y());
                byteCounter += 16;
            }
        }
        input.skipBytes((contentLength * 2) - byteCounter);
        return result;
    }

    /**
     * reads a readPolyLineM.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Path2D.Float readPolygonM(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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;

        Path2D.Float result = new Path2D.Float(Path2D.WIND_NON_ZERO, numPoints);
        for (int i = 0; i < numParts; i++)
        {
            FloatXY mf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
            result.moveTo(mf.x(), mf.y());
            byteCounter += 16;
            for (int ii = (partBegin[i] + 1); ii < partBegin[i + 1]; ii++)
            {
                FloatXY lf = this.coordinateTransform.floatTransform(input.readDouble(), input.readDouble());
                result.lineTo(lf.x(), lf.y());
                byteCounter += 16;
            }
        }
        input.skipBytes((contentLength * 2) - byteCounter);
        return result;
    }

    /**
     * reads a readMultiPointM.
     * @param input the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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 Point2D[] readMultiPointM(final ObjectEndianInputStream input, final int contentLength,
            final boolean skipBoundingBox) throws IOException
    {
        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 the inputStream
     * @param contentLength the contentLength
     * @param skipBoundingBox 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;
    }

    @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 the key names of the attribute data
     */
    public String[] getAttributeKeyNames()
    {
        return this.dbfReader.getColumnNames();
    }

    @Override
    public URL getURL()
    {
        return this.shpFile;
    }

}