package mil.nga.geopackage.map.geom;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolygonOptions;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.maps.android.PolyUtil;
import com.google.maps.android.SphericalUtil;

import org.locationtech.proj4j.units.Units;

import java.util.ArrayList;
import java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.sf.CircularString;
import mil.nga.sf.CompoundCurve;
import mil.nga.sf.Curve;
import mil.nga.sf.CurvePolygon;
import mil.nga.sf.Geometry;
import mil.nga.sf.GeometryCollection;
import mil.nga.sf.GeometryType;
import mil.nga.sf.LineString;
import mil.nga.sf.MultiLineString;
import mil.nga.sf.MultiPoint;
import mil.nga.sf.MultiPolygon;
import mil.nga.sf.Point;
import mil.nga.sf.Polygon;
import mil.nga.sf.PolyhedralSurface;
import mil.nga.sf.TIN;
import mil.nga.sf.Triangle;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionTransform;
import mil.nga.sf.util.GeometryUtils;

/**
 * Provides conversions methods between geometry object and Google Maps Android
 * API v2 Shapes
 *
 * @author osbornb
 */
public class GoogleMapShapeConverter {

    /**
     * Projection
     */
    private final Projection projection;

    /**
     * Transformation to WGS 84
     */
    private final ProjectionTransform toWgs84;

    /**
     * Transformation from WGS 84
     */
    private final ProjectionTransform fromWgs84;

    /**
     * Transformation to Web Mercator
     */
    private final ProjectionTransform toWebMercator;

    /**
     * Transformation from Web Mercator
     */
    private final ProjectionTransform fromWebMercator;

    /**
     * Convert polygon exteriors to specified orientation
     */
    private PolygonOrientation exteriorOrientation = PolygonOrientation.COUNTERCLOCKWISE;

    /**
     * Convert polygon holes to specified orientation
     */
    private PolygonOrientation holeOrientation = PolygonOrientation.CLOCKWISE;

    /**
     * Tolerance in meters for simplifying lines and polygons to a similar curve with fewer points.
     * Default is null resulting in no simplification
     */
    private Double simplifyTolerance;

    /**
     * Constructor
     *
     * @since 1.3.2
     */
    public GoogleMapShapeConverter() {
        this(null);
    }

