WKBWriter.java

package org.cugos.wkg;

import org.cugos.wkg.WKB.Endian;
import org.cugos.wkg.WKB.GeometryType;
import org.cugos.wkg.WKB.GeometryTypeFlag;
import org.cugos.wkg.WKB.Type;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;

/**
 * A Well Known Binary Writer.
 * @author Jared Erickson
 */
public class WKBWriter implements Writer<byte[]> {

    /**
     * The WKB Type (WKB or EWKB)
     */
    private final Type wkbType;

    /**
     * Whether the byte order is big or little endian
     */
    private final Endian endian;

    /**
     * Create a WKBWriter with Type WKB and Big Endian byte order
     */
    public WKBWriter() {
        this(Type.WKB, Endian.Big);
    }

    /**
     * Create a WKBWriter
     * @param wkbType The WKB.Type standard
     * @param edian The WKB.Endian byte order
     */
    public WKBWriter(Type wkbType, Endian edian) {
        this.endian = edian;
        this.wkbType = wkbType;
    }

    /**
     * Write the Geometry to a hex String
     * @param geometry The Geometry
     * @return A WKB hex String
     */
    public String writeToHex(Geometry geometry) {
        return toHex(write(geometry));
    }

    /**
     * Write a Geometry to an array of bytes
     * @param g The Geometry
     * @return An array of bytes
     */
    @Override
    public byte[] write(Geometry g) {
        if (g instanceof Point) {
            return write((Point) g);
        } else if (g instanceof LineString) {
            return write((LineString) g);
        } else if (g instanceof LinearRing) {
            return write((LinearRing) g);
        } else if (g instanceof Triangle) {
            return write((Triangle) g);
        } else if (g instanceof Polygon) {
            return write((Polygon) g);
        } else if (g instanceof MultiPoint) {
            return write((MultiPoint) g);
        } else if (g instanceof MultiLineString) {
            return write((MultiLineString) g);
        } else if (g instanceof MultiPolygon) {
            return write((MultiPolygon) g);
        } else if (g instanceof GeometryCollection) {
            return write((GeometryCollection) g);
        } else if (g instanceof CircularString) {
            return write((CircularString) g);
        } else if (g instanceof CurvePolygon) {
            return write((CurvePolygon) g);
        } else if (g instanceof CompoundCurve) {
            return write((CompoundCurve) g);
        } else if (g instanceof MultiCurve) {
            return write((MultiCurve) g);
        } else if (g instanceof PolyHedralSurface) {
            return write((PolyHedralSurface) g);
        } else if (g instanceof MultiSurface) {
            return write((MultiSurface) g);
        } else if (g instanceof Tin) {
            return write((Tin) g);
        } else {
            throw new IllegalArgumentException("Unsupported Geometry! " + g.getClass().getName());
        }
    }

    @Override
    public String getName() {
        return "WKB";
    }

    /**
     * Calculate the number of bytes for a given Geometry.
     * @param g The Geometry
     * @return The number of bytes necessary for the given Geometry
     */
    protected int calculateNumberOfBytes(Geometry g) {
        if (g instanceof Point) {
            return calculateNumberOfBytes((Point) g);
        } else if (g instanceof LineString) {
            return calculateNumberOfBytes((LineString) g);
        } else if (g instanceof LinearRing) {
            return calculateNumberOfBytes((LinearRing) g);
        } else if (g instanceof Triangle) {
            return calculateNumberOfBytes((Triangle) g);
        } else if (g instanceof Polygon) {
            return calculateNumberOfBytes((Polygon) g);
        } else if (g instanceof MultiPoint) {
            return calculateNumberOfBytes((MultiPoint) g);
        } else if (g instanceof MultiLineString) {
            return calculateNumberOfBytes((MultiLineString) g);
        } else if (g instanceof MultiPolygon) {
            return calculateNumberOfBytes((MultiPolygon) g);
        } else if (g instanceof GeometryCollection) {
            return calculateNumberOfBytes((GeometryCollection) g);
        } else if (g instanceof CircularString) {
            return calculateNumberOfBytes((CircularString) g);
        } else if (g instanceof CompoundCurve) {
            return calculateNumberOfBytes((CompoundCurve) g);
        }  else if (g instanceof CurvePolygon) {
            return calculateNumberOfBytes((CurvePolygon) g);
        } else if (g instanceof MultiCurve) {
            return calculateNumberOfBytes((MultiCurve) g);
        } else if (g instanceof PolyHedralSurface) {
            return calculateNumberOfBytes((PolyHedralSurface) g);
        } else if (g instanceof MultiSurface) {
            return calculateNumberOfBytes((MultiSurface) g);
        } else if (g instanceof Tin) {
            return calculateNumberOfBytes((Tin) g);
        } else {
            throw new IllegalArgumentException("Unsupported Geometry! " + g.getClass().getName());
        }
    }

