/*
 * Copyright (C) 2014 United States Government as represented by the Administrator of the
 * National Aeronautics and Space Administration. All Rights Reserved.
 */
/**
 * @exports GeoJSONParser
 */
define(['../../error/ArgumentError',
        '../../util/Color',
        './GeoJSONConstants',
        './GeoJSONCRS',
        './GeoJSONFeature',
        './GeoJSONFeatureCollection',
        './GeoJSONGeometry',
        './GeoJSONGeometryCollection',
        './GeoJSONGeometryLineString',
        './GeoJSONGeometryMultiLineString',
        './GeoJSONGeometryMultiPoint',
        './GeoJSONGeometryMultiPolygon',
        './GeoJSONGeometryPoint',
        './GeoJSONGeometryPolygon',
        '../../geom/Location',
        '../../util/Logger',
        '../../shapes/Placemark',
        '../../shapes/PlacemarkAttributes',
        '../../shapes/Polygon',
        '../../geom/Position',
        '../../util/proj4-src',
        '../../layer/RenderableLayer',
        '../../shapes/ShapeAttributes',
        '../../shapes/SurfacePolygon',
        '../../shapes/SurfacePolyline'
    ],
    function (ArgumentError,
              Color,
              GeoJSONConstants,
              GeoJSONCRS,
              GeoJSONFeature,
              GeoJSONFeatureCollection,
              GeoJSONGeometry,
              GeoJSONGeometryCollection,
              GeoJSONGeometryLineString,
              GeoJSONGeometryMultiLineString,
              GeoJSONGeometryMultiPoint,
              GeoJSONGeometryMultiPolygon,
              GeoJSONGeometryPoint,
              GeoJSONGeometryPolygon,
              Location,
              Logger,
              Placemark,
              PlacemarkAttributes,
              Polygon,
              Position,
              Proj4,
              RenderableLayer,
              ShapeAttributes,
              SurfacePolygon,
              SurfacePolyline) {
        "use strict";

        /**
         * Constructs a GeoJSON object for a specified GeoJSON data source. Call [load]{@link GeoJSONParser#load} to
         * retrieve the GeoJSON and create shapes for it.
         * @alias GeoJSONParser
         * @constructor
         * @classdesc Parses a GeoJSON and creates shapes representing its contents. Points and MultiPoints in
         * the GeoJSON are represented by [Placemarks]{@link Placemark}, Lines and MultiLines are represented by
         * [SurfacePolylines]{@link SurfacePolyline}, and Polygons and MultiPolygons are represented
         * by [SurfacePolygons]{@link SurfacePolygon}.
         * <p>
         * An attribute callback may also be specified to examine each geometry and configure the shape created for it.
         * This function enables the application to assign independent attributes to each
         * shape. An argument to this function provides any attributes specified in a properties member of GeoJSON
         * feature.
         * @param {String} dataSource The data source of the GeoJSON. Can be a string or an URL to a GeoJSON.
         * @throws {ArgumentError} If the specified data source is null or undefined.
         */
        var GeoJSONParser = function (dataSource) {
            if (!dataSource) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "constructor", "missingDataSource"));
            }

            // Documented in defineProperties below.
            this._dataSource = dataSource;

            // Documented in defineProperties below.
            this._geoJSONObject = null;

            // Documented in defineProperties below.
            this._geoJSONType = null;

            // Documented in defineProperties below.
            this._crs = null;

            // Documented in defineProperties below.
            this._layer = null;

            // Documented in defineProperties below.
            this._parserCompletionCallback = null;

            // Documented in defineProperties below.
            this._shapeConfigurationCallback = this.defaultShapeConfigurationCallback;


            this.defaultPlacemarkAttributes = new PlacemarkAttributes(null);

            this.defaultShapeAttributes = new ShapeAttributes(null);

            this.setProj4jsAliases();
        };

        Object.defineProperties(GeoJSONParser.prototype, {
            /**
             * The GeoJSON data source as specified to this GeoJSON's constructor.
             * @memberof GeoJSONParser.prototype
             * @type {String}
             * @readonly
             */
            dataSource: {
                get: function () {
                    return this._dataSource;
                }
            },

            /**
             * The GeoJSON object resulting from the parsing of GeoJSON string.
             * @memberof GeoJSONParser.prototype
             * @type {Object}
             * @readonly
             */
            geoJSONObject: {
                get: function () {
                    return this._geoJSONObject;
                }
            },

            /**
             * The type of the GeoJSON. The type can be one of the following:
             * <ul>
             *     <li>GeoJSONConstants.TYPE_POINT</li>
             *     <li>GeoJSONConstants.TYPE_MULTI_POINT</li>
             *     <li>GeoJSONConstants.TYPE_LINE_STRING</li>
             *     <li>GeoJSONConstants.TYPE_MULTI_LINE_STRING</li>
             *     <li>GeoJSONConstants.TYPE_POLYGON</li>
             *     <li>GeoJSONConstants.TYPE_MULTI_POLYGON</li>
             *     <li>GeoJSONConstants.TYPE_GEOMETRY_COLLECTION</li>
             *     <li>GeoJSONConstants.TYPE_FEATURE</li>
             *     <li>GeoJSONConstants.TYPE_FEATURE_COLLECTION</li>
             * </ul>
             * This value is defined after GeoJSON parsing.
             * @memberof GeoJSONParser.prototype
             * @type {String}
             * @readonly
             */
            geoJSONType: {
                get: function () {
                    return this._geoJSONType;
                }
            },

            /**
             *
             */
            crs: {
                get: function () {
                    return this._crs;
                }
            },

            /**
             * The layer containing the shapes representing the geometries in this GeoJSON, as specified to this
             * GeoJSON's constructor or created by the constructor if no layer was specified.
             * @memberof GeoJSONParser.prototype
             * @type {RenderableLayer}
             * @readonly
             */
            layer: {
                get: function () {
                    return this._layer;
                }
            },

            /** The completion callback specified to [load]{@link GeoJSONParser#load}. An optional function called when
             * the GeoJSON loading is complete and
             * all the shapes have been added to the layer.
             * @memberof GeoJSONParser.prototype
             * @type {Function}
             * @readonly
             */
            parserCompletionCallback: {
                get: function () {
                    return this._parserCompletionCallback;
                }
            },

            /**
             * The attribute callback specified to [load]{@link GeoJSONParser#load}.
             * See that method's description for details.
             * @memberof GeoJSONParser.prototype
             * @type {Function}
             * @default [defaultShapeConfigurationCallback]{@link GeoJSONParser#defaultShapeConfigurationCallback}
             * @readonly
             */
            shapeConfigurationCallback: {
                get: function () {
                    return this._shapeConfigurationCallback;
                }
            }
        });

        /**
         * Retrieves the GeoJSON, parses it and creates shapes representing its contents. The result is a layer
         * containing the created shapes. A function can also be specified to be called for each GeoJSON geometry so
         * that the attributes and other properties of the shape created for it can be assigned.
         * @param {Function} parserCompletionCallback An optional function called when the GeoJSON loading is
         * complete and all the shapes have been added to the layer.
         * @param {Function} shapeConfigurationCallback An optional function called by the addRenderablesFor*
         * methods just prior to creating a shape for the indicated GeoJSON geometry. This function
         * can be used to assign attributes to newly created shapes. The callback function's first argument is the
         * current geometry object.  The second argument to the callback function is the object containing the
         * properties read from the corresponding GeoJSON properties member, if any.
         * See the following methods for descriptions of the configuration properties they recognize:
         * <ul>
         *     <li>[addRenderablesForPoint]{@link GeoJSONParser#addRenderablesForPoint}</li>
         *     <li>[addRenderablesForMultiPoint]{@link GeoJSONParser#addRenderablesForMultiPoint}</li>
         *     <li>[addRenderablesForLineString]{@link GeoJSONParser#addRenderablesForLineString}</li>
         *     <li>[addRenderablesForMultiLineString]{@link GeoJSONParser#addRenderablesForMultiLineString}</li>
         *     <li>[addRenderablesForPolygon]{@link GeoJSONParser#addRenderablesForPolygon}</li>
         *     <li>[addRenderablesForMultiPolygon]{@link GeoJSONParser#addRenderablesForMultiPolygon}</li>
         *     <li>[addRenderablesForGeometryCollection]{@link GeoJSONParser#addRenderablesForGeometryCollection}</li>
         *     <li>[addRenderablesForFeature]{@link GeoJSONParser#addRenderablesForFeature}</li>
         *     <li>[addRenderablesForFeatureCollection]{@link GeoJSONParser#addRenderablesForFeatureCollection}</li>
         * </ul>
         *
         * @param {RenderableLayer} layer A {@link RenderableLayer} to hold the shapes created for each GeoJSON
         * geometry. If null, a new layer is created and assigned to this object's [layer]{@link GeoJSONParser#layer}
         * property.
         */

        GeoJSONParser.prototype.load = function (parserCompletionCallback, shapeConfigurationCallback, layer) {
            if (parserCompletionCallback) {
                this._parserCompletionCallback = parserCompletionCallback;
            }

            if (shapeConfigurationCallback) {
                this._shapeConfigurationCallback = shapeConfigurationCallback;
            }

            this._layer = layer || new RenderableLayer();

            if (this.isDataSourceJson()){
                this.parse(this.dataSource);
            }
            else {
                this.requestUrl(this.dataSource);
            }
        };

        /**
         * The default [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for this GeoJSON.
         * It is called if none was specified to the [load]{@link GeoJSONParser#load} method.
         * This method assigns shared, default attributes to the shapes created for each geometry. Any changes to these
         * attributes will have an effect in all shapes created by this GeoJSON.
         * <p>
         * For all geometry, the GeoJSON's properties are checked for an attribute named "name", "Name" or "NAME".
         * If found, the returned shape configuration contains a name property holding the value associated with
         * the attribute. This value is specified as the label displayName property for all shapes created.
         * For {@link Placemark} shapes it is also specified as the placemark label.
         * It is specified as the displayName for all other shapes.
         *
         * @param {GeoJSONGeometry} geometry An object containing the geometry associated with this GeoJSON.
         * @param {Object} properties An object containing the attribute-value pairs found in GeoJSON feature
         * properties member.
         * @returns {Object} An object with properties as described above.
         */
        GeoJSONParser.prototype.defaultShapeConfigurationCallback = function (geometry, properties) {
            var configuration = {};

            var name = properties.name || properties.Name || properties.NAME;
            if (name) {
                configuration.name = name;
            }

            if (geometry.isPointType() || geometry.isMultiPointType()) {
                configuration.attributes = this.defaultPlacemarkAttributes;
            } else if (geometry.isLineStringType() || geometry.isMultiLineStringType()) {
                configuration.attributes = this.defaultShapeAttributes;
            } else if (geometry.isPolygonType() || geometry.isMultiPolygonType()) {
                configuration.attributes = this.defaultShapeAttributes;
            }

            return configuration;
        };

        // Get GeoJSON string using XMLHttpRequest. Internal use only.
        GeoJSONParser.prototype.requestUrl = function (url) {
            var xhr = new XMLHttpRequest();

            xhr.open("GET", url, true);
            xhr.responseType = 'text';
            xhr.onreadystatechange = (function () {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        this.parse(xhr.response);
                    }
                    else {
                        Logger.log(Logger.LEVEL_WARNING,
                            "GeoJSON retrieval failed (" + xhr.statusText + "): " + url);
                    }
                }
            }).bind(this);

            xhr.onerror = function () {
                Logger.log(Logger.LEVEL_WARNING, "GeoJSON retrieval failed: " + url);
            };

            xhr.ontimeout = function () {
                Logger.log(Logger.LEVEL_WARNING, "GeoJSON retrieval timed out: " + url);
            };

            xhr.send(null);
        };

        // Parse GeoJSON string using built in method JSON.parse(). Internal use only.
        GeoJSONParser.prototype.parse = function (geoJSONString) {
            try {
                this._geoJSONObject = JSON.parse(geoJSONString);
            }
            catch (e) {
                Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "parse",
                    "invalidGeoJSONObject")
            }
            finally {
                if (this.geoJSONObject){
                    if (Object.prototype.toString.call(this.geoJSONObject) === '[object Array]') {
                        throw new ArgumentError(
                            Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "parse",
                                "invalidGeoJSONObjectLength"));
                    }

                    if (this.geoJSONObject.hasOwnProperty(GeoJSONConstants.FIELD_TYPE)) {
                        this.setGeoJSONType();
                        this.setGeoJSONCRS();
                    }
                    else{
                        throw new ArgumentError(
                            Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "parse",
                                "missingGeoJSONType"));
                    }

                    if (!!this._parserCompletionCallback && typeof this._parserCompletionCallback === "function") {
                        this._parserCompletionCallback(this.layer);
                    }
                }
            }
        };

        // Set GeoJSON CRS object.
        // If no crs member can be so acquired, the default CRS shall apply to the GeoJSON object.
        // The crs member should be on the top-level GeoJSON object in a hierarchy (in feature collection, feature,
        // geometry order) and should not be repeated or overridden on children or grandchildren of the object.
        // Internal use only.
        GeoJSONParser.prototype.setGeoJSONCRS = function () {
            if (this.geoJSONObject[GeoJSONConstants.FIELD_CRS]){
                this._crs = new GeoJSONCRS (
                    this.geoJSONObject[GeoJSONConstants.FIELD_CRS][GeoJSONConstants.FIELD_TYPE],
                    this.geoJSONObject[GeoJSONConstants.FIELD_CRS][GeoJSONConstants.FIELD_PROPERTIES]);

                var crsCallback = (function() {
                    this.addRenderablesForGeoJSON(this.layer);
                }).bind(this);

                this.crs.setCRSString(crsCallback);
            }
            else{
                // If no CRS, consider default one
                this.addRenderablesForGeoJSON(this.layer);
            }
        };

        /**
         * Iterates over this GeoJSON's geometries and creates shapes for them. See the following methods for the
         * details of the shapes created and their use of the
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback}:
         * <ul>
         *     <li>[addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}</li>
         *     <li>[addRenderablesForGeometryCollection]{@link GeoJSONParser#addRenderablesForGeometryCollection}</li>
         *     <li>[addRenderablesForFeature]{@link GeoJSONParser#addRenderablesForFeature}</li>
         *     <li>[addRenderablesForFeatureCollection]{@link GeoJSONParser#addRenderablesForFeatureCollection}</li>
         * </ul>
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForGeoJSON = function (layer) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForGeoJSON", "missingLayer"));
            }

            switch(this.geoJSONType) {
                case GeoJSONConstants.TYPE_FEATURE:
                    var feature = new  GeoJSONFeature(
                        this.geoJSONObject[GeoJSONConstants.FIELD_GEOMETRY],
                        this.geoJSONObject[GeoJSONConstants.FIELD_PROPERTIES],
                        this.geoJSONObject[GeoJSONConstants.FIELD_ID],
                        this.geoJSONObject[GeoJSONConstants.FIELD_BBOX]
                    );
                    this.addRenderablesForFeature(
                        layer,
                        feature);
                    break;
                case GeoJSONConstants.TYPE_FEATURE_COLLECTION:
                    var featureCollection = new GeoJSONFeatureCollection(
                        this.geoJSONObject[GeoJSONConstants.FIELD_FEATURES],
                        this.geoJSONObject[GeoJSONConstants.FIELD_BBOX]
                    );
                    this.addRenderablesForFeatureCollection(
                        layer,
                        featureCollection);
                    break;
                case GeoJSONConstants.TYPE_GEOMETRY_COLLECTION:
                    var geometryCollection = new GeoJSONGeometryCollection(
                        this.geoJSONObject[GeoJSONConstants.FIELD_GEOMETRIES],
                        this.geoJSONObject[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForGeometryCollection(
                        layer,
                        geometryCollection,
                        null);
                    break;
                default:
                    this.addRenderablesForGeometry(
                        layer,
                        this.geoJSONObject,
                        null);
                    break;
            }
        };

        /**
         * Creates shape for a geometry. See the following methods for the
         * details of the shapes created and their use of the
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback}:
         * <ul>
         *     <li>[addRenderablesForPoint]{@link GeoJSONParser#addRenderablesForPoint}</li>
         *     <li>[addRenderablesForMultiPoint]{@link GeoJSONParser#addRenderablesForMultiPoint}</li>
         *     <li>[addRenderablesForLineString]{@link GeoJSONParser#addRenderablesForLineString}</li>
         *     <li>[addRenderablesForMultiLineString]{@link GeoJSONParser#addRenderablesForMultiLineString}</li>
         *     <li>[addRenderablesForPolygon]{@link GeoJSONParser#addRenderablesForPolygon}</li>
         *     <li>[addRenderablesForMultiPolygon]{@link GeoJSONParser#addRenderablesForMultiPolygon}</li>
         * </ul>
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometry} geometry An object containing the current geometry.
         * @param {Object} properties An object containing the attribute-value pairs found in GeoJSON feature
         * properties member.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForGeometry = function (layer, geometry, properties){
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForGeometry", "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForGeometry", "missingGeometry"));
            }

            switch(geometry[GeoJSONConstants.FIELD_TYPE]){
                case GeoJSONConstants.TYPE_POINT:
                    var pointGeometry = new GeoJSONGeometryPoint(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForPoint(
                        layer,
                        pointGeometry,
                        properties ? properties : null);
                    break;
                case GeoJSONConstants.TYPE_MULTI_POINT:
                    var multiPointGeometry = new GeoJSONGeometryMultiPoint(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForMultiPoint(
                        layer,
                        multiPointGeometry,
                        properties ? properties : null);
                    break;
                case GeoJSONConstants.TYPE_LINE_STRING:
                    var lineStringGeometry = new GeoJSONGeometryLineString(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForLineString(
                        layer,
                        lineStringGeometry,
                        properties ? properties : null);
                    break;
                case GeoJSONConstants.TYPE_MULTI_LINE_STRING:
                    var multiLineStringGeometry = new GeoJSONGeometryMultiLineString(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForMultiLineString(
                        layer,
                        multiLineStringGeometry,
                        properties ? properties : null);
                    break;
                case GeoJSONConstants.TYPE_POLYGON:
                    var polygonGeometry = new GeoJSONGeometryPolygon(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]
                    );
                    this.addRenderablesForPolygon(
                        layer,
                        polygonGeometry,
                        properties ? properties : null);
                    break;
                case GeoJSONConstants.TYPE_MULTI_POLYGON:
                    var multiPolygonGeometry = new GeoJSONGeometryMultiPolygon(
                        geometry[GeoJSONConstants.FIELD_COORDINATES],
                        geometry[GeoJSONConstants.FIELD_TYPE],
                        geometry[GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForMultiPolygon(
                        layer,
                        multiPolygonGeometry,
                        properties ? properties : null);
                    break;
                default:
                    break;
            }
        }

        /**
         * Creates a {@link Placemark} for a Point geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryPoint} geometry The Point geometry object.
         * @param {Object} properties The properties related to the Point geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForPoint = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForPoint", "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForPoint", "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                var longitude = geometry.coordinates[0],
                    latitude = geometry.coordinates[1],
                    altitude = geometry.coordinates[2] ?  geometry.coordinates[2] : 0;

                var reprojectedCoordinate = this.getReprojectedIfRequired(
                    latitude,
                    longitude,
                    this.crs);
                var position = new Position(reprojectedCoordinate[1], reprojectedCoordinate[0], altitude);
                var placemark = new Placemark(
                    position,
                    false,
                    configuration && configuration.attributes ? configuration.attributes : null);

                placemark.altitudeMode = WorldWind.RELATIVE_TO_GROUND;
                if (configuration && configuration.name){
                    placemark.label = configuration.name;
                }
                if (configuration.highlightAttributes) {
                    placemark.highlightAttributes = configuration.highlightAttributes;
                }
                if (configuration && configuration.pickDelegate) {
                    placemark.pickDelegate = configuration.pickDelegate;
                }
                if (configuration && configuration.userProperties) {
                    placemark.userProperties = configuration.userProperties;
                }
                layer.addRenderable(placemark);
            }
        };

        /**
         * Creates {@link Placemark}s for a MultiPoint geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryMultiPoint} geometry The MultiPoint geometry object.
         * @param {Object} properties The properties related to the MultiPoint geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForMultiPoint = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiPoint",
                        "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiPoint",
                        "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                for (var pointIndex = 0, points = geometry.coordinates.length; pointIndex < points; pointIndex += 1){
                    var longitude = geometry.coordinates[pointIndex][0],
                        latitude = geometry.coordinates[pointIndex][1],
                        altitude = geometry.coordinates[pointIndex][2] ?  geometry.coordinates[pointIndex][2] : 0;

                    var reprojectedCoordinate = this.getReprojectedIfRequired(
                        latitude,
                        longitude,
                        this.crs);
                    var position = new Position(reprojectedCoordinate[1], reprojectedCoordinate[0], altitude);
                    var placemark = new Placemark(
                        position,
                        false,
                        configuration && configuration.attributes ? configuration.attributes : null);
                    placemark.altitudeMode = WorldWind.RELATIVE_TO_GROUND;
                    if (configuration && configuration.name){
                        placemark.label = configuration.name;
                    }
                    if (configuration.highlightAttributes) {
                        placemark.highlightAttributes = configuration.highlightAttributes;
                    }
                    if (configuration && configuration.pickDelegate) {
                        placemark.pickDelegate = configuration.pickDelegate;
                    }
                    if (configuration && configuration.userProperties) {
                        placemark.userProperties = configuration.userProperties;
                    }
                    layer.addRenderable(placemark);
                }
            }
        };

        /**
         * Creates a {@link SurfacePolyline} for a LineString geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryLineString} geometry The LineString geometry object.
         * @param {Object} properties The properties related to the LineString geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForLineString = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForLineString",
                        "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForLineString",
                        "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                var positions = [];
                for (var pointsIndex = 0, points = geometry.coordinates; pointsIndex < points.length; pointsIndex++) {
                    var longitude = points[pointsIndex][0],
                        latitude = points[pointsIndex][1];
                    //altitude = points[pointsIndex][2] ?  points[pointsIndex][2] : 0,
                    var reprojectedCoordinate = this.getReprojectedIfRequired(
                        latitude,
                        longitude,
                        this.crs);
                    var position = new Location(reprojectedCoordinate[1], reprojectedCoordinate[0]);
                    positions.push(position);
                }

                var shape;
                shape = new SurfacePolyline(
                    positions,
                    configuration && configuration.attributes ? configuration.attributes : null);
                if (configuration.highlightAttributes) {
                    shape.highlightAttributes = configuration.highlightAttributes;
                }
                if (configuration && configuration.pickDelegate) {
                    shape.pickDelegate = configuration.pickDelegate;
                }
                if (configuration && configuration.userProperties) {
                    shape.userProperties = configuration.userProperties;
                }
                layer.addRenderable(shape);
            }
        };

        /**
         * Creates {@link SurfacePolyline}s for a MultiLineString geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryMultiLineString} geometry The MultiLineString geometry object.
         * @param {Object} properties The properties related to the MultiLineString geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForMultiLineString = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiLineString",
                        "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiLineString",
                        "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                for (var linesIndex = 0, lines = geometry.coordinates; linesIndex < lines.length; linesIndex++) {
                    var positions = [];

                    for (var positionIndex = 0, points = lines[linesIndex]; positionIndex < points.length;
                         positionIndex++) {
                        var longitude = points[positionIndex][0],
                            latitude = points[positionIndex][1];
                        //altitude = points[positionIndex][2] ?  points[positionIndex][2] : 0,
                        var reprojectedCoordinate = this.getReprojectedIfRequired(
                            latitude,
                            longitude,
                            this.crs);
                        var position = new Location(reprojectedCoordinate[1], reprojectedCoordinate[0]);
                        positions.push(position);
                    }

                    var shape;
                    shape = new SurfacePolyline(
                        positions,
                        configuration && configuration.attributes ? configuration.attributes : null);
                    if (configuration.highlightAttributes) {
                        shape.highlightAttributes = configuration.highlightAttributes;
                    }
                    if (configuration && configuration.pickDelegate) {
                        shape.pickDelegate = configuration.pickDelegate;
                    }
                    if (configuration && configuration.userProperties) {
                        shape.userProperties = configuration.userProperties;
                    }
                    layer.addRenderable(shape);
                }
            }
        };

        /**
         * Creates a {@link SurfacePolygon} for a Polygon geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryPolygon} geometry The Polygon geometry object.
         * @param {Object} properties The properties related to the Polygon geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForPolygon = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForPolygon", "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForPolygon", "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                for (var boundariesIndex = 0, boundaries = geometry.coordinates;
                     boundariesIndex < boundaries.length; boundariesIndex++) {
                    var positions = [];

                    for (var positionIndex = 0, points = boundaries[boundariesIndex];
                         positionIndex < points.length; positionIndex++) {
                        var longitude = points[positionIndex][0],
                            latitude = points[positionIndex][1];
                        //altitude = points[positionIndex][2] ?  points[positionIndex][2] : 0,
                        var reprojectedCoordinate = this.getReprojectedIfRequired(
                            latitude,
                            longitude,
                            this.crs);
                        var position = new Location(reprojectedCoordinate[1], reprojectedCoordinate[0]);
                        positions.push(position);
                    }

                    var shape;
                    shape = new SurfacePolygon(
                        positions,
                        configuration && configuration.attributes ? configuration.attributes : null);
                    if (configuration.highlightAttributes) {
                        shape.highlightAttributes = configuration.highlightAttributes;
                    }
                    if (configuration && configuration.pickDelegate) {
                        shape.pickDelegate = configuration.pickDelegate;
                    }
                    if (configuration && configuration.userProperties) {
                        shape.userProperties = configuration.userProperties;
                    }                    layer.addRenderable(shape);
                }
            }
        };

        /**
         * Creates {@link SurfacePolygon}s for a MultiPolygon geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry}.
         * <p>
         * This method invokes this GeoJSON's
         * [shapeConfigurationCallback]{@link GeoJSONParser#shapeConfigurationCallback} for the geometry.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryMultiPolygon} geometry The MultiPolygon geometry object.
         * @param {Object} properties The properties related to the MultiPolygon geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified geometry is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForMultiPolygon = function (layer, geometry, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiPolygon",
                        "missingLayer"));
            }

            if (!geometry) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForMultiPolygon",
                        "missingGeometry"));
            }

            var configuration = this.shapeConfigurationCallback(geometry, properties);

            if (!this.crs || this.crs.isCRSSupported()) {
                for (var polygonsIndex = 0, polygons = geometry.coordinates;
                     polygonsIndex < polygons.length; polygonsIndex++) {
                    var boundaries = [];
                    for (var boundariesIndex = 0; boundariesIndex < polygons[polygonsIndex].length; boundariesIndex++) {
                        var positions = [];
                        for (var positionIndex = 0, points = polygons[polygonsIndex][boundariesIndex];
                             positionIndex < points.length; positionIndex++) {
                            var longitude = points[positionIndex][0],
                                latitude = points[positionIndex][1];
                            //altitude = points[positionIndex][2] ?  points[positionIndex][2] : 0,;

                            var reprojectedCoordinate = this.getReprojectedIfRequired(
                                latitude,
                                longitude,
                                this.crs);
                            var position = new Location(reprojectedCoordinate[1], reprojectedCoordinate[0]);
                            positions.push(position);
                        }
                        boundaries.push(positions);
                    }
                    var shape;
                    shape = new SurfacePolygon(
                        boundaries,
                        configuration && configuration.attributes ? configuration.attributes : null);
                    if (configuration.highlightAttributes) {
                        shape.highlightAttributes = configuration.highlightAttributes;
                    }
                    if (configuration && configuration.pickDelegate) {
                        shape.pickDelegate = configuration.pickDelegate;
                    }
                    if (configuration && configuration.userProperties) {
                        shape.userProperties = configuration.userProperties;
                    }
                    layer.addRenderable(shape);
                }
            }
        };

        /**
         * Iterates over the GeoJSON GeometryCollection geometries and creates {@link GeoJSONGeometry}s for them.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeoJSON]{@link GeoJSONParser#addRenderablesForGeoJSON}.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONGeometryCollection} geometryCollection The GeometryCollection object.
         * @param {Object} properties The properties related to the GeometryCollection geometry.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified featureCollection is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForGeometryCollection = function (layer, geometryCollection, properties) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForGeometryCollection",
                        "missingLayer"));
            }

            if (!geometryCollection) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForGeometryCollection",
                        "missingGeometryCollection"));
            }


            for (var geometryIndex = 0, geometries = geometryCollection.geometries;
                 geometryIndex < geometries.length; geometryIndex++) {
                if(geometries[geometryIndex].hasOwnProperty(GeoJSONConstants.FIELD_TYPE)){
                    this.addRenderablesForGeometry(layer, geometries[geometryIndex], properties);
                }
            }
        };

        /**
         * Calls [addRenderablesForGeometry]{@link GeoJSONParser#addRenderablesForGeometry} or
         * [addRenderablesForGeometryCollection]{@link GeoJSONParser#addRenderablesForGeometryCollection}
         * depending on the type of feature geometry.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeoJSON]{@link GeoJSONParser#addRenderablesForGeoJSON}.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONFeature} feature The Feature object.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified feature is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForFeature = function (layer, feature) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForFeature", "missingLayer"));
            }

            if (!feature) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForFeature", "missingFeature"));
            }

            if (feature.geometry.type === GeoJSONConstants.TYPE_GEOMETRY_COLLECTION) {
                var geometryCollection = new GeoJSONGeometryCollection(
                    feature.geometry.geometries,
                    feature.bbox);
                this.addRenderablesForGeometryCollection(
                    layer,
                    geometryCollection,
                    feature.properties);
            }
            else {
                this.addRenderablesForGeometry(
                    layer,
                    feature.geometry,
                    feature.properties
                );
            }
        };

        /**
         * Iterates over the GeoJSON FeatureCollection features and creates {@link GeoJSONFeature}s for them.
         * Applications typically do not call this method directly. It is called by
         * [addRenderablesForGeoJSON]{@link GeoJSONParser#addRenderablesForGeoJSON}.
         * @param {RenderableLayer} layer The layer in which to place the newly created shapes.
         * @param {GeoJSONFeatureCollection} featureCollection The FeatureCollection object.
         * @throws {ArgumentError} If the specified layer is null or undefined.
         * @throws {ArgumentError} If the specified featureCollection is null or undefined.
         */
        GeoJSONParser.prototype.addRenderablesForFeatureCollection = function (layer, featureCollection) {
            if (!layer) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForFeatureCollection",
                        "missingLayer"));
            }

            if (!featureCollection) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "addRenderablesForFeatureCollection",
                        "missingFeatureCollection"));
            }

            if (featureCollection.features.length > 0) {
                for (var featureIndex = 0; featureIndex < featureCollection.features.length; featureIndex++) {
                    var feature = new GeoJSONFeature(
                        featureCollection.features[featureIndex][GeoJSONConstants.FIELD_GEOMETRY],
                        featureCollection.features[featureIndex][GeoJSONConstants.FIELD_PROPERTIES],
                        featureCollection.features[featureIndex][GeoJSONConstants.FIELD_ID],
                        featureCollection.features[featureIndex][GeoJSONConstants.FIELD_BBOX]);
                    this.addRenderablesForFeature(
                        layer,
                        feature);
                }
            }
        };

        // Set type of GeoJSON object. Internal use ony.
        GeoJSONParser.prototype.setGeoJSONType = function () {
            switch (this.geoJSONObject[GeoJSONConstants.FIELD_TYPE]) {
                case GeoJSONConstants.TYPE_POINT:
                    this._geoJSONType = GeoJSONConstants.TYPE_POINT;
                    break;
                case GeoJSONConstants.TYPE_MULTI_POINT:
                    this._geoJSONType = GeoJSONConstants.TYPE_MULTI_POINT;
                    break;
                case GeoJSONConstants.TYPE_LINE_STRING:
                    this._geoJSONType = GeoJSONConstants.TYPE_LINE_STRING;
                    break;
                case GeoJSONConstants.TYPE_MULTI_LINE_STRING:
                    this._geoJSONType = GeoJSONConstants.TYPE_MULTI_LINE_STRING;
                    break;
                case GeoJSONConstants.TYPE_POLYGON:
                    this._geoJSONType = GeoJSONConstants.TYPE_POLYGON;
                    break;
                case GeoJSONConstants.TYPE_MULTI_POLYGON:
                    this._geoJSONType = GeoJSONConstants.TYPE_MULTI_POLYGON;
                    break;
                case GeoJSONConstants.TYPE_GEOMETRY_COLLECTION:
                    this._geoJSONType = GeoJSONConstants.TYPE_GEOMETRY_COLLECTION;
                    break;
                case GeoJSONConstants.TYPE_FEATURE:
                    this._geoJSONType = GeoJSONConstants.TYPE_FEATURE;
                    break;
                case GeoJSONConstants.TYPE_FEATURE_COLLECTION:
                    this._geoJSONType = GeoJSONConstants.TYPE_FEATURE_COLLECTION;
                    break;
                default:
                    throw new ArgumentError(
                        Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "setGeoJSONType", "invalidGeoJSONType"));
            }
        };

        /**
         * Reprojects GeoJSON geometry coordinates if required using proj4js.
         *
         * @param {Number} latitude The latitude coordinate of the geometry.
         * @param {Number} longitude The longitude coordinate of the geometry.
         * @param {GeoJSONCRS} crsObject The GeoJSON CRS object.
         * @returns {Number[]} An array containing reprojected coordinates.
         * @throws {ArgumentError} If the specified latitude is null or undefined.
         * @throws {ArgumentError} If the specified longitude is null or undefined.
         * @throws {ArgumentError} If the specified crsObject is null or undefined.
         */
        GeoJSONParser.prototype.getReprojectedIfRequired = function (latitude, longitude, crsObject) {
            if (!latitude && latitude !== 0.0) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "getReprojectedIfRequired",
                        "missingLatitude"));
            }

            if (!longitude && longitude !== 0.0) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GeoJSON", "getReprojectedIfRequired",
                        "missingLongitude"));
            }

            if (!crsObject || crsObject.isDefault()){
                return [longitude, latitude];
            }
            else{
                return Proj4(crsObject.projectionString, GeoJSONConstants.EPSG4326_CRS, [longitude, latitude]);
            }
        };

        // Use this function to add aliases for some projection strings that proj4js doesn't recognize.
        GeoJSONParser.prototype.setProj4jsAliases = function () {
            Proj4.defs([
                [
                    'urn:ogc:def:crs:OGC:1.3:CRS84',
                    Proj4.defs('EPSG:4326')
                ],
                [
                    'urn:ogc:def:crs:EPSG::3857',
                    Proj4.defs('EPSG:3857')

                ]
            ]);
        };

        /**
        * Indicate whether the data source is of a JSON type.
        * @returns {Boolean} True if the data source is of JSON type.
        */
        GeoJSONParser.prototype.isDataSourceJson = function() {
            try {
                JSON.parse(this.dataSource);
            } catch (e) {
                return false;
            }
            return true;
        }

        return GeoJSONParser;
    }
);