    /**
     * Constructor with specified projection, see
     * {@link FeatureDao#getProjection}
     *
     * @param projection projection
     */
    public GoogleMapShapeConverter(Projection projection) {
        this.projection = projection;
        if (projection != null) {
            toWgs84 = projection
                    .getTransformation(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
            Projection wgs84 = toWgs84.getToProjection();
            fromWgs84 = wgs84.getTransformation(projection);
            toWebMercator = projection.getTransformation(ProjectionConstants.EPSG_WEB_MERCATOR);
            Projection webMercator = toWebMercator.getToProjection();
            fromWebMercator = webMercator.getTransformation(projection);
        } else {
            toWgs84 = null;
            fromWgs84 = null;
            toWebMercator = null;
            fromWebMercator = null;
        }
    }

    /**
     * Get the projection
     *
     * @return projection
     */
    public Projection getProjection() {
        return projection;
    }

    /**
     * Get exterior orientation for conversions. Defaults to PolygonOrientation.COUNTERCLOCKWISE
     *
     * @return exterior orientation
     * @since 1.3.2
     */
    public PolygonOrientation getExteriorOrientation() {
        return exteriorOrientation;
    }

    /**
     * Set the exterior orientation for conversions, set to null to maintain orientation
     *
     * @param exteriorOrientation orientation
     * @since 1.3.2
     */
    public void setExteriorOrientation(PolygonOrientation exteriorOrientation) {
        this.exteriorOrientation = exteriorOrientation;
    }

    /**
     * Get polygon hole orientation for conversions. Defaults to PolygonOrientation.CLOCKWISE
     *
     * @return hole orientation
     * @since 1.3.2
     */
    public PolygonOrientation getHoleOrientation() {
        return holeOrientation;
    }

    /**
     * Set the polygon hole orientation for conversions, set to null to maintain orientation
     *
     * @param holeOrientation orientation
     * @since 1.3.2
     */
    public void setHoleOrientation(PolygonOrientation holeOrientation) {
        this.holeOrientation = holeOrientation;
    }

    /**
     * Get the simplify tolerance in meters to simplify lines and polygons to similar curves with fewer points
     *
     * @return simplify tolerance in meters, null for no simplification
     */
    public Double getSimplifyTolerance() {
        return simplifyTolerance;
    }

    /**
     * Set the simplify tolerance in meters to simplify lines and polygons to similar curves with fewer points
     *
     * @param simplifyTolerance simplify tolerance in meters, null for no simplification
     */
    public void setSimplifyTolerance(Double simplifyTolerance) {
        this.simplifyTolerance = simplifyTolerance;
    }

    /**
     * Transform a projection point to WGS84
     *
     * @param point projection point
     * @return WGS84 point
     */
    public Point toWgs84(Point point) {
        if (projection != null) {
            point = toWgs84.transform(point);
        }
        return point;
    }

    /**
     * Transform a WGS84 point to the projection
     *
     * @param point WGS84 point
     * @return projection point
     */
    public Point toProjection(Point point) {
        if (projection != null) {
            point = fromWgs84.transform(point);
        }
        return point;
    }

    /**
     * Convert a {@link Point} to a {@link LatLng}
     *
     * @param point point
     * @return lat lng
     */
    public LatLng toLatLng(Point point) {
        point = toWgs84(point);
        LatLng latLng = new LatLng(point.getY(), point.getX());
        return latLng;
    }

    /**
     * Convert a {@link LatLng} to a {@link Point}
     *
     * @param latLng lat lng
     * @return point
     */
    public Point toPoint(LatLng latLng) {
        return toPoint(latLng, false, false);
    }

    /**
     * Convert a {@link LatLng} to a {@link Point}
     *
     * @param latLng lat lng
     * @param hasZ   has z flag
     * @param hasM   has m flag
     * @return point
     */
    public Point toPoint(LatLng latLng, boolean hasZ, boolean hasM) {
        double y = latLng.latitude;
        double x = latLng.longitude;
        Point point = new Point(hasZ, hasM, x, y);
        point = toProjection(point);
        return point;
    }

    /**
     * Convert a {@link LineString} to a {@link PolylineOptions}
     *
     * @param lineString line string
     * @return polyline options
     */
    public PolylineOptions toPolyline(LineString lineString) {

        PolylineOptions polylineOptions = new PolylineOptions();
        Double z = null;

        // Try to simplify the number of points in the line string
        List<Point> points = simplifyPoints(lineString.getPoints());

        for (Point point : points) {
            LatLng latLng = toLatLng(point);
            polylineOptions.add(latLng);
            if (point.hasZ()) {
                z = (z == null) ? point.getZ() : Math.max(z, point.getZ());
            }
        }

        if (lineString.hasZ() && z != null) {
            polylineOptions.zIndex(z.floatValue());
        }

        return polylineOptions;
    }

    /**
     * Convert a {@link Polyline} to a {@link LineString}
     *
     * @param polyline polyline
     * @return line string
     */
    public LineString toLineString(Polyline polyline) {
        return toLineString(polyline, false, false);
    }

    /**
     * Convert a {@link Polyline} to a {@link LineString}
     *
     * @param polyline polyline
     * @param hasZ     has z flag
     * @param hasM     has m flag
     * @return line string
     */
    public LineString toLineString(Polyline polyline, boolean hasZ, boolean hasM) {
        return toLineString(polyline.getPoints(), hasZ, hasM);
    }

    /**
     * Convert a {@link PolylineOptions} to a {@link LineString}
     *
     * @param polyline polyline options
     * @return line string
     */
    public LineString toLineString(PolylineOptions polyline) {
        return toLineString(polyline, false, false);
    }

    /**
     * Convert a {@link PolylineOptions} to a {@link LineString}
     *
     * @param polyline polyline options
     * @param hasZ     has z flag
     * @param hasM     has m flag
     * @return line string
     */
    public LineString toLineString(PolylineOptions polyline, boolean hasZ,
                                   boolean hasM) {
        return toLineString(polyline.getPoints(), hasZ, hasM);
    }

    /**
     * Convert a list of {@link LatLng} to a {@link LineString}
     *
     * @param latLngs lat lngs
     * @return line string
     */
    public LineString toLineString(List<LatLng> latLngs) {
        return toLineString(latLngs, false, false);
    }

    /**
     * Convert a list of {@link LatLng} to a {@link LineString}
     *
     * @param latLngs lat lngs
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return line string
     */
    public LineString toLineString(List<LatLng> latLngs, boolean hasZ,
                                   boolean hasM) {

        LineString lineString = new LineString(hasZ, hasM);

        populateLineString(lineString, latLngs);

        return lineString;
    }

    /**
     * Convert a list of {@link LatLng} to a {@link CircularString}
     *
     * @param latLngs lat lngs
     * @return circular string
     */
    public CircularString toCircularString(List<LatLng> latLngs) {
        return toCircularString(latLngs, false, false);
    }

    /**
     * Convert a list of {@link LatLng} to a {@link CircularString}
     *
     * @param latLngs lat lngs
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return circular string
     */
    public CircularString toCircularString(List<LatLng> latLngs, boolean hasZ,
                                           boolean hasM) {

        CircularString circularString = new CircularString(hasZ, hasM);

        populateLineString(circularString, latLngs);

        return circularString;
    }

    /**
     * Convert a list of {@link LatLng} to a {@link LineString}
     *
     * @param lineString line string
     * @param latLngs    lat lngs
     */
    public void populateLineString(LineString lineString, List<LatLng> latLngs) {

        for (LatLng latLng : latLngs) {
            Point point = toPoint(latLng, lineString.hasZ(), lineString.hasM());
            lineString.addPoint(point);
        }
    }

    /**
     * Convert a {@link Polygon} to a {@link PolygonOptions}
     *
     * @param polygon polygon
     * @return polygon options
     */
    public PolygonOptions toPolygon(Polygon polygon) {

        PolygonOptions polygonOptions = new PolygonOptions();

        List<LineString> rings = polygon.getRings();

        if (!rings.isEmpty()) {

            Double z = null;

            // Add the polygon points
            LineString polygonLineString = rings.get(0);

            // Try to simplify the number of points in the polygon ring
            List<Point> points = simplifyPoints(polygonLineString.getPoints());

            for (Point point : points) {
                LatLng latLng = toLatLng(point);
                polygonOptions.add(latLng);
                if (point.hasZ()) {
                    z = (z == null) ? point.getZ() : Math.max(z, point.getZ());
                }
            }

            // Add the holes
            for (int i = 1; i < rings.size(); i++) {
                LineString hole = rings.get(i);

                // Try to simplify the number of points in the polygon hole
                List<Point> holePoints = simplifyPoints(hole.getPoints());

                List<LatLng> holeLatLngs = new ArrayList<LatLng>();
                for (Point point : holePoints) {
                    LatLng latLng = toLatLng(point);
                    holeLatLngs.add(latLng);
                    if (point.hasZ()) {
                        z = (z == null) ? point.getZ() : Math.max(z,
                                point.getZ());
                    }
                }
                polygonOptions.addHole(holeLatLngs);
            }

            if (polygon.hasZ() && z != null) {
                polygonOptions.zIndex(z.floatValue());
            }
        }

        return polygonOptions;
    }

    /**
     * Convert a {@link CurvePolygon} to a {@link PolygonOptions}
     *
     * @param curvePolygon curve polygon
     * @return polygon options
     * @since 1.4.1
     */
    public PolygonOptions toCurvePolygon(CurvePolygon curvePolygon) {

        PolygonOptions polygonOptions = new PolygonOptions();

        List<Curve> rings = curvePolygon.getRings();

        if (!rings.isEmpty()) {

            Double z = null;

            // Add the polygon points
            Curve curve = rings.get(0);
            if (curve instanceof CompoundCurve) {
                CompoundCurve compoundCurve = (CompoundCurve) curve;
                for (LineString lineString : compoundCurve.getLineStrings()) {

                    // Try to simplify the number of points in the compound curve
                    List<Point> points = simplifyPoints(lineString.getPoints());

                    for (Point point : points) {
                        LatLng latLng = toLatLng(point);
                        polygonOptions.add(latLng);
                        if (point.hasZ()) {
                            z = (z == null) ? point.getZ() : Math.max(z, point.getZ());
                        }
                    }
                }
            } else if (curve instanceof LineString) {
                LineString lineString = (LineString) curve;

                // Try to simplify the number of points in the curve
                List<Point> points = simplifyPoints(lineString.getPoints());

                for (Point point : points) {
                    LatLng latLng = toLatLng(point);
                    polygonOptions.add(latLng);
                    if (point.hasZ()) {
                        z = (z == null) ? point.getZ() : Math.max(z, point.getZ());
                    }
                }
            } else {
                throw new GeoPackageException("Unsupported Curve Type: "
                        + curve.getClass().getSimpleName());
            }

            // Add the holes
            for (int i = 1; i < rings.size(); i++) {
                Curve hole = rings.get(i);
                List<LatLng> holeLatLngs = new ArrayList<LatLng>();
                if (hole instanceof CompoundCurve) {
                    CompoundCurve holeCompoundCurve = (CompoundCurve) hole;
                    for (LineString holeLineString : holeCompoundCurve.getLineStrings()) {

                        // Try to simplify the number of points in the hole
                        List<Point> holePoints = simplifyPoints(holeLineString.getPoints());

                        for (Point point : holePoints) {
                            LatLng latLng = toLatLng(point);
                            holeLatLngs.add(latLng);
                            if (point.hasZ()) {
                                z = (z == null) ? point.getZ() : Math.max(z,
                                        point.getZ());
                            }
                        }
                    }
                } else if (hole instanceof LineString) {
                    LineString holeLineString = (LineString) hole;

                    // Try to simplify the number of points in the hole
                    List<Point> holePoints = simplifyPoints(holeLineString.getPoints());

                    for (Point point : holePoints) {
                        LatLng latLng = toLatLng(point);
                        holeLatLngs.add(latLng);
                        if (point.hasZ()) {
                            z = (z == null) ? point.getZ() : Math.max(z,
                                    point.getZ());
                        }
                    }
                } else {
                    throw new GeoPackageException("Unsupported Curve Hole Type: "
                            + hole.getClass().getSimpleName());
                }

                polygonOptions.addHole(holeLatLngs);
            }

            if (curvePolygon.hasZ() && z != null) {
                polygonOptions.zIndex(z.floatValue());
            }
        }

        return polygonOptions;
    }

    /**
     * When the simplify tolerance is set, simplify the points to a similar
     * curve with fewer points.
     *
     * @param points ordered points
     * @return simplified points
     */
    private List<Point> simplifyPoints(List<Point> points) {

        List<Point> simplifiedPoints = null;
        if (simplifyTolerance != null) {

            // Reproject to web mercator if not in meters
            if (projection != null && !projection.isUnit(Units.METRES)) {
                points = toWebMercator.transform(points);
            }

            // Simplify the points
            simplifiedPoints = GeometryUtils.simplifyPoints(points,
                    simplifyTolerance);

            // Reproject back to the original projection
            if (projection != null && !projection.isUnit(Units.METRES)) {
                simplifiedPoints = fromWebMercator.transform(simplifiedPoints);
            }
        } else {
            simplifiedPoints = points;
        }

        return simplifiedPoints;
    }

    /**
     * Convert a {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link Polygon}
     *
     * @param polygon polygon
     * @return polygon
     */
    public Polygon toPolygon(com.google.android.gms.maps.model.Polygon polygon) {
        return toPolygon(polygon, false, false);
    }

    /**
     * Convert a {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link Polygon}
     *
     * @param polygon polygon
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return polygon
     */
    public Polygon toPolygon(com.google.android.gms.maps.model.Polygon polygon,
                             boolean hasZ, boolean hasM) {
        return toPolygon(polygon.getPoints(), polygon.getHoles(), hasZ, hasM);
    }

    /**
     * Convert a {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link Polygon}
     *
     * @param polygon polygon options
     * @return polygon
     */
    public Polygon toPolygon(PolygonOptions polygon) {
        return toPolygon(polygon, false, false);
    }

    /**
     * Convert a {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link Polygon}
     *
     * @param polygon polygon options
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return polygon
     */
    public Polygon toPolygon(PolygonOptions polygon, boolean hasZ, boolean hasM) {
        return toPolygon(polygon.getPoints(), polygon.getHoles(), hasZ, hasM);
    }

    /**
     * Convert a list of {@link LatLng} and list of hole list {@link LatLng} to
     * a {@link Polygon}
     *
     * @param latLngs lat lngs
     * @param holes   list of holes
     * @return polygon
     */
    public Polygon toPolygon(List<LatLng> latLngs, List<List<LatLng>> holes) {
        return toPolygon(latLngs, holes, false, false);
    }

    /**
     * Convert a list of {@link LatLng} and list of hole list {@link LatLng} to
     * a {@link Polygon}
     *
     * @param latLngs lat lngs
     * @param holes   list of holes
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return polygon
     */
    public Polygon toPolygon(List<LatLng> latLngs, List<List<LatLng>> holes,
                             boolean hasZ, boolean hasM) {

        Polygon polygon = new Polygon(hasZ, hasM);

        // Close the ring if needed and determine orientation
        closePolygonRing(latLngs);
        PolygonOrientation ringOrientation = getOrientation(latLngs);

        // Add the polygon points
        LineString polygonLineString = new LineString(hasZ, hasM);
        for (LatLng latLng : latLngs) {
            Point point = toPoint(latLng);
            // Add exterior in desired orientation order
            if (exteriorOrientation == null || exteriorOrientation == ringOrientation) {
                polygonLineString.addPoint(point);
            } else {
                polygonLineString.getPoints().add(0, point);
            }
        }
        polygon.addRing(polygonLineString);

        // Add the holes
        if (holes != null) {
            for (List<LatLng> hole : holes) {

                // Close the hole if needed and determine orientation
                closePolygonRing(hole);
                PolygonOrientation ringHoleOrientation = getOrientation(hole);

                LineString holeLineString = new LineString(hasZ, hasM);
                for (LatLng latLng : hole) {
                    Point point = toPoint(latLng);
                    // Add holes in desired orientation order
                    if (holeOrientation == null || holeOrientation == ringHoleOrientation) {
                        holeLineString.addPoint(point);
                    } else {
                        holeLineString.getPoints().add(0, point);
                    }
                }
                polygon.addRing(holeLineString);
            }
        }

        return polygon;
    }

    /**
     * Close the polygon ring (exterior or hole) points if needed
     *
     * @param points ring points
     * @since 1.3.2
     */
    public void closePolygonRing(List<LatLng> points) {
        if (!PolyUtil.isClosedPolygon(points)) {
            LatLng first = points.get(0);
            points.add(new LatLng(first.latitude, first.longitude));
        }
    }

    /**
     * Determine the closed points orientation
     *
     * @param points closed points
     * @return orientation
     * @since 1.3.2
     */
    public PolygonOrientation getOrientation(List<LatLng> points) {
        return SphericalUtil.computeSignedArea(points) >= 0 ? PolygonOrientation.COUNTERCLOCKWISE : PolygonOrientation.CLOCKWISE;
    }

    /**
     * Convert a {@link MultiPoint} to a {@link MultiLatLng}
     *
     * @param multiPoint multi point
     * @return multi lat lng
     */
    public MultiLatLng toLatLngs(MultiPoint multiPoint) {

        MultiLatLng multiLatLng = new MultiLatLng();

        for (Point point : multiPoint.getPoints()) {
            LatLng latLng = toLatLng(point);
            multiLatLng.add(latLng);
        }

        return multiLatLng;
    }

    /**
     * Convert a {@link MultiLatLng} to a {@link MultiPoint}
     *
     * @param latLngs lat lngs
     * @return multi point
     */
    public MultiPoint toMultiPoint(MultiLatLng latLngs) {
        return toMultiPoint(latLngs, false, false);
    }

    /**
     * Convert a {@link MultiLatLng} to a {@link MultiPoint}
     *
     * @param latLngs lat lngs
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return multi point
     */
    public MultiPoint toMultiPoint(MultiLatLng latLngs, boolean hasZ,
                                   boolean hasM) {
        return toMultiPoint(latLngs.getLatLngs(), hasZ, hasM);
    }

    /**
     * Convert a {@link MultiLatLng} to a {@link MultiPoint}
     *
     * @param latLngs lat lngs
     * @return multi point
     */
    public MultiPoint toMultiPoint(List<LatLng> latLngs) {
        return toMultiPoint(latLngs, false, false);
    }

    /**
     * Convert a {@link MultiLatLng} to a {@link MultiPoint}
     *
     * @param latLngs lat lngs
     * @param hasZ    has z flag
     * @param hasM    has m flag
     * @return multi point
     */
    public MultiPoint toMultiPoint(List<LatLng> latLngs, boolean hasZ,
                                   boolean hasM) {

        MultiPoint multiPoint = new MultiPoint(hasZ, hasM);

        for (LatLng latLng : latLngs) {
            Point point = toPoint(latLng);
            multiPoint.addPoint(point);
        }

        return multiPoint;
    }

    /**
     * Convert a {@link MultiLineString} to a {@link MultiPolylineOptions}
     *
     * @param multiLineString multi line string
     * @return multi polyline options
     */
    public MultiPolylineOptions toPolylines(MultiLineString multiLineString) {

        MultiPolylineOptions polylines = new MultiPolylineOptions();

        for (LineString lineString : multiLineString.getLineStrings()) {
            PolylineOptions polyline = toPolyline(lineString);
            polylines.add(polyline);
        }

        return polylines;
    }

    /**
     * Convert a list of {@link Polyline} to a {@link MultiLineString}
     *
     * @param polylineList polyline list
     * @return multi line string
     */
    public MultiLineString toMultiLineString(List<Polyline> polylineList) {
        return toMultiLineString(polylineList, false, false);
    }

    /**
     * Convert a list of {@link Polyline} to a {@link MultiLineString}
     *
     * @param polylineList polyline list
     * @param hasZ         has z flag
     * @param hasM         has m flag
     * @return multi line string
     */
    public MultiLineString toMultiLineString(List<Polyline> polylineList,
                                             boolean hasZ, boolean hasM) {

        MultiLineString multiLineString = new MultiLineString(hasZ, hasM);

        for (Polyline polyline : polylineList) {
            LineString lineString = toLineString(polyline);
            multiLineString.addLineString(lineString);
        }

        return multiLineString;
    }

    /**
     * Convert a list of List<LatLng> to a {@link MultiLineString}
     *
     * @param polylineList polyline list
     * @return multi line string
     */
    public MultiLineString toMultiLineStringFromList(
            List<List<LatLng>> polylineList) {
        return toMultiLineStringFromList(polylineList, false, false);
    }

    /**
     * Convert a list of List<LatLng> to a {@link MultiLineString}
     *
     * @param polylineList polyline list
     * @param hasZ         has z flag
     * @param hasM         has m flag
     * @return multi line string
     */
    public MultiLineString toMultiLineStringFromList(
            List<List<LatLng>> polylineList, boolean hasZ, boolean hasM) {

        MultiLineString multiLineString = new MultiLineString(hasZ, hasM);

        for (List<LatLng> polyline : polylineList) {
            LineString lineString = toLineString(polyline);
            multiLineString.addLineString(lineString);
        }

        return multiLineString;
    }

    /**
     * Convert a list of List<LatLng> to a {@link CompoundCurve}
     *
     * @param polylineList polyline list
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveFromList(List<List<LatLng>> polylineList) {
        return toCompoundCurveFromList(polylineList, false, false);
    }

    /**
     * Convert a list of List<LatLng> to a {@link CompoundCurve}
     *
     * @param polylineList polyline list
     * @param hasZ         has z flag
     * @param hasM         has m flag
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveFromList(
            List<List<LatLng>> polylineList, boolean hasZ, boolean hasM) {

        CompoundCurve compoundCurve = new CompoundCurve(hasZ, hasM);

        for (List<LatLng> polyline : polylineList) {
            LineString lineString = toLineString(polyline);
            compoundCurve.addLineString(lineString);
        }

        return compoundCurve;
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link MultiLineString}
     *
     * @param multiPolylineOptions multi polyline options
     * @return multi line string
     */
    public MultiLineString toMultiLineStringFromOptions(
            MultiPolylineOptions multiPolylineOptions) {
        return toMultiLineStringFromOptions(multiPolylineOptions, false, false);
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link MultiLineString}
     *
     * @param multiPolylineOptions multi polyline options
     * @param hasZ                 has z flag
     * @param hasM                 has m flag
     * @return multi line string
     */
    public MultiLineString toMultiLineStringFromOptions(
            MultiPolylineOptions multiPolylineOptions, boolean hasZ,
            boolean hasM) {

        MultiLineString multiLineString = new MultiLineString(hasZ, hasM);

        for (PolylineOptions polyline : multiPolylineOptions
                .getPolylineOptions()) {
            LineString lineString = toLineString(polyline);
            multiLineString.addLineString(lineString);
        }

        return multiLineString;
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link CompoundCurve}
     *
     * @param multiPolylineOptions multi polyline options
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveFromOptions(
            MultiPolylineOptions multiPolylineOptions) {
        return toCompoundCurveFromOptions(multiPolylineOptions, false, false);
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link CompoundCurve}
     *
     * @param multiPolylineOptions multi polyline options
     * @param hasZ                 has z flag
     * @param hasM                 has m flag
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveFromOptions(
            MultiPolylineOptions multiPolylineOptions, boolean hasZ,
            boolean hasM) {

        CompoundCurve compoundCurve = new CompoundCurve(hasZ, hasM);

        for (PolylineOptions polyline : multiPolylineOptions
                .getPolylineOptions()) {
            LineString lineString = toLineString(polyline);
            compoundCurve.addLineString(lineString);
        }

        return compoundCurve;
    }

    /**
     * Convert a {@link MultiPolygon} to a {@link MultiPolygonOptions}
     *
     * @param multiPolygon multi polygon
     * @return multi polygon options
     */
    public MultiPolygonOptions toPolygons(MultiPolygon multiPolygon) {

        MultiPolygonOptions polygons = new MultiPolygonOptions();

        for (Polygon polygon : multiPolygon.getPolygons()) {
            PolygonOptions polygonOptions = toPolygon(polygon);
            polygons.add(polygonOptions);
        }

        return polygons;
    }

    /**
     * Convert a list of {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link MultiPolygon}
     *
     * @param polygonList polygon list
     * @return multi polygon
     */
    public MultiPolygon toMultiPolygon(
            List<com.google.android.gms.maps.model.Polygon> polygonList) {
        return toMultiPolygon(polygonList, false, false);
    }

    /**
     * Convert a list of {@link com.google.android.gms.maps.model.Polygon} to a
     * {@link MultiPolygon}
     *
     * @param polygonList polygon list
     * @param hasZ        has z flag
     * @param hasM        has m flag
     * @return multi polygon
     */
    public MultiPolygon toMultiPolygon(
            List<com.google.android.gms.maps.model.Polygon> polygonList,
            boolean hasZ, boolean hasM) {

        MultiPolygon multiPolygon = new MultiPolygon(hasZ, hasM);

        for (com.google.android.gms.maps.model.Polygon mapPolygon : polygonList) {
            Polygon polygon = toPolygon(mapPolygon);
            multiPolygon.addPolygon(polygon);
        }

        return multiPolygon;
    }

    /**
     * Convert a list of {@link Polygon} to a {@link MultiPolygon}
     *
     * @param polygonList polygon list
     * @return multi polygon
     */
    public MultiPolygon createMultiPolygon(List<Polygon> polygonList) {
        return createMultiPolygon(polygonList, false, false);
    }

    /**
     * Convert a list of {@link Polygon} to a {@link MultiPolygon}
     *
     * @param polygonList polygon list
     * @param hasZ        has z flag
     * @param hasM        has m flag
     * @return multi polygon
     */
    public MultiPolygon createMultiPolygon(List<Polygon> polygonList,
                                           boolean hasZ, boolean hasM) {

        MultiPolygon multiPolygon = new MultiPolygon(hasZ, hasM);

        for (Polygon polygon : polygonList) {
            multiPolygon.addPolygon(polygon);
        }

        return multiPolygon;
    }

    /**
     * Convert a {@link MultiPolygonOptions} to a {@link MultiPolygon}
     *
     * @param multiPolygonOptions multi polygon options
     * @return multi polygon
     */
    public MultiPolygon toMultiPolygonFromOptions(
            MultiPolygonOptions multiPolygonOptions) {
        return toMultiPolygonFromOptions(multiPolygonOptions, false, false);
    }

    /**
     * Convert a list of {@link PolygonOptions} to a {@link MultiPolygon}
     *
     * @param multiPolygonOptions multi polygon options
     * @param hasZ                has z flag
     * @param hasM                has m flag
     * @return multi polygon
     */
    public MultiPolygon toMultiPolygonFromOptions(
            MultiPolygonOptions multiPolygonOptions, boolean hasZ, boolean hasM) {

        MultiPolygon multiPolygon = new MultiPolygon(hasZ, hasM);

        for (PolygonOptions mapPolygon : multiPolygonOptions
                .getPolygonOptions()) {
            Polygon polygon = toPolygon(mapPolygon);
            multiPolygon.addPolygon(polygon);
        }

        return multiPolygon;
    }

    /**
     * Convert a {@link CompoundCurve} to a {@link MultiPolylineOptions}
     *
     * @param compoundCurve compound curve
     * @return multi polyline options
     */
    public MultiPolylineOptions toPolylines(CompoundCurve compoundCurve) {

        MultiPolylineOptions polylines = new MultiPolylineOptions();

        for (LineString lineString : compoundCurve.getLineStrings()) {
            PolylineOptions polyline = toPolyline(lineString);
            polylines.add(polyline);
        }

        return polylines;
    }

    /**
     * Convert a list of {@link Polyline} to a {@link CompoundCurve}
     *
     * @param polylineList polyline list
     * @return compound curve
     */
    public CompoundCurve toCompoundCurve(List<Polyline> polylineList) {
        return toCompoundCurve(polylineList, false, false);
    }

    /**
     * Convert a list of {@link Polyline} to a {@link CompoundCurve}
     *
     * @param polylineList polyline list
     * @param hasZ         has z flag
     * @param hasM         has m flag
     * @return compound curve
     */
    public CompoundCurve toCompoundCurve(List<Polyline> polylineList,
                                         boolean hasZ, boolean hasM) {

        CompoundCurve compoundCurve = new CompoundCurve(hasZ, hasM);

        for (Polyline polyline : polylineList) {
            LineString lineString = toLineString(polyline);
            compoundCurve.addLineString(lineString);
        }

        return compoundCurve;
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link CompoundCurve}
     *
     * @param multiPolylineOptions multi polyline options
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveWithOptions(
            MultiPolylineOptions multiPolylineOptions) {
        return toCompoundCurveWithOptions(multiPolylineOptions, false, false);
    }

    /**
     * Convert a {@link MultiPolylineOptions} to a {@link CompoundCurve}
     *
     * @param multiPolylineOptions multi polyline options
     * @param hasZ                 has z flag
     * @param hasM                 has m flag
     * @return compound curve
     */
    public CompoundCurve toCompoundCurveWithOptions(
            MultiPolylineOptions multiPolylineOptions, boolean hasZ,
            boolean hasM) {

        CompoundCurve compoundCurve = new CompoundCurve(hasZ, hasM);

        for (PolylineOptions polyline : multiPolylineOptions
                .getPolylineOptions()) {
            LineString lineString = toLineString(polyline);
            compoundCurve.addLineString(lineString);
        }

        return compoundCurve;
    }

    /**
     * Convert a {@link PolyhedralSurface} to a {@link MultiPolygonOptions}
     *
     * @param polyhedralSurface polyhedral surface
     * @return multi polygon options
     */
    public MultiPolygonOptions toPolygons(PolyhedralSurface polyhedralSurface) {

        MultiPolygonOptions polygons = new MultiPolygonOptions();

        for (Polygon polygon : polyhedralSurface.getPolygons()) {
            PolygonOptions polygonOptions = toPolygon(polygon);
            polygons.add(polygonOptions);
        }

        return polygons;
    }

    /**
     * Convert a list of {@link Polygon} to a {@link PolyhedralSurface}
     *
     * @param polygonList polygon list
     * @return polyhedral surface
     */
    public PolyhedralSurface toPolyhedralSurface(
            List<com.google.android.gms.maps.model.Polygon> polygonList) {
        return toPolyhedralSurface(polygonList, false, false);
    }

    /**
     * Convert a list of {@link Polygon} to a {@link PolyhedralSurface}
     *
     * @param polygonList polygon list
     * @param hasZ        has z flag
     * @param hasM        has m flag
     * @return polyhedral surface
     */
    public PolyhedralSurface toPolyhedralSurface(
            List<com.google.android.gms.maps.model.Polygon> polygonList,
            boolean hasZ, boolean hasM) {

        PolyhedralSurface polyhedralSurface = new PolyhedralSurface(hasZ, hasM);

        for (com.google.android.gms.maps.model.Polygon mapPolygon : polygonList) {
            Polygon polygon = toPolygon(mapPolygon);
            polyhedralSurface.addPolygon(polygon);
        }

        return polyhedralSurface;
    }

    /**
     * Convert a {@link MultiPolygonOptions} to a {@link PolyhedralSurface}
     *
     * @param multiPolygonOptions multi polygon options
     * @return polyhedral surface
     */
    public PolyhedralSurface toPolyhedralSurfaceWithOptions(
            MultiPolygonOptions multiPolygonOptions) {
        return toPolyhedralSurfaceWithOptions(multiPolygonOptions, false, false);
    }

    /**
     * Convert a {@link MultiPolygonOptions} to a {@link PolyhedralSurface}
     *
     * @param multiPolygonOptions multi polygon options
     * @param hasZ                has z flag
     * @param hasM                has m flag
     * @return polyhedral surface
     */
    public PolyhedralSurface toPolyhedralSurfaceWithOptions(
            MultiPolygonOptions multiPolygonOptions, boolean hasZ, boolean hasM) {

        PolyhedralSurface polyhedralSurface = new PolyhedralSurface(hasZ, hasM);

        for (PolygonOptions mapPolygon : multiPolygonOptions
                .getPolygonOptions()) {
            Polygon polygon = toPolygon(mapPolygon);
            polyhedralSurface.addPolygon(polygon);
        }

        return polyhedralSurface;
    }

    /**
     * Convert a {@link Geometry} to a Map shape
     *
     * @param geometry geometry
     * @return google map shape
     */
    @SuppressWarnings("unchecked")
    public GoogleMapShape toShape(Geometry geometry) {

        GoogleMapShape shape = null;

        GeometryType geometryType = geometry.getGeometryType();
        switch (geometryType) {
            case POINT:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.LAT_LNG, toLatLng((Point) geometry));
                break;
            case LINESTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYLINE_OPTIONS,
                        toPolyline((LineString) geometry));
                break;
            case POLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON_OPTIONS,
                        toPolygon((Polygon) geometry));
                break;
            case MULTIPOINT:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_LAT_LNG,
                        toLatLngs((MultiPoint) geometry));
                break;
            case MULTILINESTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYLINE_OPTIONS,
                        toPolylines((MultiLineString) geometry));
                break;
            case MULTIPOLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON_OPTIONS,
                        toPolygons((MultiPolygon) geometry));
                break;
            case CIRCULARSTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYLINE_OPTIONS,
                        toPolyline((CircularString) geometry));
                break;
            case COMPOUNDCURVE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYLINE_OPTIONS,
                        toPolylines((CompoundCurve) geometry));
                break;
            case CURVEPOLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON_OPTIONS,
                        toCurvePolygon((CurvePolygon) geometry));
                break;
            case POLYHEDRALSURFACE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON_OPTIONS,
                        toPolygons((PolyhedralSurface) geometry));
                break;
            case TIN:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON_OPTIONS,
                        toPolygons((TIN) geometry));
                break;
            case TRIANGLE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON_OPTIONS,
                        toPolygon((Triangle) geometry));
                break;
            case GEOMETRYCOLLECTION:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.COLLECTION,
                        toShapes((GeometryCollection<Geometry>) geometry));
                break;
            default:
                throw new GeoPackageException("Unsupported Geometry Type: "
                        + geometryType.getName());
        }

        return shape;
    }

    /**
     * Convert a {@link GeometryCollection} to a list of Map shapes
     *
     * @param geometryCollection geometry collection
     * @return google map shapes
     */
    public List<GoogleMapShape> toShapes(
            GeometryCollection<Geometry> geometryCollection) {

        List<GoogleMapShape> shapes = new ArrayList<GoogleMapShape>();

        for (Geometry geometry : geometryCollection.getGeometries()) {
            GoogleMapShape shape = toShape(geometry);
            shapes.add(shape);
        }

        return shapes;
    }

    /**
     * Convert a {@link Geometry} to a Map shape and add it
     *
     * @param map      google map
     * @param geometry geometry
     * @return google map shape
     */
    @SuppressWarnings("unchecked")
    public GoogleMapShape addToMap(GoogleMap map, Geometry geometry) {

        GoogleMapShape shape = null;

        GeometryType geometryType = geometry.getGeometryType();
        switch (geometryType) {
            case POINT:
                shape = new GoogleMapShape(geometryType, GoogleMapShapeType.MARKER,
                        addLatLngToMap(map, toLatLng((Point) geometry)));
                break;
            case LINESTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYLINE, addPolylineToMap(map,
                        toPolyline((LineString) geometry)));
                break;
            case POLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON, addPolygonToMap(map,
                        toPolygon((Polygon) geometry)));
                break;
            case MULTIPOINT:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_MARKER, addLatLngsToMap(map,
                        toLatLngs((MultiPoint) geometry)));
                break;
            case MULTILINESTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYLINE, addPolylinesToMap(map,
                        toPolylines((MultiLineString) geometry)));
                break;
            case MULTIPOLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON, addPolygonsToMap(map,
                        toPolygons((MultiPolygon) geometry)));
                break;
            case CIRCULARSTRING:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYLINE, addPolylineToMap(map,
                        toPolyline((CircularString) geometry)));
                break;
            case COMPOUNDCURVE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYLINE, addPolylinesToMap(map,
                        toPolylines((CompoundCurve) geometry)));
                break;
            case CURVEPOLYGON:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON, addPolygonToMap(map,
                        toCurvePolygon((CurvePolygon) geometry)));
                break;
            case POLYHEDRALSURFACE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON, addPolygonsToMap(map,
                        toPolygons((PolyhedralSurface) geometry)));
                break;
            case TIN:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.MULTI_POLYGON, addPolygonsToMap(map,
                        toPolygons((TIN) geometry)));
                break;
            case TRIANGLE:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.POLYGON, addPolygonToMap(map,
                        toPolygon((Triangle) geometry)));
                break;
            case GEOMETRYCOLLECTION:
                shape = new GoogleMapShape(geometryType,
                        GoogleMapShapeType.COLLECTION, addToMap(map,
                        (GeometryCollection<Geometry>) geometry));
                break;
            default:
                throw new GeoPackageException("Unsupported Geometry Type: "
                        + geometryType.getName());
        }

        return shape;
    }

    /**
     * Add a shape to the map
     *
     * @param map   google map
     * @param shape google map shape
     * @return google map shape
     */
    public static GoogleMapShape addShapeToMap(GoogleMap map,
                                               GoogleMapShape shape) {

        GoogleMapShape addedShape = null;

        switch (shape.getShapeType()) {

            case LAT_LNG:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MARKER, addLatLngToMap(map,
                        (LatLng) shape.getShape()));
                break;
            case MARKER_OPTIONS:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MARKER, addMarkerOptionsToMap(map,
                        (MarkerOptions) shape.getShape()));
                break;
            case POLYLINE_OPTIONS:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.POLYLINE, addPolylineToMap(map,
                        (PolylineOptions) shape.getShape()));
                break;
            case POLYGON_OPTIONS:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.POLYGON, addPolygonToMap(map,
                        (PolygonOptions) shape.getShape()));
                break;
            case MULTI_LAT_LNG:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_MARKER, addLatLngsToMap(map,
                        (MultiLatLng) shape.getShape()));
                break;
            case MULTI_POLYLINE_OPTIONS:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_POLYLINE, addPolylinesToMap(map,
                        (MultiPolylineOptions) shape.getShape()));
                break;
            case MULTI_POLYGON_OPTIONS:
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_POLYGON, addPolygonsToMap(map,
                        (MultiPolygonOptions) shape.getShape()));
                break;
            case COLLECTION:
                List<GoogleMapShape> addedShapeList = new ArrayList<GoogleMapShape>();
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape
                        .getShape();
                for (GoogleMapShape shapeListItem : shapeList) {
                    addedShapeList.add(addShapeToMap(map, shapeListItem));
                }
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.COLLECTION, addedShapeList);
                break;
            default:
                throw new GeoPackageException("Unsupported Shape Type: "
                        + shape.getShapeType());

        }

        return addedShape;
    }

    /**
     * Add a LatLng to the map
     *
     * @param map    google map
     * @param latLng lat lng
     * @return marker
     */
    public static Marker addLatLngToMap(GoogleMap map, LatLng latLng) {
        return addLatLngToMap(map, latLng, new MarkerOptions());
    }

    /**
     * Add MarkerOptions to the map
     *
     * @param map     google map
     * @param options marker options
     * @return marker
     */
    public static Marker addMarkerOptionsToMap(GoogleMap map,
                                               MarkerOptions options) {
        return map.addMarker(options);
    }

    /**
     * Add a LatLng to the map
     *
     * @param map     google map
     * @param latLng  lat lng
     * @param options marker options
     * @return marker
     */
    public static Marker addLatLngToMap(GoogleMap map, LatLng latLng,
                                        MarkerOptions options) {
        return map.addMarker(options.position(latLng));
    }

    /**
     * Add a Polyline to the map
     *
     * @param map      google map
     * @param polyline polyline options
     * @return polyline
     */
    public static Polyline addPolylineToMap(GoogleMap map,
                                            PolylineOptions polyline) {
        return map.addPolyline(polyline);
    }

    /**
     * Add a Polygon to the map
     *
     * @param map     google map
     * @param polygon polygon options
     * @return polygon
     */
    public static com.google.android.gms.maps.model.Polygon addPolygonToMap(
            GoogleMap map, PolygonOptions polygon) {
        return map.addPolygon(polygon);
    }

    /**
     * Add a list of LatLngs to the map
     *
     * @param map     google map
     * @param latLngs lat lngs
     * @return multi marker
     */
    public static MultiMarker addLatLngsToMap(GoogleMap map, MultiLatLng latLngs) {
        MultiMarker multiMarker = new MultiMarker();
        for (LatLng latLng : latLngs.getLatLngs()) {
            MarkerOptions markerOptions = new MarkerOptions();
            if (latLngs.getMarkerOptions() != null) {
                markerOptions.icon(latLngs.getMarkerOptions().getIcon());
                markerOptions.anchor(latLngs.getMarkerOptions().getAnchorU(),
                        markerOptions.getAnchorV());
                markerOptions.draggable(latLngs.getMarkerOptions()
                        .isDraggable());
                markerOptions.visible(latLngs.getMarkerOptions().isVisible());
                markerOptions.zIndex(latLngs.getMarkerOptions().getZIndex());
            }
            Marker marker = addLatLngToMap(map, latLng, markerOptions);
            multiMarker.add(marker);
        }
        return multiMarker;
    }

    /**
     * Add a list of Polylines to the map
     *
     * @param map       google map
     * @param polylines multi polyline options
     * @return multi polyline
     */
    public static MultiPolyline addPolylinesToMap(GoogleMap map,
                                                  MultiPolylineOptions polylines) {
        MultiPolyline multiPolyline = new MultiPolyline();
        for (PolylineOptions polylineOption : polylines.getPolylineOptions()) {
            if (polylines.getOptions() != null) {
                polylineOption.color(polylines.getOptions().getColor());
                polylineOption.geodesic(polylines.getOptions().isGeodesic());
                polylineOption.visible(polylines.getOptions().isVisible());
                polylineOption.zIndex(polylines.getOptions().getZIndex());
                polylineOption.width(polylines.getOptions().getWidth());
            }
            Polyline polyline = addPolylineToMap(map, polylineOption);
            multiPolyline.add(polyline);
        }
        return multiPolyline;
    }

    /**
     * Add a list of Polygons to the map
     *
     * @param map      google map
     * @param polygons multi polygon options
     * @return multi polygon
     */
    public static mil.nga.geopackage.map.geom.MultiPolygon addPolygonsToMap(
            GoogleMap map, MultiPolygonOptions polygons) {
        mil.nga.geopackage.map.geom.MultiPolygon multiPolygon = new mil.nga.geopackage.map.geom.MultiPolygon();
        for (PolygonOptions polygonOption : polygons.getPolygonOptions()) {
            if (polygons.getOptions() != null) {
                polygonOption.fillColor(polygons.getOptions().getFillColor());
                polygonOption.strokeColor(polygons.getOptions()
                        .getStrokeColor());
                polygonOption.geodesic(polygons.getOptions().isGeodesic());
                polygonOption.visible(polygons.getOptions().isVisible());
                polygonOption.zIndex(polygons.getOptions().getZIndex());
                polygonOption.strokeWidth(polygons.getOptions().getStrokeWidth());
            }
            com.google.android.gms.maps.model.Polygon polygon = addPolygonToMap(
                    map, polygonOption);
            multiPolygon.add(polygon);
        }
        return multiPolygon;
    }

    /**
     * Convert a {@link GeometryCollection} to a list of Map shapes and add to
     * the map
     *
     * @param map                google map
     * @param geometryCollection geometry collection
     * @return google map shapes
     */
    public List<GoogleMapShape> addToMap(GoogleMap map,
                                         GeometryCollection<Geometry> geometryCollection) {

        List<GoogleMapShape> shapes = new ArrayList<GoogleMapShape>();

        for (Geometry geometry : geometryCollection.getGeometries()) {
            GoogleMapShape shape = addToMap(map, geometry);
            shapes.add(shape);
        }

        return shapes;
    }

    /**
     * Add a shape to the map as markers
     *
     * @param map                      google map
     * @param shape                    google map shape
     * @param markerOptions            marker options
     * @param polylineMarkerOptions    polyline marker options
     * @param polygonMarkerOptions     polygon marker options
     * @param polygonMarkerHoleOptions polygon marker hole options
     * @param globalPolylineOptions    global polyline options
     * @param globalPolygonOptions     global polygon options
     * @return google map shape markers
     */
    public GoogleMapShapeMarkers addShapeToMapAsMarkers(GoogleMap map,
                                                        GoogleMapShape shape, MarkerOptions markerOptions,
                                                        MarkerOptions polylineMarkerOptions,
                                                        MarkerOptions polygonMarkerOptions,
                                                        MarkerOptions polygonMarkerHoleOptions,
                                                        PolylineOptions globalPolylineOptions,
                                                        PolygonOptions globalPolygonOptions) {

        GoogleMapShapeMarkers shapeMarkers = new GoogleMapShapeMarkers();
        GoogleMapShape addedShape = null;

        switch (shape.getShapeType()) {

            case LAT_LNG:
                if (markerOptions == null) {
                    markerOptions = new MarkerOptions();
                }
                Marker latLngMarker = addLatLngToMap(map,
                        (LatLng) shape.getShape(), markerOptions);
                shapeMarkers.add(latLngMarker);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MARKER, latLngMarker);
                break;
            case MARKER_OPTIONS:
                MarkerOptions shapeMarkerOptions = (MarkerOptions) shape.getShape();
                if (markerOptions != null) {
                    shapeMarkerOptions.icon(markerOptions.getIcon());
                    shapeMarkerOptions.anchor(markerOptions.getAnchorU(),
                            markerOptions.getAnchorV());
                    shapeMarkerOptions.draggable(markerOptions.isDraggable());
                    shapeMarkerOptions.visible(markerOptions.isVisible());
                    shapeMarkerOptions.zIndex(markerOptions.getZIndex());
                }
                Marker markerOptionsMarker = addMarkerOptionsToMap(map,
                        shapeMarkerOptions);
                shapeMarkers.add(markerOptionsMarker);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MARKER, markerOptionsMarker);
                break;
            case POLYLINE_OPTIONS:
                PolylineMarkers polylineMarkers = addPolylineToMapAsMarkers(map,
                        (PolylineOptions) shape.getShape(), polylineMarkerOptions,
                        globalPolylineOptions);
                shapeMarkers.add(polylineMarkers);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.POLYLINE_MARKERS, polylineMarkers);
                break;
            case POLYGON_OPTIONS:
                PolygonMarkers polygonMarkers = addPolygonToMapAsMarkers(
                        shapeMarkers, map, (PolygonOptions) shape.getShape(),
                        polygonMarkerOptions, polygonMarkerHoleOptions,
                        globalPolygonOptions);
                shapeMarkers.add(polygonMarkers);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.POLYGON_MARKERS, polygonMarkers);
                break;
            case MULTI_LAT_LNG:
                MultiLatLng multiLatLng = (MultiLatLng) shape.getShape();
                if (markerOptions != null) {
                    multiLatLng.setMarkerOptions(markerOptions);
                }
                MultiMarker multiMarker = addLatLngsToMap(map, multiLatLng);
                shapeMarkers.add(multiMarker);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_MARKER, multiMarker);
                break;
            case MULTI_POLYLINE_OPTIONS:
                MultiPolylineMarkers multiPolylineMarkers = addMultiPolylineToMapAsMarkers(
                        shapeMarkers, map, (MultiPolylineOptions) shape.getShape(),
                        polylineMarkerOptions, globalPolylineOptions);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_POLYLINE_MARKERS,
                        multiPolylineMarkers);
                break;
            case MULTI_POLYGON_OPTIONS:
                MultiPolygonMarkers multiPolygonMarkers = addMultiPolygonToMapAsMarkers(
                        shapeMarkers, map, (MultiPolygonOptions) shape.getShape(),
                        polygonMarkerOptions, polygonMarkerHoleOptions,
                        globalPolygonOptions);
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.MULTI_POLYGON_MARKERS,
                        multiPolygonMarkers);
                break;
            case COLLECTION:
                List<GoogleMapShape> addedShapeList = new ArrayList<GoogleMapShape>();
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape
                        .getShape();
                for (GoogleMapShape shapeListItem : shapeList) {
                    GoogleMapShapeMarkers shapeListItemMarkers = addShapeToMapAsMarkers(
                            map, shapeListItem, markerOptions,
                            polylineMarkerOptions, polygonMarkerOptions,
                            polygonMarkerHoleOptions, globalPolylineOptions,
                            globalPolygonOptions);
                    shapeMarkers.add(shapeListItemMarkers);
                    addedShapeList.add(shapeListItemMarkers.getShape());
                }
                addedShape = new GoogleMapShape(shape.getGeometryType(),
                        GoogleMapShapeType.COLLECTION, addedShapeList);
                break;
            default:
                throw new GeoPackageException("Unsupported Shape Type: "
                        + shape.getShapeType());

        }

        shapeMarkers.setShape(addedShape);

        return shapeMarkers;
    }

    /**
     * Add the list of points as markers
     *
     * @param map                 google map
     * @param points              points
     * @param customMarkerOptions custom marker options
     * @param ignoreIdenticalEnds ignore identical ends flag
     * @return list of markers
     */
    public List<Marker> addPointsToMapAsMarkers(GoogleMap map,
                                                List<LatLng> points, MarkerOptions customMarkerOptions,
                                                boolean ignoreIdenticalEnds) {

        List<Marker> markers = new ArrayList<Marker>();
        for (int i = 0; i < points.size(); i++) {
            LatLng latLng = points.get(i);

            if (points.size() > 1 && i + 1 == points.size() && ignoreIdenticalEnds) {
                LatLng firstLatLng = points.get(0);
                if (latLng.latitude == firstLatLng.latitude
                        && latLng.longitude == firstLatLng.longitude) {
                    break;
                }
            }

            MarkerOptions markerOptions = new MarkerOptions();
            if (customMarkerOptions != null) {
                markerOptions.icon(customMarkerOptions.getIcon());
                markerOptions.anchor(customMarkerOptions.getAnchorU(),
                        customMarkerOptions.getAnchorV());
                markerOptions.draggable(customMarkerOptions.isDraggable());
                markerOptions.visible(customMarkerOptions.isVisible());
                markerOptions.zIndex(customMarkerOptions.getZIndex());
            }
            Marker marker = addLatLngToMap(map, latLng, markerOptions);
            markers.add(marker);
        }
        return markers;
    }

    /**
     * Add a Polyline to the map as markers
     *
     * @param map                   google map
     * @param polylineOptions       polyline options
     * @param polylineMarkerOptions polyline marker options
     * @param globalPolylineOptions global polyline options
     * @return polyline markers
     */
    public PolylineMarkers addPolylineToMapAsMarkers(GoogleMap map,
                                                     PolylineOptions polylineOptions,
                                                     MarkerOptions polylineMarkerOptions,
                                                     PolylineOptions globalPolylineOptions) {

        PolylineMarkers polylineMarkers = new PolylineMarkers(this);

        if (globalPolylineOptions != null) {
            polylineOptions.color(globalPolylineOptions.getColor());
            polylineOptions.geodesic(globalPolylineOptions.isGeodesic());
            polylineOptions.visible(globalPolylineOptions.isVisible());
            polylineOptions.zIndex(globalPolylineOptions.getZIndex());
            polylineOptions.width(globalPolylineOptions.getWidth());
        }

        Polyline polyline = addPolylineToMap(map, polylineOptions);
        polylineMarkers.setPolyline(polyline);

        List<Marker> markers = addPointsToMapAsMarkers(map,
                polylineOptions.getPoints(), polylineMarkerOptions, false);
        polylineMarkers.setMarkers(markers);

        return polylineMarkers;
    }

    /**
     * Add a Polygon to the map as markers
     *
     * @param shapeMarkers             google map shape markers
     * @param map                      google map
     * @param polygonOptions           polygon options
     * @param polygonMarkerOptions     polygon marker options
     * @param polygonMarkerHoleOptions polygon marker hole options
     * @param globalPolygonOptions     global polygon options
     * @return polygon markers
     */
    public PolygonMarkers addPolygonToMapAsMarkers(
            GoogleMapShapeMarkers shapeMarkers, GoogleMap map,
            PolygonOptions polygonOptions, MarkerOptions polygonMarkerOptions,
            MarkerOptions polygonMarkerHoleOptions,
            PolygonOptions globalPolygonOptions) {

        PolygonMarkers polygonMarkers = new PolygonMarkers(this);

        if (globalPolygonOptions != null) {
            polygonOptions.fillColor(globalPolygonOptions.getFillColor());
            polygonOptions.strokeColor(globalPolygonOptions.getStrokeColor());
            polygonOptions.geodesic(globalPolygonOptions.isGeodesic());
            polygonOptions.visible(globalPolygonOptions.isVisible());
            polygonOptions.zIndex(globalPolygonOptions.getZIndex());
            polygonOptions.strokeWidth(globalPolygonOptions.getStrokeWidth());
        }

        com.google.android.gms.maps.model.Polygon polygon = addPolygonToMap(
                map, polygonOptions);
        polygonMarkers.setPolygon(polygon);

        List<Marker> markers = addPointsToMapAsMarkers(map,
                polygon.getPoints(), polygonMarkerOptions, true);
        polygonMarkers.setMarkers(markers);

        for (List<LatLng> holes : polygon.getHoles()) {
            List<Marker> holeMarkers = addPointsToMapAsMarkers(map, holes,
                    polygonMarkerHoleOptions, true);
            PolygonHoleMarkers polygonHoleMarkers = new PolygonHoleMarkers(
                    polygonMarkers);
            polygonHoleMarkers.setMarkers(holeMarkers);
            shapeMarkers.add(polygonHoleMarkers);
            polygonMarkers.addHole(polygonHoleMarkers);
        }

        return polygonMarkers;
    }

    /**
     * Add a MultiPolylineOptions to the map as markers
     *
     * @param shapeMarkers          google map shape markers
     * @param map                   google map
     * @param multiPolyline         multi polyline options
     * @param polylineMarkerOptions polyline marker options
     * @param globalPolylineOptions global polyline options
     * @return multi polyline markers
     */
    public MultiPolylineMarkers addMultiPolylineToMapAsMarkers(
            GoogleMapShapeMarkers shapeMarkers, GoogleMap map,
            MultiPolylineOptions multiPolyline,
            MarkerOptions polylineMarkerOptions,
            PolylineOptions globalPolylineOptions) {
        MultiPolylineMarkers polylines = new MultiPolylineMarkers();
        for (PolylineOptions polylineOptions : multiPolyline
                .getPolylineOptions()) {
            PolylineMarkers polylineMarker = addPolylineToMapAsMarkers(map,
                    polylineOptions, polylineMarkerOptions,
                    globalPolylineOptions);
            shapeMarkers.add(polylineMarker);
            polylines.add(polylineMarker);
        }
        return polylines;
    }

    /**
     * Add a MultiPolygonOptions to the map as markers
     *
     * @param shapeMarkers             google map shape markers
     * @param map                      google map
     * @param multiPolygon             multi polygon options
     * @param polygonMarkerOptions     polygon marker options
     * @param polygonMarkerHoleOptions polygon marker hole options
     * @param globalPolygonOptions     global polygon options
     * @return multi polygon markers
     */
    public MultiPolygonMarkers addMultiPolygonToMapAsMarkers(
            GoogleMapShapeMarkers shapeMarkers, GoogleMap map,
            MultiPolygonOptions multiPolygon,
            MarkerOptions polygonMarkerOptions,
            MarkerOptions polygonMarkerHoleOptions,
            PolygonOptions globalPolygonOptions) {
        MultiPolygonMarkers multiPolygonMarkers = new MultiPolygonMarkers();
        for (PolygonOptions polygon : multiPolygon.getPolygonOptions()) {
            PolygonMarkers polygonMarker = addPolygonToMapAsMarkers(
                    shapeMarkers, map, polygon, polygonMarkerOptions,
                    polygonMarkerHoleOptions, globalPolygonOptions);
            shapeMarkers.add(polygonMarker);
            multiPolygonMarkers.add(polygonMarker);
        }
        return multiPolygonMarkers;
    }

    /**
     * Get a list of points as LatLng from a list of Markers
     *
     * @param markers list of markers
     * @return lat lngs
     */
    public List<LatLng> getPointsFromMarkers(List<Marker> markers) {
        List<LatLng> points = new ArrayList<LatLng>();
        for (Marker marker : markers) {
            points.add(marker.getPosition());
        }
        return points;
    }

    /**
     * Convert a GoogleMapShape to a Geometry
     *
     * @param shape google map shape
     * @return geometry
     */
    public Geometry toGeometry(GoogleMapShape shape) {

        Geometry geometry = null;
        Object shapeObject = shape.getShape();

        switch (shape.getGeometryType()) {

            case POINT:
                LatLng point = null;
                switch (shape.getShapeType()) {
                    case LAT_LNG:
                        point = (LatLng) shapeObject;
                        break;
                    case MARKER_OPTIONS:
                        MarkerOptions markerOptions = (MarkerOptions) shapeObject;
                        point = markerOptions.getPosition();
                        break;
                    case MARKER:
                        Marker marker = (Marker) shapeObject;
                        point = marker.getPosition();
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }
                if (point != null) {
                    geometry = toPoint(point);
                }

                break;
            case LINESTRING:
            case CIRCULARSTRING:
                List<LatLng> lineStringPoints = null;
                switch (shape.getShapeType()) {
                    case POLYLINE_OPTIONS:
                        PolylineOptions polylineOptions = (PolylineOptions) shapeObject;
                        lineStringPoints = polylineOptions.getPoints();
                        break;
                    case POLYLINE:
                        Polyline polyline = (Polyline) shapeObject;
                        lineStringPoints = polyline.getPoints();
                        break;
                    case POLYLINE_MARKERS:
                        PolylineMarkers polylineMarkers = (PolylineMarkers) shapeObject;
                        if (!polylineMarkers.isValid()) {
                            throw new GeoPackageException(
                                    PolylineMarkers.class.getSimpleName()
                                            + " is not valid to create "
                                            + shape.getGeometryType().getName());
                        }
                        if (!polylineMarkers.isDeleted()) {
                            lineStringPoints = getPointsFromMarkers(polylineMarkers
                                    .getMarkers());
                        }
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }
                if (lineStringPoints != null) {
                    switch (shape.getGeometryType()) {
                        case LINESTRING:
                            geometry = toLineString(lineStringPoints);
                            break;
                        case CIRCULARSTRING:
                            geometry = toCircularString(lineStringPoints);
                            break;
                        default:
                            throw new GeoPackageException("Unhandled "
                                    + shape.getGeometryType().getName());
                    }
                }

                break;
            case POLYGON:
                List<LatLng> polygonPoints = null;
                List<List<LatLng>> holePointList = null;
                switch (shape.getShapeType()) {
                    case POLYGON_OPTIONS:
                        PolygonOptions polygonOptions = (PolygonOptions) shapeObject;
                        polygonPoints = polygonOptions.getPoints();
                        holePointList = polygonOptions.getHoles();
                        break;
                    case POLYGON:
                        com.google.android.gms.maps.model.Polygon polygon = (com.google.android.gms.maps.model.Polygon) shapeObject;
                        polygonPoints = polygon.getPoints();
                        holePointList = polygon.getHoles();
                        break;
                    case POLYGON_MARKERS:
                        PolygonMarkers polygonMarkers = (PolygonMarkers) shapeObject;
                        if (!polygonMarkers.isValid()) {
                            throw new GeoPackageException(
                                    PolygonMarkers.class.getSimpleName()
                                            + " is not valid to create "
                                            + shape.getGeometryType().getName());
                        }
                        if (!polygonMarkers.isDeleted()) {
                            polygonPoints = getPointsFromMarkers(polygonMarkers
                                    .getMarkers());
                            holePointList = new ArrayList<List<LatLng>>();
                            for (PolygonHoleMarkers hole : polygonMarkers.getHoles()) {
                                if (!hole.isDeleted()) {
                                    List<LatLng> holePoints = getPointsFromMarkers(hole
                                            .getMarkers());
                                    holePointList.add(holePoints);
                                }
                            }
                        }
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }
                if (polygonPoints != null) {
                    geometry = toPolygon(polygonPoints, holePointList);
                }

                break;
            case MULTIPOINT:
                List<LatLng> multiPoints = null;
                switch (shape.getShapeType()) {
                    case MULTI_LAT_LNG:
                        MultiLatLng multiLatLng = (MultiLatLng) shapeObject;
                        multiPoints = multiLatLng.getLatLngs();
                        break;
                    case MULTI_MARKER:
                        MultiMarker multiMarker = (MultiMarker) shapeObject;
                        multiPoints = getPointsFromMarkers(multiMarker.getMarkers());
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }
                if (multiPoints != null) {
                    geometry = toMultiPoint(multiPoints);
                }

                break;
            case MULTILINESTRING:
            case COMPOUNDCURVE:
                switch (shape.getShapeType()) {
                    case MULTI_POLYLINE_OPTIONS:
                        MultiPolylineOptions multiPolylineOptions = (MultiPolylineOptions) shapeObject;
                        switch (shape.getGeometryType()) {
                            case MULTILINESTRING:
                                geometry = toMultiLineStringFromOptions(multiPolylineOptions);
                                break;
                            case COMPOUNDCURVE:
                                geometry = toCompoundCurveFromOptions(multiPolylineOptions);
                                break;
                            default:
                                throw new GeoPackageException("Unhandled "
                                        + shape.getGeometryType().getName());
                        }
                        break;
                    case MULTI_POLYLINE:
                        MultiPolyline multiPolyline = (MultiPolyline) shapeObject;
                        switch (shape.getGeometryType()) {
                            case MULTILINESTRING:
                                geometry = toMultiLineString(multiPolyline.getPolylines());
                                break;
                            case COMPOUNDCURVE:
                                geometry = toCompoundCurve(multiPolyline.getPolylines());
                                break;
                            default:
                                throw new GeoPackageException("Unhandled "
                                        + shape.getGeometryType().getName());
                        }
                        break;
                    case MULTI_POLYLINE_MARKERS:
                        MultiPolylineMarkers multiPolylineMarkers = (MultiPolylineMarkers) shapeObject;
                        if (!multiPolylineMarkers.isValid()) {
                            throw new GeoPackageException(
                                    MultiPolylineMarkers.class.getSimpleName()
                                            + " is not valid to create "
                                            + shape.getGeometryType().getName());
                        }
                        if (!multiPolylineMarkers.isDeleted()) {
                            List<List<LatLng>> multiPolylineMarkersList = new ArrayList<List<LatLng>>();
                            for (PolylineMarkers polylineMarkers : multiPolylineMarkers
                                    .getPolylineMarkers()) {
                                if (!polylineMarkers.isDeleted()) {
                                    multiPolylineMarkersList
                                            .add(getPointsFromMarkers(polylineMarkers
                                                    .getMarkers()));
                                }
                            }
                            switch (shape.getGeometryType()) {
                                case MULTILINESTRING:
                                    geometry = toMultiLineStringFromList(multiPolylineMarkersList);
                                    break;
                                case COMPOUNDCURVE:
                                    geometry = toCompoundCurveFromList(multiPolylineMarkersList);
                                    break;
                                default:
                                    throw new GeoPackageException("Unhandled "
                                            + shape.getGeometryType().getName());
                            }
                        }
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }

                break;
            case MULTIPOLYGON:
                switch (shape.getShapeType()) {
                    case MULTI_POLYGON_OPTIONS:
                        MultiPolygonOptions multiPolygonOptions = (MultiPolygonOptions) shapeObject;
                        geometry = toMultiPolygonFromOptions(multiPolygonOptions);
                        break;
                    case MULTI_POLYGON:
                        mil.nga.geopackage.map.geom.MultiPolygon multiPolygon = (mil.nga.geopackage.map.geom.MultiPolygon) shapeObject;
                        geometry = toMultiPolygon(multiPolygon.getPolygons());
                        break;
                    case MULTI_POLYGON_MARKERS:
                        MultiPolygonMarkers multiPolygonMarkers = (MultiPolygonMarkers) shapeObject;
                        if (!multiPolygonMarkers.isValid()) {
                            throw new GeoPackageException(
                                    MultiPolygonMarkers.class.getSimpleName()
                                            + " is not valid to create "
                                            + shape.getGeometryType().getName());
                        }
                        if (!multiPolygonMarkers.isDeleted()) {
                            List<Polygon> multiPolygonMarkersList = new ArrayList<Polygon>();
                            for (PolygonMarkers polygonMarkers : multiPolygonMarkers
                                    .getPolygonMarkers()) {

                                if (!polygonMarkers.isDeleted()) {

                                    List<LatLng> multiPolygonPoints = getPointsFromMarkers(polygonMarkers
                                            .getMarkers());
                                    List<List<LatLng>> multiPolygonHolePoints = new ArrayList<List<LatLng>>();
                                    for (PolygonHoleMarkers hole : polygonMarkers
                                            .getHoles()) {
                                        if (!hole.isDeleted()) {
                                            List<LatLng> holePoints = getPointsFromMarkers(hole
                                                    .getMarkers());
                                            multiPolygonHolePoints.add(holePoints);
                                        }
                                    }

                                    multiPolygonMarkersList
                                            .add(toPolygon(multiPolygonPoints,
                                                    multiPolygonHolePoints));
                                }

                            }
                            geometry = createMultiPolygon(multiPolygonMarkersList);
                        }
                        break;
                    default:
                        throw new GeoPackageException("Not a valid "
                                + shape.getGeometryType().getName() + " shape type: "
                                + shape.getShapeType());
                }
                break;

            case POLYHEDRALSURFACE:
            case TIN:
            case TRIANGLE:
                throw new GeoPackageException("Unsupported GeoPackage type: "
                        + shape.getGeometryType());
            case GEOMETRYCOLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shapeObject;
                GeometryCollection<Geometry> geometryCollection = new GeometryCollection<Geometry>(
                        false, false);
                for (GoogleMapShape shapeListItem : shapeList) {
                    Geometry subGeometry = toGeometry(shapeListItem);
                    if (subGeometry != null) {
                        geometryCollection.addGeometry(subGeometry);
                    }
                }
                if (geometryCollection.numGeometries() > 0) {
                    geometry = geometryCollection;
                }
                break;
            default:
        }

        return geometry;
    }

    /**
     * Transform the bounding box in the feature projection to web mercator
     *
     * @param boundingBox bounding box in feature projection
     * @return bounding box in web mercator
     */
    public BoundingBox boundingBoxToWebMercator(BoundingBox boundingBox) {
        if (projection == null) {
            throw new GeoPackageException("Shape Converter projection is null");
        }
        return boundingBox.transform(toWebMercator);
    }

    /**
     * Transform the bounding box in the feature projection to WGS84
     *
     * @param boundingBox bounding box in feature projection
     * @return bounding box in WGS84
     */
    public BoundingBox boundingBoxToWgs84(BoundingBox boundingBox) {
        if (projection == null) {
            throw new GeoPackageException("Shape Converter projection is null");
        }
        return boundingBox.transform(toWgs84);
    }

    /**
     * Transform the bounding box in web mercator to the feature projection
     *
     * @param boundingBox bounding box in web mercator
     * @return bounding box in the feature projection
     */
    public BoundingBox boundingBoxFromWebMercator(BoundingBox boundingBox) {
        if (projection == null) {
            throw new GeoPackageException("Shape Converter projection is null");
        }
        return boundingBox.transform(fromWebMercator);
    }

    /**
     * Transform the bounding box in WGS84 to the feature projection
     *
     * @param boundingBox bounding box in WGS84
     * @return bounding box in the feature projection
     */
    public BoundingBox boundingBoxFromWgs84(BoundingBox boundingBox) {
        if (projection == null) {
            throw new GeoPackageException("Shape Converter projection is null");
        }
        return boundingBox.transform(fromWgs84);
    }

}