    /**
     * Write the Geometry into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param g The Geometry
     */
    protected void putGeometry(ByteBuffer buffer, Geometry g) {
        if (g instanceof Point) {
            putPoint(buffer, (Point) g);
        } else if (g instanceof LineString) {
            putLineString(buffer, (LineString) g);
        } else if (g instanceof LinearRing) {
            putLinearRing(buffer, (LinearRing) g);
        } else if (g instanceof Triangle) {
            putTriangle(buffer, (Triangle) g);
        } else if (g instanceof Polygon) {
            putPolygon(buffer, (Polygon) g);
        } else if (g instanceof MultiPoint) {
            putMultiPoint(buffer, (MultiPoint) g);
        } else if (g instanceof MultiLineString) {
            putMultiLineString(buffer, (MultiLineString) g);
        } else if (g instanceof MultiPolygon) {
            putMultiPolygon(buffer, (MultiPolygon) g);
        } else if (g instanceof GeometryCollection) {
            putGeometryCollection(buffer, (GeometryCollection) g);
        } else if (g instanceof CircularString) {
            putCircularString(buffer, (CircularString) g);
        } else if (g instanceof CompoundCurve) {
            putCompoundCurve(buffer, (CompoundCurve) g);
        }  else if (g instanceof CurvePolygon) {
            putCurvePolygon(buffer, (CurvePolygon) g);
        } else if (g instanceof MultiCurve) {
           putMultiCurve(buffer, (MultiCurve) g);
        } else if (g instanceof PolyHedralSurface) {
            putPolyHedralSurface(buffer, (PolyHedralSurface) g);
        } else if (g instanceof MultiSurface) {
            putMultiSurface(buffer, (MultiSurface) g);
        } else if (g instanceof Tin) {
            putTin(buffer, (Tin) g);
        } else {
            throw new IllegalArgumentException("Unsupported Geometry! " + g.getClass().getName());
        }
    }

    /**
     * Calculate the number of bytes for header including an optional SRID.
     * This includes the byte order and geometry type, so minimum of 5.
     * @param srid The optional SRID
     * @return
     */
    private int calculateNumberOfBytes(String srid) {
        int numberOfBytes = 1 + 4;
        if (wkbType == Type.EWKB && srid != null) {
            numberOfBytes += 4;
        }
        return numberOfBytes;
    }

    /**
     * Calculate the number of bytes for the given number of Coordinates with the given Dimension
     * @param numberOfCoordinates The number of Coordinates
     * @param dimension The Dimension of the Coordinates
     * @return The number of bytes
     */
    private int calculateNumberOfBytes(int numberOfCoordinates, Dimension dimension) {
        int multiplier = getMultiplier(dimension);
        return 8 * (numberOfCoordinates * multiplier);
    }

    /**
     * Get the multiplier for a Coordinate based on the Dimension.
     * @param dimension The Dimension
     * @return A multiplier
     */
    private int getMultiplier(Dimension dimension) {
        if (dimension == Dimension.Two) {
            return 2;
        } else if (dimension == Dimension.TwoMeasured || dimension == Dimension.Three) {
            return 3;
        } else if (dimension == Dimension.ThreeMeasured) {
            return 4;
        } else {
            return 2;
        }
    }

    /**
     * Write the byte order into the ByteBuffer and order the ByteBuffer
     * @param buffer The ByteBuffer
     */
    private void putByteOrder(ByteBuffer buffer) {
        if (endian == Endian.Big) {
            buffer.order(ByteOrder.BIG_ENDIAN);
        } else {
            buffer.order(ByteOrder.LITTLE_ENDIAN);
        }
        buffer.put((byte) endian.getValue());
    }

    /**
     * Write the geometry type into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param geometryType The WKB.GeometryType
     * @param dimension The Dimension
     * @param srid The SRID
     */
    private void putGeometryType(ByteBuffer buffer, GeometryType geometryType, Dimension dimension, String srid) {
        int b = geometryType.getValue();
        if (wkbType == Type.EWKB) {
            if (dimension == Dimension.Three || dimension == Dimension.ThreeMeasured) {
                b = b | GeometryTypeFlag.Z.getValue();
            }
            if (dimension == Dimension.TwoMeasured || dimension == Dimension.ThreeMeasured) {
                b = b | GeometryTypeFlag.M.getValue();
            }
            if (srid != null) {
                b = b | GeometryTypeFlag.SRID.getValue();
            }
        }
        buffer.putInt(b);
    }

    /**
     * Write an SRID into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param srid The SRID
     */
    private void putSrid(ByteBuffer buffer, String srid) {
        if (wkbType == Type.EWKB && srid != null) {
            buffer.putInt(Integer.parseInt(srid));
        }
    }

    /**
     * Write a Coordinate into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param coord The Coordinates
     */
    private void putCoordinate(ByteBuffer buffer, Coordinate coord) {
        Dimension dimension = coord.getDimension();
        buffer.putDouble(coord.getX());
        buffer.putDouble(coord.getY());
        if (dimension == Dimension.Three || dimension == Dimension.ThreeMeasured) {
            buffer.putDouble(coord.getZ());
        }
        if (dimension == Dimension.TwoMeasured || dimension == Dimension.ThreeMeasured) {
            buffer.putDouble(coord.getM());
        }
    }

    /**
     * Write a List of Coordinates into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param coords The List of Coordinates
     */
    private void putCoordinates(ByteBuffer buffer, List<Coordinate> coords) {
        buffer.putInt(coords.size());
        for(Coordinate coord : coords) {
            putCoordinate(buffer, coord);
        }
    }

    // Point

    /**
     * Write a Point to a hex String
     * @param point The Point
     * @return A hex String
     */
    public String writeToHex(Point point) {
        return toHex(write(point));
    }

    /**
     * Write a Point to a byte array
     * @param point The Point
     * @return A byte array
     */
    public byte[] write(Point point) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(point));
        putPoint(buffer, point);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the Point
     * @param point The Point
     * @return The number of bytes necessary for the Point
     */
    private int calculateNumberOfBytes(Point point) {
        return calculateNumberOfBytes(point.getSrid()) +
                calculateNumberOfBytes(point.getNumberOfCoordinates(), point.getDimension());
    }

    /**
     * Write the Point into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param point The Point
     */
    private void putPoint(ByteBuffer buffer, Point point) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.Point, point.getDimension(), point.getSrid());
        putSrid(buffer, point.getSrid());
        if (!point.isEmpty()) {
            putCoordinate(buffer, point.getCoordinate());
        }
    }

    // LinearRing

    /**
     * Write a LinearRing to a hex String
     * @param linearRing The LinearRing
     * @return A hex String
     */
    public String writeToHex(LinearRing linearRing) {
        return toHex(write(linearRing));
    }

    /**
     * Write a LinearRing to a byte array
     * @param linearRing The LinearRing
     * @return A byte array
     */
    public byte[] write(LinearRing linearRing) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(linearRing));
        putLineString(buffer, linearRing);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the LinearRing
     * @param linearRing The LinearRing
     * @return The number of bytes necessary for the LinearRing
     */
    private int calculateNumberOfBytes(LinearRing linearRing) {
        return 4 + calculateNumberOfBytes(linearRing.getSrid()) +
                calculateNumberOfBytes(linearRing.getNumberOfCoordinates(), linearRing.getDimension());
    }

    /**
     * Write the LinearRing into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param linearRing The LinearRing
     */
    private void putLinearRing(ByteBuffer buffer, LinearRing linearRing) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.LineString, linearRing.getDimension(), linearRing.getSrid());
        putSrid(buffer, linearRing.getSrid());
        if (!linearRing.isEmpty()) {
            putCoordinates(buffer, linearRing.getCoordinates());
        }
    }

    // LineString

    /**
     * Write a LineString to a hex String
     * @param lineString The LineString
     * @return A hex String
     */
    public String writeToHex(LineString lineString) {
        return toHex(write(lineString));
    }

    /**
     * Write a LineString to a byte array
     * @param lineString The LineString
     * @return A byte array
     */
    public byte[] write(LineString lineString) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(lineString));
        putLineString(buffer, lineString);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the LineString
     * @param lineString The LineString
     * @return The number of bytes necessary for the LineString
     */
    private int calculateNumberOfBytes(LineString lineString) {
        return 4 + calculateNumberOfBytes(lineString.getSrid()) +
                calculateNumberOfBytes(lineString.getNumberOfCoordinates(), lineString.getDimension());
    }

    /**
     * Write the LineString into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param lineString The LineString
     */
    private void putLineString(ByteBuffer buffer, LineString lineString) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.LineString, lineString.getDimension(), lineString.getSrid());
        putSrid(buffer, lineString.getSrid());
        if (!lineString.isEmpty()) {
            putCoordinates(buffer, lineString.getCoordinates());
        }
    }

    // Polygon

    /**
     * Write a Polygon to a hex String
     * @param polygon The Polygon
     * @return A hex String
     */
    public String writeToHex(Polygon polygon) {
        return toHex(write(polygon));
    }

    /**
     * Write a Polygon to a byte array
     * @param polygon The Polygon
     * @return A byte array
     */
    public byte[] write(Polygon polygon) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(polygon));
        putPolygon(buffer, polygon);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the Polygon
     * @param polygon The Polygon
     * @return The number of bytes necessary for the Polygon
     */
    private int calculateNumberOfBytes(Polygon polygon) {
        int numberOfRings = 0;
        if (!polygon.isEmpty()) {
            numberOfRings = 1 + polygon.getInnerLinearRings().size();
        }
        return 4 +
            numberOfRings * 4 + // Number of coordinate for each ring
            calculateNumberOfBytes(polygon.getSrid()) +
            calculateNumberOfBytes(polygon.getNumberOfCoordinates(), polygon.getDimension());
    }

    /**
     * Write the Polygon into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param polygon The Polygon
     */
    private void putPolygon(ByteBuffer buffer, Polygon polygon) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.Polygon, polygon.getDimension(), polygon.getSrid());
        putSrid(buffer, polygon.getSrid());
        // Number of Rings
        int numberOfRings = 0;
        if (!polygon.isEmpty()) {
            numberOfRings = 1 + polygon.getInnerLinearRings().size();
        }
        buffer.putInt(numberOfRings);
        // Rings
        if (!polygon.isEmpty()) {
            putCoordinates(buffer, polygon.getOuterLinearRing().getCoordinates());
            for(LinearRing ring : polygon.getInnerLinearRings()) {
                putCoordinates(buffer, ring.getCoordinates());
            }
        }
    }

    // MultiPoint

    /**
     * Write a MultiPoint to a hex String
     * @param multiPoint The MultiPoint
     * @return A hex String
     */
    public String writeToHex(MultiPoint multiPoint) {
        return toHex(write(multiPoint));
    }

    /**
     * Write a MultiPoint to a byte array
     * @param multiPoint The MultiPoint
     * @return A byte array
     */
    public byte[] write(MultiPoint multiPoint) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(multiPoint));
        putMultiPoint(buffer, multiPoint);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the MultiPoint
     * @param multiPoint The MultiPoint
     * @return The number of bytes necessary for the MultiPoint
     */
    private int calculateNumberOfBytes(MultiPoint multiPoint) {
        return 4 +
            // Number of coordinates
            calculateNumberOfBytes(multiPoint.getSrid()) +
            // Points
            ((calculateNumberOfBytes(multiPoint.getSrid()) + calculateNumberOfBytes(1, multiPoint.getDimension())) * multiPoint.getPoints().size());
    }

    /**
     * Write the MultiPoint into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param multiPoint The MultiPoint
     */
    private void putMultiPoint(ByteBuffer buffer, MultiPoint multiPoint) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.MultiPoint, multiPoint.getDimension(), multiPoint.getSrid());
        putSrid(buffer, multiPoint.getSrid());
        buffer.putInt(multiPoint.getNumberOfCoordinates());
        if (!multiPoint.isEmpty()) {
            for (Point pt : multiPoint.getPoints()) {
                putPoint(buffer, pt);
            }
        }
    }

    // MultiLineString

    /**
     * Write a MultiLineString to a hex String
     * @param multiLineString The MultiLineString
     * @return A hex String
     */
    public String writeToHex(MultiLineString multiLineString) {
        return toHex(write(multiLineString));
    }

    /**
     * Write a MultiLineString to a byte array
     * @param multiLineString The MultiLineString
     * @return A byte array
     */
    public byte[] write(MultiLineString multiLineString) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(multiLineString));
        putMultiLineString(buffer, multiLineString);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the MultiLineString
     * @param multiLineString The MultiLineString
     * @return The number of bytes necessary for the MultiLineString
     */
    private int calculateNumberOfBytes(MultiLineString multiLineString) {
        int numberOfBytes = 4 + calculateNumberOfBytes(multiLineString.getSrid());
        for(LineString lineString : multiLineString.getLineStrings()) {
            numberOfBytes += calculateNumberOfBytes(lineString);
        }
        return numberOfBytes;
    }

    /**
     * Write the MultiLineString into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param multiLineString The MultiLineString
     */
    private void putMultiLineString(ByteBuffer buffer, MultiLineString multiLineString) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.MultiLineString, multiLineString.getDimension(), multiLineString.getSrid());
        putSrid(buffer, multiLineString.getSrid());
        buffer.putInt(multiLineString.getLineStrings().size());
        if (!multiLineString.isEmpty()) {
            for (LineString lineString : multiLineString.getLineStrings()) {
                putLineString(buffer, lineString);
            }
        }
    }

    // MultiPolygon

    /**
     * Write a MultiPolygon to a hex String
     * @param multiPolygon The MultiPolygon
     * @return A hex String
     */
    public String writeToHex(MultiPolygon multiPolygon) {
        return toHex(write(multiPolygon));
    }

    /**
     * Write a MultiPolygon to a byte array
     * @param multiPolygon The MultiPolygon
     * @return A byte array
     */
    public byte[] write(MultiPolygon multiPolygon) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(multiPolygon));
        putMultiPolygon(buffer, multiPolygon);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the MultiPolygon
     * @param multiPolygon The MultiPolygon
     * @return The number of bytes necessary for the MultiPolygon
     */
    private int calculateNumberOfBytes(MultiPolygon multiPolygon) {
        int numberOfBytes = 4 + calculateNumberOfBytes(multiPolygon.getSrid());
        for(Polygon polygon : multiPolygon.getPolygons()) {
            numberOfBytes += calculateNumberOfBytes(polygon);
        }
        return numberOfBytes;
    }

    /**
     * Write the MultiPolygon into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param multiPolygon The MultiPolygon
     */
    private void putMultiPolygon(ByteBuffer buffer, MultiPolygon multiPolygon) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.MultiPolygon, multiPolygon.getDimension(), multiPolygon.getSrid());
        putSrid(buffer, multiPolygon.getSrid());
        buffer.putInt(multiPolygon.getPolygons().size());
        if (!multiPolygon.isEmpty()) {
            for (Polygon polygon: multiPolygon.getPolygons()) {
                putPolygon(buffer, polygon);
            }
        }
    }

    // GeometryCollection

    /**
     * Write a GeometryCollection to a hex String
     * @param geometryCollection The GeometryCollection
     * @return A hex String
     */
    public String writeToHex(GeometryCollection geometryCollection) {
        return toHex(write(geometryCollection));
    }

    /**
     * Write a GeometryCollection to a byte array
     * @param geometryCollection The GeometryCollection
     * @return A byte array
     */
    public byte[] write(GeometryCollection geometryCollection) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(geometryCollection));
        putGeometryCollection(buffer, geometryCollection);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the GeometryCollection
     * @param geometryCollection The GeometryCollection
     * @return The number of bytes necessary for the GeometryCollection
     */
    private int calculateNumberOfBytes(GeometryCollection geometryCollection) {
        int numberOfBytes = 4 + calculateNumberOfBytes(geometryCollection.getSrid());
        for(Geometry geometry : geometryCollection.getGeometries()) {
            numberOfBytes += calculateNumberOfBytes(geometry);
        }
        return numberOfBytes;
    }

    /**
     * Write the GeometryCollection into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param geometryCollection The GeometryCollection
     */
    private void putGeometryCollection(ByteBuffer buffer, GeometryCollection geometryCollection) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.GeometryCollection, geometryCollection.getDimension(), geometryCollection.getSrid());
        putSrid(buffer, geometryCollection.getSrid());
        buffer.putInt(geometryCollection.getGeometries().size());
        if (!geometryCollection.isEmpty()) {
            for (Geometry geometry : geometryCollection.getGeometries()) {
                putGeometry(buffer, geometry);
            }
        }
    }

    // CircularString

    /**
     * Write a CircularString to a hex String
     * @param circularString The CircularString
     * @return A hex String
     */
    public String writeToHex(CircularString circularString) {
        return toHex(write(circularString));
    }

    /**
     * Write a CircularString to a byte array
     * @param circularString The CircularString
     * @return A byte array
     */
    public byte[] write(CircularString circularString) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(circularString));
        putCircularString(buffer, circularString);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the CircularString
     * @param circularString The CircularString
     * @return The number of bytes necessary for the CircularString
     */
    private int calculateNumberOfBytes(CircularString circularString) {
        return 4 + calculateNumberOfBytes(circularString.getSrid()) +
                calculateNumberOfBytes(circularString.getNumberOfCoordinates(), circularString.getDimension());
    }

    /**
     * Write the CircularString into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param circularString The CircularString
     */
    private void putCircularString(ByteBuffer buffer, CircularString circularString) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.CircularString, circularString.getDimension(), circularString.getSrid());
        putSrid(buffer, circularString.getSrid());
        if (!circularString.isEmpty()) {
            putCoordinates(buffer, circularString.getCoordinates());
        }
    }

    // Curve

    /**
     * Calculate the number of bytes necessary for the Curve
     * @param curve The GeometryCollection
     * @return The number of bytes necessary for the Curve
     */
    private int calculateNumberOfBytes(Curve curve) {
        if (curve instanceof LineString) {
            return calculateNumberOfBytes((LineString) curve);
        } else if (curve instanceof CircularString) {
            return calculateNumberOfBytes((CircularString) curve);
        } else if (curve instanceof CompoundCurve) {
            return calculateNumberOfBytes((CompoundCurve) curve);
        } else {
            throw new IllegalArgumentException("Unsupported Curve! " + curve.getClass().getName());
        }
    }

    /**
     * Write the Curve into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param curve The Curve
     */
    private void putCurve(ByteBuffer buffer, Curve curve) {
        if (curve instanceof LineString) {
            putLineString(buffer, (LineString) curve);
        } else if (curve instanceof CircularString) {
            putCircularString(buffer, (CircularString) curve);
        } else if (curve instanceof CompoundCurve) {
            putCompoundCurve(buffer, (CompoundCurve) curve);
        } else {
            throw new IllegalArgumentException("Unsupported Curve! " + curve.getClass().getName());
        }
    }

    // CompoundCurve

    /**
     * Write a CompoundCurve to a hex String
     * @param compoundCurve The CompoundCurve
     * @return A hex String
     */
    public String writeToHex(CompoundCurve compoundCurve) {
        return toHex(write(compoundCurve));
    }

    /**
     * Write a CompoundCurve to a byte array
     * @param compoundCurve The CompoundCurve
     * @return A byte array
     */
    public byte[] write(CompoundCurve compoundCurve) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(compoundCurve));
        putCompoundCurve(buffer, compoundCurve);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the CompoundCurve
     * @param compoundCurve The CompoundCurve
     * @return The number of bytes necessary for the CompoundCurve
     */
    private int calculateNumberOfBytes(CompoundCurve compoundCurve) {
        int numberOfBytes = 4 + calculateNumberOfBytes(compoundCurve.getSrid());
        for (Curve curve : compoundCurve.getCurves()) {
            numberOfBytes += calculateNumberOfBytes(curve);
        }
        return  numberOfBytes;
    }

    /**
     * Write the CompoundCurve into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param compoundCurve The CompoundCurve
     */
    private void putCompoundCurve(ByteBuffer buffer, CompoundCurve compoundCurve) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.CompoundCurve, compoundCurve.getDimension(), compoundCurve.getSrid());
        putSrid(buffer, compoundCurve.getSrid());
        buffer.putInt(compoundCurve.getCurves().size());
        if (!compoundCurve.isEmpty()) {
            for (Curve curve : compoundCurve.getCurves()) {
                putCurve(buffer, curve);
            }
        }
    }

    // CurvePolygon

    /**
     * Write a CurvePolygon to a hex String
     * @param curvePolygon The CurvePolygon
     * @return A hex String
     */
    public String writeToHex(CurvePolygon curvePolygon) {
        return toHex(write(curvePolygon));
    }

    /**
     * Write a CurvePolygon to a byte array
     * @param curvePolygon The CurvePolygon
     * @return A byte array
     */
    public byte[] write(CurvePolygon curvePolygon) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(curvePolygon));
        putCurvePolygon(buffer, curvePolygon);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the CurvePolygon
     * @param curvePolygon The CurvePolygon
     * @return The number of bytes necessary for the CurvePolygon
     */
    private int calculateNumberOfBytes(CurvePolygon curvePolygon) {
        int numberOfBytes = 4 + calculateNumberOfBytes(curvePolygon.getSrid());
        numberOfBytes += calculateNumberOfBytes(curvePolygon.getOuterCurve());
        for (Curve curve : curvePolygon.getInnerCurves()) {
            numberOfBytes += calculateNumberOfBytes(curve);
        }
        return  numberOfBytes;
    }

    /**
     * Write the CurvePolygon into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param curvePolygon The CurvePolygon
     */
    private void putCurvePolygon(ByteBuffer buffer, CurvePolygon curvePolygon) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.CurvePolygon, curvePolygon.getDimension(), curvePolygon.getSrid());
        putSrid(buffer, curvePolygon.getSrid());
        int numberOfCurves = 0;
        if (!curvePolygon.isEmpty()) {
            numberOfCurves += 1 + curvePolygon.getInnerCurves().size();
        }
        buffer.putInt(numberOfCurves);
        if (!curvePolygon.isEmpty()) {
            putCurve(buffer, curvePolygon.getOuterCurve());
            for (Curve curve : curvePolygon.getInnerCurves()) {
                putCurve(buffer, curve);
            }
        }
    }

    // MultiCurve

    /**
     * Write a MultiCurve to a hex String
     * @param multiCurve The MultiCurve
     * @return A hex String
     */
    public String writeToHex(MultiCurve multiCurve) {
        return toHex(write(multiCurve));
    }

    /**
     * Write a MultiCurve to a byte array
     * @param multiCurve The MultiCurve
     * @return A byte array
     */
    public byte[] write(MultiCurve multiCurve) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(multiCurve));
        putMultiCurve(buffer, multiCurve);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the MultiCurve
     * @param multiCurve The MultiCurve
     * @return The number of bytes necessary for the MultiCurve
     */
    private int calculateNumberOfBytes(MultiCurve multiCurve) {
        int numberOfBytes = 4 + calculateNumberOfBytes(multiCurve.getSrid());
        for (Curve curve : multiCurve.getCurves()) {
            numberOfBytes += calculateNumberOfBytes(curve);
        }
        return  numberOfBytes;
    }

    /**
     * Write the MultiCurve into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param multiCurve The MultiCurve
     */
    private void putMultiCurve(ByteBuffer buffer, MultiCurve multiCurve) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.MultiCurve, multiCurve.getDimension(), multiCurve.getSrid());
        putSrid(buffer, multiCurve.getSrid());
        buffer.putInt(multiCurve.getCurves().size());
        if (!multiCurve.isEmpty()) {
            for (Curve curve : multiCurve.getCurves()) {
                putCurve(buffer, curve);
            }
        }
    }

    // Surface

    /**
     * Calculate the number of bytes necessary for the Surface
     * @param surface The Surface
     * @return The number of bytes necessary for the Surface
     */
    private int calculateNumberOfBytes(Surface surface) {
        if (surface instanceof Triangle) {
            return calculateNumberOfBytes((Triangle) surface);
        } else if (surface instanceof Polygon) {
            return calculateNumberOfBytes((Polygon) surface);
        } else if (surface instanceof PolyHedralSurface) {
            return calculateNumberOfBytes((PolyHedralSurface) surface);
        } else if (surface instanceof Tin) {
            return calculateNumberOfBytes((Tin) surface);
        } else if (surface instanceof CurvePolygon) {
            return calculateNumberOfBytes((CurvePolygon) surface);
        } else {
            throw new IllegalArgumentException("Unknown Surface: " + surface.getClass().getName());
        }
    }

    /**
     * Write the Surface into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param surface The Surface
     */
    private void putSurface(ByteBuffer buffer, Surface surface) {
        if (surface instanceof Triangle) {
            putTriangle(buffer, (Triangle) surface);
        } else if (surface instanceof Polygon) {
            putPolygon(buffer, (Polygon) surface);
        } else if (surface instanceof PolyHedralSurface) {
            putPolyHedralSurface(buffer, (PolyHedralSurface) surface);
        } else if (surface instanceof Tin) {
            putTin(buffer, (Tin) surface);
        } else if (surface instanceof CurvePolygon) {
            putCurvePolygon(buffer, (CurvePolygon) surface);
        } else {
            throw new IllegalArgumentException("Unknown Surface: " + surface.getClass().getName());
        }
    }

    // MultiSurface

    /**
     * Write a MultiSurface to a hex String
     * @param multiSurface The MultiSurface
     * @return A hex String
     */
    public String writeToHex(MultiSurface multiSurface) {
        return toHex(write(multiSurface));
    }

    /**
     * Write a MultiSurface to a byte array
     * @param multiSurface The MultiSurface
     * @return A byte array
     */
    public byte[] write(MultiSurface multiSurface) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(multiSurface));
        putMultiSurface(buffer, multiSurface);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the MultiSurface
     * @param multiSurface The MultiSurface
     * @return The number of bytes necessary for the MultiSurface
     */
    private int calculateNumberOfBytes(MultiSurface multiSurface) {
        int numberOfBytes = 4 + calculateNumberOfBytes(multiSurface.getSrid());
        for (Surface surface : multiSurface.getSurfaces()) {
            numberOfBytes += calculateNumberOfBytes(surface);
        }
        return  numberOfBytes;
    }

    /**
     * Write the MultiSurface into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param multiSurface The MultiSurface
     */
    private void putMultiSurface(ByteBuffer buffer, MultiSurface multiSurface) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.MultiSurface, multiSurface.getDimension(), multiSurface.getSrid());
        putSrid(buffer, multiSurface.getSrid());
        buffer.putInt(multiSurface.getSurfaces().size());
        if (!multiSurface.isEmpty()) {
            for (Surface surface : multiSurface.getSurfaces()) {
                putSurface(buffer, surface);
            }
        }
    }

    // Tin

    /**
     * Write a Tin to a hex String
     * @param tin The Tin
     * @return A hex String
     */
    public String writeToHex(Tin tin) {
        return toHex(write(tin));
    }

    /**
     * Write a Tin to a byte array
     * @param tin The Tin
     * @return A byte array
     */
    public byte[] write(Tin tin) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(tin));
        putTin(buffer, tin);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the Tin
     * @param tin The Tin
     * @return The number of bytes necessary for the Tin
     */
    private int calculateNumberOfBytes(Tin tin) {
        int numberOfBytes = 4 + calculateNumberOfBytes(tin.getSrid());
        for(Triangle triangle : tin.getTriangles()) {
            numberOfBytes += calculateNumberOfBytes(triangle);
        }
        return  numberOfBytes;
    }

    /**
     * Write the Tin into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param tin The Tin
     */
    private void putTin(ByteBuffer buffer, Tin tin) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.Tin, tin.getDimension(), tin.getSrid());
        putSrid(buffer, tin.getSrid());
        buffer.putInt(tin.getTriangles().size());
        if (!tin.isEmpty()) {
            for(Triangle triangle : tin.getTriangles()) {
                putTriangle(buffer, triangle);
            }
        }
    }

    // Triangle

    /**
     * Write a Triangle to a hex String
     * @param triangle The Triangle
     * @return A hex String
     */
    public String writeToHex(Triangle triangle) {
        return toHex(write(triangle));
    }

    /**
     * Write a Triangle to a byte array
     * @param triangle The Triangle
     * @return A byte array
     */
    public byte[] write(Triangle triangle) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(triangle));
        putTriangle(buffer, triangle);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the Triangle
     * @param triangle The Triangle
     * @return The number of bytes necessary for the Triangle
     */
    private int calculateNumberOfBytes(Triangle triangle) {
        int numberOfRings = 0;
        if (!triangle.isEmpty()) {
            numberOfRings = 1 + triangle.getInnerLinearRings().size();
        }
        return 4 +
                numberOfRings * 4 + // Number of coordinate for each ring
                calculateNumberOfBytes(triangle.getSrid()) +
                calculateNumberOfBytes(triangle.getNumberOfCoordinates(), triangle.getDimension());
    }

    /**
     * Write the Triangle into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param triangle The Triangle
     */
    private void putTriangle(ByteBuffer buffer, Triangle triangle) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.Triangle, triangle.getDimension(), triangle.getSrid());
        putSrid(buffer, triangle.getSrid());
        // Number of Rings
        int numberOfRings = 0;
        if (!triangle.isEmpty()) {
            numberOfRings = 1 + triangle.getInnerLinearRings().size();
        }
        buffer.putInt(numberOfRings);
        // Rings
        if (!triangle.isEmpty()) {
            putCoordinates(buffer, triangle.getOuterLinearRing().getCoordinates());
            for(LinearRing ring : triangle.getInnerLinearRings()) {
                putCoordinates(buffer, ring.getCoordinates());
            }
        }
    }

    // PolyhedralSurface

    /**
     * Write a PolyhedralSurface to a hex String
     * @param polyHedralSurface The PolyhedralSurface
     * @return A hex String
     */
    public String writeToHex(PolyHedralSurface polyHedralSurface) {
        return toHex(write(polyHedralSurface));
    }

    /**
     * Write a PolyhedralSurface to a byte array
     * @param polyHedralSurface The PolyhedralSurface
     * @return A byte array
     */
    public byte[] write(PolyHedralSurface polyHedralSurface) {
        ByteBuffer buffer = ByteBuffer.allocate(calculateNumberOfBytes(polyHedralSurface));
        putPolyHedralSurface(buffer, polyHedralSurface);
        return buffer.array();
    }

    /**
     * Calculate the number of bytes necessary for the PolyhedralSurface
     * @param polyHedralSurface The PolyhedralSurface
     * @return The number of bytes necessary for the PolyhedralSurface
     */
    private int calculateNumberOfBytes(PolyHedralSurface polyHedralSurface) {
        int numberOfBytes = 4 + calculateNumberOfBytes(polyHedralSurface.getSrid());
        for (Polygon polygon : polyHedralSurface.getPolygons()) {
            numberOfBytes += calculateNumberOfBytes(polygon);
        }
        return  numberOfBytes;
    }

    /**
     * Write the PolyhedralSurface into the ByteBuffer
     * @param buffer The ByteBuffer
     * @param polyHedralSurface The PolyhedralSurface
     */
    private void putPolyHedralSurface(ByteBuffer buffer, PolyHedralSurface polyHedralSurface) {
        putByteOrder(buffer);
        putGeometryType(buffer, GeometryType.PolyHedralSurface, polyHedralSurface.getDimension(), polyHedralSurface.getSrid());
        putSrid(buffer, polyHedralSurface.getSrid());
        buffer.putInt(polyHedralSurface.getPolygons().size());
        if (!polyHedralSurface.isEmpty()) {
            for (Polygon polygon : polyHedralSurface.getPolygons()) {
                putPolygon(buffer, polygon);
            }
        }
    }

    /**
     * The hex array
     */
    private final static char[] hexArray = "0123456789ABCDEF".toCharArray();

    /**
     * Convert an array of bytes to a hex String.
     * @param bytes The array of bytes
     * @return The hex String
     */
    private static String toHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }
}