/*global define*/
define([
        '../Core/AssociativeArray',
        '../Core/BoundingRectangle',
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Cartographic',
        '../Core/ClockRange',
        '../Core/ClockStep',
        '../Core/Color',
        '../Core/createGuid',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/Ellipsoid',
        '../Core/Event',
        '../Core/getAbsoluteUri',
        '../Core/getExtensionFromUri',
        '../Core/getFilenameFromUri',
        '../Core/Iso8601',
        '../Core/joinUrls',
        '../Core/JulianDate',
        '../Core/loadBlob',
        '../Core/loadXML',
        '../Core/Math',
        '../Core/NearFarScalar',
        '../Core/PinBuilder',
        '../Core/PolygonHierarchy',
        '../Core/Rectangle',
        '../Core/RuntimeError',
        '../Core/TimeInterval',
        '../Core/TimeIntervalCollection',
        '../Scene/HeightReference',
        '../Scene/HorizontalOrigin',
        '../Scene/LabelStyle',
        '../Scene/SceneMode',
        '../ThirdParty/Autolinker',
        '../ThirdParty/Uri',
        '../ThirdParty/when',
        '../ThirdParty/zip',
        './BillboardGraphics',
        './CompositePositionProperty',
        './CorridorGraphics',
        './DataSource',
        './DataSourceClock',
        './Entity',
        './EntityCluster',
        './EntityCollection',
        './LabelGraphics',
        './PathGraphics',
        './PolygonGraphics',
        './PolylineGraphics',
        './PositionPropertyArray',
        './RectangleGraphics',
        './ReferenceProperty',
        './SampledPositionProperty',
        './ScaledPositionProperty',
        './TimeIntervalCollectionProperty',
        './WallGraphics'
    ], function(
        AssociativeArray,
        BoundingRectangle,
        Cartesian2,
        Cartesian3,
        Cartographic,
        ClockRange,
        ClockStep,
        Color,
        createGuid,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        Ellipsoid,
        Event,
        getAbsoluteUri,
        getExtensionFromUri,
        getFilenameFromUri,
        Iso8601,
        joinUrls,
        JulianDate,
        loadBlob,
        loadXML,
        CesiumMath,
        NearFarScalar,
        PinBuilder,
        PolygonHierarchy,
        Rectangle,
        RuntimeError,
        TimeInterval,
        TimeIntervalCollection,
        HeightReference,
        HorizontalOrigin,
        LabelStyle,
        SceneMode,
        Autolinker,
        Uri,
        when,
        zip,
        BillboardGraphics,
        CompositePositionProperty,
        CorridorGraphics,
        DataSource,
        DataSourceClock,
        Entity,
        EntityCluster,
        EntityCollection,
        LabelGraphics,
        PathGraphics,
        PolygonGraphics,
        PolylineGraphics,
        PositionPropertyArray,
        RectangleGraphics,
        ReferenceProperty,
        SampledPositionProperty,
        ScaledPositionProperty,
        TimeIntervalCollectionProperty,
        WallGraphics) {
    'use strict';

    // IE 8 doesn't have a DOM parser and can't run Cesium anyway, so just bail.
    if (typeof DOMParser === 'undefined') {
        return {};
    }

    //This is by no means an exhaustive list of MIME types.
    //The purpose of this list is to be able to accurately identify content embedded
    //in KMZ files. Eventually, we can make this configurable by the end user so they can add
    //there own content types if they have KMZ files that require it.
    var MimeTypes = {
        avi : "video/x-msvideo",
        bmp : "image/bmp",
        bz2 : "application/x-bzip2",
        chm : "application/vnd.ms-htmlhelp",
        css : "text/css",
        csv : "text/csv",
        doc : "application/msword",
        dvi : "application/x-dvi",
        eps : "application/postscript",
        flv : "video/x-flv",
        gif : "image/gif",
        gz : "application/x-gzip",
        htm : "text/html",
        html : "text/html",
        ico : "image/vnd.microsoft.icon",
        jnlp : "application/x-java-jnlp-file",
        jpeg : "image/jpeg",
        jpg : "image/jpeg",
        m3u : "audio/x-mpegurl",
        m4v : "video/mp4",
        mathml : "application/mathml+xml",
        mid : "audio/midi",
        midi : "audio/midi",
        mov : "video/quicktime",
        mp3 : "audio/mpeg",
        mp4 : "video/mp4",
        mp4v : "video/mp4",
        mpeg : "video/mpeg",
        mpg : "video/mpeg",
        odp : "application/vnd.oasis.opendocument.presentation",
        ods : "application/vnd.oasis.opendocument.spreadsheet",
        odt : "application/vnd.oasis.opendocument.text",
        ogg : "application/ogg",
        pdf : "application/pdf",
        png : "image/png",
        pps : "application/vnd.ms-powerpoint",
        ppt : "application/vnd.ms-powerpoint",
        ps : "application/postscript",
        qt : "video/quicktime",
        rdf : "application/rdf+xml",
        rss : "application/rss+xml",
        rtf : "application/rtf",
        svg : "image/svg+xml",
        swf : "application/x-shockwave-flash",
        text : "text/plain",
        tif : "image/tiff",
        tiff : "image/tiff",
        txt : "text/plain",
        wav : "audio/x-wav",
        wma : "audio/x-ms-wma",
        wmv : "video/x-ms-wmv",
        xml : "application/xml",
        zip : "application/zip",

        detectFromFilename : function(filename) {
            var ext = filename.toLowerCase();
            ext = getExtensionFromUri(ext);
            return MimeTypes[ext];
        }
    };

    var parser = new DOMParser();
    var autolinker = new Autolinker({
        stripPrefix : false,
        twitter : false,
        email : false,
        replaceFn : function(linker, match) {
            if (!match.protocolUrlMatch) {
                //Prevent matching of non-explicit urls.
                //i.e. foo.id won't match but http://foo.id will
                return false;
            }
        }
    });

    var BILLBOARD_SIZE = 32;

    var BILLBOARD_NEAR_DISTANCE = 2414016;
    var BILLBOARD_NEAR_RATIO = 1.0;
    var BILLBOARD_FAR_DISTANCE = 1.6093e+7;
    var BILLBOARD_FAR_RATIO = 0.1;

    function isZipFile(blob) {
        var magicBlob = blob.slice(0, Math.min(4, blob.size));
        var deferred = when.defer();
        var reader = new FileReader();
        reader.addEventListener('load', function() {
            deferred.resolve(new DataView(reader.result).getUint32(0, false) === 0x504b0304);
        });
        reader.addEventListener('error', function() {
            deferred.reject(reader.error);
        });
        reader.readAsArrayBuffer(magicBlob);
        return deferred.promise;
    }

    function readBlobAsText(blob) {
        var deferred = when.defer();
        var reader = new FileReader();
        reader.addEventListener('load', function() {
            deferred.resolve(reader.result);
        });
        reader.addEventListener('error', function() {
            deferred.reject(reader.error);
        });
        reader.readAsText(blob);
        return deferred.promise;
    }

    function loadXmlFromZip(reader, entry, uriResolver, deferred) {
        entry.getData(new zip.TextWriter(), function(text) {
            uriResolver.kml = parser.parseFromString(text, 'application/xml');
            deferred.resolve();
        });
    }

    function loadDataUriFromZip(reader, entry, uriResolver, deferred) {
        var mimeType = defaultValue(MimeTypes.detectFromFilename(entry.filename), 'application/octet-stream');
        entry.getData(new zip.Data64URIWriter(mimeType), function(dataUri) {
            uriResolver[entry.filename] = dataUri;
            deferred.resolve();
        });
    }

    function replaceAttributes(div, elementType, attributeName, uriResolver) {
        var keys = uriResolver.keys;
        var baseUri = new Uri('.');
        var elements = div.querySelectorAll(elementType);
        for (var i = 0; i < elements.length; i++) {
            var element = elements[i];
            var value = element.getAttribute(attributeName);
            var uri = new Uri(value).resolve(baseUri).toString();
            var index = keys.indexOf(uri);
            if (index !== -1) {
                var key = keys[index];
                element.setAttribute(attributeName, uriResolver[key]);
                if (elementType === 'a' && element.getAttribute('download') === null) {
                    element.setAttribute('download', key);
                }
            }
        }
    }

    function proxyUrl(url, proxy) {
        if (defined(proxy)) {
            if (new Uri(url).isAbsolute()) {
                url = proxy.getURL(url);
            }
        }
        return url;
    }

    // an optional context is passed to allow for some malformed kmls (those with multiple geometries with same ids) to still parse
    // correctly, as they do in Google Earth.
    function createEntity(node, entityCollection, context) {
        var id = queryStringAttribute(node, 'id');
        id = defined(id) && id.length !== 0 ? id : createGuid();
        if(defined(context)){
            id = context + id;
        }

        // If we have a duplicate ID just generate one.
        // This isn't valid KML but Google Earth handles this case.
        var entity = entityCollection.getById(id);
        if (defined(entity)) {
            id = createGuid();
            if(defined(context)){
                id = context + id;
            }
        }

        entity = entityCollection.add(new Entity({id : id}));
        if (!defined(entity.kml)) {
            entity.addProperty('kml');
            entity.kml = new KmlFeatureData();
        }
        return entity;
    }

    function isExtrudable(altitudeMode, gxAltitudeMode) {
        return altitudeMode === 'absolute' || altitudeMode === 'relativeToGround' || gxAltitudeMode === 'relativeToSeaFloor';
    }

    function readCoordinate(value) {
        //Google Earth treats empty or missing coordinates as 0.
        if (!defined(value)) {
            return Cartesian3.fromDegrees(0, 0, 0);
        }

        var digits = value.match(/[^\s,\n]+/g);
        if (!defined(digits)) {
            return Cartesian3.fromDegrees(0, 0, 0);
        }

        var longitude = parseFloat(digits[0]);
        var latitude = parseFloat(digits[1]);
        var height = parseFloat(digits[2]);

        longitude = isNaN(longitude) ? 0.0 : longitude;
        latitude = isNaN(latitude) ? 0.0 : latitude;
        height = isNaN(height) ? 0.0 : height;

        return Cartesian3.fromDegrees(longitude, latitude, height);
    }

    function readCoordinates(element) {
        if (!defined(element)) {
            return undefined;
        }

        var tuples = element.textContent.match(/[^\s\n]+/g);
        if (!defined(tuples)) {
            return undefined;
        }

        var length = tuples.length;
        var result = new Array(length);
        var resultIndex = 0;
        for (var i = 0; i < length; i++) {
            result[resultIndex++] = readCoordinate(tuples[i]);
        }
        return result;
    }

    var kmlNamespaces = [null, undefined, 'http://www.opengis.net/kml/2.2', 'http://earth.google.com/kml/2.2', 'http://earth.google.com/kml/2.1', 'http://earth.google.com/kml/2.0'];
    var gxNamespaces = ['http://www.google.com/kml/ext/2.2'];
    var atomNamespaces = ['http://www.w3.org/2005/Atom'];
    var namespaces = {
        kml : kmlNamespaces,
        gx : gxNamespaces,
        atom : atomNamespaces,
        kmlgx : kmlNamespaces.concat(gxNamespaces)
    };

    function queryNumericAttribute(node, attributeName) {
        if (!defined(node)) {
            return undefined;
        }

        var value = node.getAttribute(attributeName);
        if (value !== null) {
            var result = parseFloat(value);
            return !isNaN(result) ? result : undefined;
        }
        return undefined;
    }

    function queryStringAttribute(node, attributeName) {
        if (!defined(node)) {
            return undefined;
        }
        var value = node.getAttribute(attributeName);
        return value !== null ? value : undefined;
    }

    function queryFirstNode(node, tagName, namespace) {
        if (!defined(node)) {
            return undefined;
        }
        var childNodes = node.childNodes;
        var length = childNodes.length;
        for (var q = 0; q < length; q++) {
            var child = childNodes[q];
            if (child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1) {
                return child;
            }
        }
        return undefined;
    }

    function queryNodes(node, tagName, namespace) {
        if (!defined(node)) {
            return undefined;
        }
        var result = [];
        var childNodes = node.getElementsByTagNameNS('*', tagName);
        var length = childNodes.length;
        for (var q = 0; q < length; q++) {
            var child = childNodes[q];
            if (child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1) {
                result.push(child);
            }
        }
        return result;
    }

    function queryChildNodes(node, tagName, namespace) {
        if (!defined(node)) {
            return [];
        }
        var result = [];
        var childNodes = node.childNodes;
        var length = childNodes.length;
        for (var q = 0; q < length; q++) {
            var child = childNodes[q];
            if (child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1) {
                result.push(child);
            }
        }
        return result;
    }

    function queryNumericValue(node, tagName, namespace) {
        var resultNode = queryFirstNode(node, tagName, namespace);
        if (defined(resultNode)) {
            var result = parseFloat(resultNode.textContent);
            return !isNaN(result) ? result : undefined;
        }
        return undefined;
    }

    function queryStringValue(node, tagName, namespace) {
        var result = queryFirstNode(node, tagName, namespace);
        if (defined(result)) {
            return result.textContent.trim();
        }
        return undefined;
    }

    function queryBooleanValue(node, tagName, namespace) {
        var result = queryFirstNode(node, tagName, namespace);
        if (defined(result)) {
            var value = result.textContent.trim();
            return value === '1' || /^true$/i.test(value);
        }
        return undefined;
    }

    function resolveHref(href, proxy, sourceUri, uriResolver) {
        if (!defined(href)) {
            return undefined;
        }
        var hrefResolved = false;
        if (defined(uriResolver)) {
            var blob = uriResolver[href];
            if (defined(blob)) {
                hrefResolved = true;
                href = blob;
            } else {
                // Needed for multiple levels of KML files in a KMZ
                var tmpHref = getAbsoluteUri(href, sourceUri);
                blob = uriResolver[tmpHref];
                if (defined(blob)) {
                    hrefResolved = true;
                    href = blob;
                }
            }
        }
        if (!hrefResolved && defined(sourceUri)) {
            href = getAbsoluteUri(href, getAbsoluteUri(sourceUri));
            href = proxyUrl(href, proxy);
        }
        return href;
    }

    var colorOptions = {};

    function parseColorString(value, isRandom) {
        if (!defined(value) || /^\s*$/gm.test(value)) {
            return undefined;
        }

        if (value[0] === '#') {
            value = value.substring(1);
        }

        var alpha = parseInt(value.substring(0, 2), 16) / 255.0;
        var blue = parseInt(value.substring(2, 4), 16) / 255.0;
        var green = parseInt(value.substring(4, 6), 16) / 255.0;
        var red = parseInt(value.substring(6, 8), 16) / 255.0;

        if (!isRandom) {
            return new Color(red, green, blue, alpha);
        }

        if (red > 0) {
            colorOptions.maximumRed = red;
        } else {
            colorOptions.red = 0;
        }
        if (green > 0) {
            colorOptions.maximumGreen = green;
        } else {
            colorOptions.green = 0;
        }
        if (blue > 0) {
            colorOptions.maximumBlue = blue;
        } else {
            colorOptions.blue = 0;
        }
        colorOptions.alpha = alpha;
        return Color.fromRandom(colorOptions);
    }

    function queryColorValue(node, tagName, namespace) {
        var value = queryStringValue(node, tagName, namespace);
        if (!defined(value)) {
            return undefined;
        }
        return parseColorString(value, queryStringValue(node, 'colorMode', namespace) === 'random');
    }

    function processTimeStamp(featureNode) {
        var node = queryFirstNode(featureNode, 'TimeStamp', namespaces.kmlgx);
        var whenString = queryStringValue(node, 'when', namespaces.kmlgx);

        if (!defined(node) || !defined(whenString) || whenString.length === 0) {
            return undefined;
        }

        //According to the KML spec, a TimeStamp represents a "single moment in time"
        //However, since Cesium animates much differently than Google Earth, that doesn't
        //Make much sense here.  Instead, we use the TimeStamp as the moment the feature
        //comes into existence.  This works much better and gives a similar feel to
        //GE's experience.
        var when = JulianDate.fromIso8601(whenString);
        var result = new TimeIntervalCollection();
        result.addInterval(new TimeInterval({
            start : when,
            stop : Iso8601.MAXIMUM_VALUE
        }));
        return result;
    }

    function processTimeSpan(featureNode) {
        var node = queryFirstNode(featureNode, 'TimeSpan', namespaces.kmlgx);
        if (!defined(node)) {
            return undefined;
        }
        var result;

        var beginNode = queryFirstNode(node, 'begin', namespaces.kmlgx);
        var beginDate = defined(beginNode) ? JulianDate.fromIso8601(beginNode.textContent) : undefined;

        var endNode = queryFirstNode(node, 'end', namespaces.kmlgx);
        var endDate = defined(endNode) ? JulianDate.fromIso8601(endNode.textContent) : undefined;

        if (defined(beginDate) && defined(endDate)) {
            if (JulianDate.lessThan(endDate, beginDate)) {
                var tmp = beginDate;
                beginDate = endDate;
                endDate = tmp;
            }
            result = new TimeIntervalCollection();
            result.addInterval(new TimeInterval({
                start : beginDate,
                stop : endDate
            }));
        } else if (defined(beginDate)) {
            result = new TimeIntervalCollection();
            result.addInterval(new TimeInterval({
                start : beginDate,
                stop : Iso8601.MAXIMUM_VALUE
            }));
        } else if (defined(endDate)) {
            result = new TimeIntervalCollection();
            result.addInterval(new TimeInterval({
                start : Iso8601.MINIMUM_VALUE,
                stop : endDate
            }));
        }

        return result;
    }

    function createDefaultBillboard() {
        var billboard = new BillboardGraphics();
        billboard.width = BILLBOARD_SIZE;
        billboard.height = BILLBOARD_SIZE;
        billboard.scaleByDistance = new NearFarScalar(BILLBOARD_NEAR_DISTANCE, BILLBOARD_NEAR_RATIO, BILLBOARD_FAR_DISTANCE, BILLBOARD_FAR_RATIO);
        billboard.pixelOffsetScaleByDistance = new NearFarScalar(BILLBOARD_NEAR_DISTANCE, BILLBOARD_NEAR_RATIO, BILLBOARD_FAR_DISTANCE, BILLBOARD_FAR_RATIO);
        return billboard;
    }

    function createDefaultPolygon() {
        var polygon = new PolygonGraphics();
        polygon.outline = true;
        polygon.outlineColor = Color.WHITE;
        return polygon;
    }

    function createDefaultLabel() {
        var label = new LabelGraphics();
        label.translucencyByDistance = new NearFarScalar(3000000, 1.0, 5000000, 0.0);
        label.pixelOffset = new Cartesian2(17, 0);
        label.horizontalOrigin = HorizontalOrigin.LEFT;
        label.font = '16px sans-serif';
        label.style = LabelStyle.FILL_AND_OUTLINE;
        return label;
    }

    function getIconHref(iconNode, dataSource, sourceUri, uriResolver, canRefresh) {
        var href = queryStringValue(iconNode, 'href', namespaces.kml);
        if (!defined(href) || (href.length === 0)) {
            return undefined;
        }

        if (href.indexOf('root://icons/palette-') === 0) {
            var palette = href.charAt(21);

            // Get the icon number
            var x = defaultValue(queryNumericValue(iconNode, 'x', namespaces.gx), 0);
            var y = defaultValue(queryNumericValue(iconNode, 'y', namespaces.gx), 0);
            x = Math.min(x / 32, 7);
            y = 7 - Math.min(y / 32, 7);
            var iconNum = (8 * y) + x;

            href = 'https://maps.google.com/mapfiles/kml/pal' + palette + '/icon' + iconNum + '.png';
        }

        href = resolveHref(href, dataSource._proxy, sourceUri, uriResolver);

        if (canRefresh) {
            var refreshMode = queryStringValue(iconNode, 'refreshMode', namespaces.kml);
            var viewRefreshMode = queryStringValue(iconNode, 'viewRefreshMode', namespaces.kml);
            if (refreshMode === 'onInterval' || refreshMode === 'onExpire') {
                console.log('KML - Unsupported Icon refreshMode: ' + refreshMode);
            } else if (viewRefreshMode === 'onStop' || viewRefreshMode === 'onRegion') {
                console.log('KML - Unsupported Icon viewRefreshMode: ' + viewRefreshMode);
            }

            var viewBoundScale = defaultValue(queryStringValue(iconNode, 'viewBoundScale', namespaces.kml), 1.0);
            var defaultViewFormat = (viewRefreshMode === 'onStop') ? 'BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]' : '';
            var viewFormat = defaultValue(queryStringValue(iconNode, 'viewFormat', namespaces.kml), defaultViewFormat);
            var httpQuery = queryStringValue(iconNode, 'httpQuery', namespaces.kml);
            var queryString = makeQueryString(viewFormat, httpQuery);

            var icon = joinUrls(href, queryString, false);
            return processNetworkLinkQueryString(dataSource._camera, dataSource._canvas, icon, viewBoundScale, dataSource._lastCameraView.bbox);
        }

        return href;
    }

    function processBillboardIcon(dataSource, node, targetEntity, sourceUri, uriResolver) {
        var scale = queryNumericValue(node, 'scale', namespaces.kml);
        var heading = queryNumericValue(node, 'heading', namespaces.kml);
        var color = queryColorValue(node, 'color', namespaces.kml);

        var iconNode = queryFirstNode(node, 'Icon', namespaces.kml);
        var icon = getIconHref(iconNode, dataSource, sourceUri, uriResolver, false);
        var x = queryNumericValue(iconNode, 'x', namespaces.gx);
        var y = queryNumericValue(iconNode, 'y', namespaces.gx);
        var w = queryNumericValue(iconNode, 'w', namespaces.gx);
        var h = queryNumericValue(iconNode, 'h', namespaces.gx);

        var hotSpotNode = queryFirstNode(node, 'hotSpot', namespaces.kml);
        var hotSpotX = queryNumericAttribute(hotSpotNode, 'x');
        var hotSpotY = queryNumericAttribute(hotSpotNode, 'y');
        var hotSpotXUnit = queryStringAttribute(hotSpotNode, 'xunits');
        var hotSpotYUnit = queryStringAttribute(hotSpotNode, 'yunits');

        var billboard = targetEntity.billboard;
        if (!defined(billboard)) {
            billboard = createDefaultBillboard();
            targetEntity.billboard = billboard;
        }

        billboard.image = icon;
        billboard.scale = scale;
        billboard.color = color;

        if (defined(x) || defined(y) || defined(w) || defined(h)) {
            billboard.imageSubRegion = new BoundingRectangle(x, y, w, h);
        }

        //GE treats a heading of zero as no heading
        //You can still point north using a 360 degree angle (or any multiple of 360)
        if (defined(heading) && heading !== 0) {
            billboard.rotation = CesiumMath.toRadians(-heading);
            billboard.alignedAxis = Cartesian3.UNIT_Z;
        }

        //Hotpot is the KML equivalent of pixel offset
        //The hotspot origin is the lower left, but we leave
        //our billboard origin at the center and simply
        //modify the pixel offset to take this into account
        scale = defaultValue(scale, 1.0);

        var xOffset;
        var yOffset;
        if (defined(hotSpotX)) {
            if (hotSpotXUnit === 'pixels') {
                xOffset = -hotSpotX * scale;
            } else if (hotSpotXUnit === 'insetPixels') {
                xOffset = (hotSpotX - BILLBOARD_SIZE) * scale;
            } else if (hotSpotXUnit === 'fraction') {
                xOffset = -hotSpotX * BILLBOARD_SIZE * scale;
            }
            xOffset += BILLBOARD_SIZE * 0.5 * scale;
        }

        if (defined(hotSpotY)) {
            if (hotSpotYUnit === 'pixels') {
                yOffset = hotSpotY * scale;
            } else if (hotSpotYUnit === 'insetPixels') {
                yOffset = (-hotSpotY + BILLBOARD_SIZE) * scale;
            } else if (hotSpotYUnit === 'fraction') {
                yOffset = hotSpotY * BILLBOARD_SIZE * scale;
            }

            yOffset -= BILLBOARD_SIZE * 0.5 * scale;
        }

        if (defined(xOffset) || defined(yOffset)) {
            billboard.pixelOffset = new Cartesian2(xOffset, yOffset);
        }
    }

    function applyStyle(dataSource, styleNode, targetEntity, sourceUri, uriResolver) {
        for (var i = 0, len = styleNode.childNodes.length; i < len; i++) {
            var node = styleNode.childNodes.item(i);
            if (node.localName === 'IconStyle') {
                processBillboardIcon(dataSource, node, targetEntity, sourceUri, uriResolver);
            } else if (node.localName === 'LabelStyle') {
                var label = targetEntity.label;
                if (!defined(label)) {
                    label = createDefaultLabel();
                    targetEntity.label = label;
                }
                label.scale = defaultValue(queryNumericValue(node, 'scale', namespaces.kml), label.scale);
                label.fillColor = defaultValue(queryColorValue(node, 'color', namespaces.kml), label.fillColor);
                label.text = targetEntity.name;
            } else if (node.localName === 'LineStyle') {
                var polyline = targetEntity.polyline;
                if (!defined(polyline)) {
                    polyline = new PolylineGraphics();
                    targetEntity.polyline = polyline;
                }
                polyline.width = queryNumericValue(node, 'width', namespaces.kml);
                polyline.material = queryColorValue(node, 'color', namespaces.kml);
                if (defined(queryColorValue(node, 'outerColor', namespaces.gx))) {
                    console.log('KML - gx:outerColor is not supported in a LineStyle');
                }
                if (defined(queryNumericValue(node, 'outerWidth', namespaces.gx))) {
                    console.log('KML - gx:outerWidth is not supported in a LineStyle');
                }
                if (defined(queryNumericValue(node, 'physicalWidth', namespaces.gx))) {
                    console.log('KML - gx:physicalWidth is not supported in a LineStyle');
                }
                if (defined(queryBooleanValue(node, 'labelVisibility', namespaces.gx))) {
                    console.log('KML - gx:labelVisibility is not supported in a LineStyle');
                }
            } else if (node.localName === 'PolyStyle') {
                var polygon = targetEntity.polygon;
                if (!defined(polygon)) {
                    polygon = createDefaultPolygon();
                    targetEntity.polygon = polygon;
                }
                polygon.material = defaultValue(queryColorValue(node, 'color', namespaces.kml), polygon.material);
                polygon.fill = defaultValue(queryBooleanValue(node, 'fill', namespaces.kml), polygon.fill);
                polygon.outline = defaultValue(queryBooleanValue(node, 'outline', namespaces.kml), polygon.outline);
            } else if (node.localName === 'BalloonStyle') {
                var bgColor = defaultValue(parseColorString(queryStringValue(node, 'bgColor', namespaces.kml)), Color.WHITE);
                var textColor = defaultValue(parseColorString(queryStringValue(node, 'textColor', namespaces.kml)), Color.BLACK);
                var text = queryStringValue(node, 'text', namespaces.kml);

                //This is purely an internal property used in style processing,
                //it never ends up on the final entity.
                targetEntity.addProperty('balloonStyle');
                targetEntity.balloonStyle = {
                    bgColor : bgColor,
                    textColor : textColor,
                    text : text
                };
            } else if (node.localName === 'ListStyle') {
                var listItemType = queryStringValue(node, 'listItemType', namespaces.kml);
                if (listItemType === 'radioFolder' || listItemType === 'checkOffOnly') {
                    console.log('KML - Unsupported ListStyle with listItemType: ' + listItemType);
                }
            }
        }
    }

    //Processes and merges any inline styles for the provided node into the provided entity.
    function computeFinalStyle(entity, dataSource, placeMark, styleCollection, sourceUri, uriResolver) {
        var result = new Entity();
        var styleEntity;

        //Google earth seems to always use the last inline Style/StyleMap only
        var styleIndex = -1;
        var childNodes = placeMark.childNodes;
        var length = childNodes.length;
        for (var q = 0; q < length; q++) {
            var child = childNodes[q];
            if (child.localName === 'Style' || child.localName === 'StyleMap') {
                styleIndex = q;
            }
        }

        if (styleIndex !== -1) {
            var inlineStyleNode = childNodes[styleIndex];
            if (inlineStyleNode.localName === 'Style') {
                applyStyle(dataSource, inlineStyleNode, result, sourceUri, uriResolver);
            } else { // StyleMap
                var pairs = queryChildNodes(inlineStyleNode, 'Pair', namespaces.kml);
                for (var p = 0; p < pairs.length; p++) {
                    var pair = pairs[p];
                    var key = queryStringValue(pair, 'key', namespaces.kml);
                    if (key === 'normal') {
                        var styleUrl = queryStringValue(pair, 'styleUrl', namespaces.kml);
                        if (defined(styleUrl)) {
                            styleEntity = styleCollection.getById(styleUrl);
                            if (!defined(styleEntity)) {
                                styleEntity = styleCollection.getById('#' + styleUrl);
                            }
                            if (defined(styleEntity)) {
                                result.merge(styleEntity);
                            }
                        } else {
                            var node = queryFirstNode(pair, 'Style', namespaces.kml);
                            applyStyle(dataSource, node, result, sourceUri, uriResolver);
                        }
                    } else {
                        console.log('KML - Unsupported StyleMap key: ' + key);
                    }
                }
            }
        }

        //Google earth seems to always use the first external style only.
        var externalStyle = queryStringValue(placeMark, 'styleUrl', namespaces.kml);
        if (defined(externalStyle)) {
            var id = externalStyle;
            if (externalStyle[0] !== '#' && externalStyle.indexOf('#') !== -1) {
                var tokens = externalStyle.split('#');
                var uri = tokens[0];
                if (defined(sourceUri)) {
                    uri = getAbsoluteUri(uri, getAbsoluteUri(sourceUri));
                }
                id = uri + '#' + tokens[1];
            }

            styleEntity = styleCollection.getById(id);
            if (!defined(styleEntity)) {
                styleEntity = styleCollection.getById('#' + id);
            }
            if (defined(styleEntity)) {
                result.merge(styleEntity);
            }
        }

        return result;
    }

    //Asynchronously processes an external style file.
    function processExternalStyles(dataSource, uri, styleCollection) {
        return loadXML(proxyUrl(uri, dataSource._proxy)).then(function(styleKml) {
            return processStyles(dataSource, styleKml, styleCollection, uri, true);
        });
    }

    //Processes all shared and external styles and stores
    //their id into the provided styleCollection.
    //Returns an array of promises that will resolve when
    //each style is loaded.
    function processStyles(dataSource, kml, styleCollection, sourceUri, isExternal, uriResolver) {
        var i;
        var id;
        var styleEntity;

        var node;
        var styleNodes = queryNodes(kml, 'Style', namespaces.kml);
        if (defined(styleNodes)) {
            var styleNodesLength = styleNodes.length;
            for (i = 0; i < styleNodesLength; i++) {
                node = styleNodes[i];
                id = queryStringAttribute(node, 'id');
                if (defined(id)) {
                    id = '#' + id;
                    if (isExternal && defined(sourceUri)) {
                        id = sourceUri + id;
                    }
                    if (!defined(styleCollection.getById(id))) {
                        styleEntity = new Entity({
                            id : id
                        });
                        styleCollection.add(styleEntity);
                        applyStyle(dataSource, node, styleEntity, sourceUri, uriResolver);
                    }
                }
            }
        }

        var styleMaps = queryNodes(kml, 'StyleMap', namespaces.kml);
        if (defined(styleMaps)) {
            var styleMapsLength = styleMaps.length;
            for (i = 0; i < styleMapsLength; i++) {
                var styleMap = styleMaps[i];
                id = queryStringAttribute(styleMap, 'id');
                if (defined(id)) {
                    var pairs = queryChildNodes(styleMap, 'Pair', namespaces.kml);
                    for (var p = 0; p < pairs.length; p++) {
                        var pair = pairs[p];
                        var key = queryStringValue(pair, 'key', namespaces.kml);
                        if (key === 'normal') {
                            id = '#' + id;
                            if (isExternal && defined(sourceUri)) {
                                id = sourceUri + id;
                            }
                            if (!defined(styleCollection.getById(id))) {
                                styleEntity = styleCollection.getOrCreateEntity(id);

                                var styleUrl = queryStringValue(pair, 'styleUrl', namespaces.kml);
                                if (defined(styleUrl)) {
                                    if (styleUrl[0] !== '#') {
                                        styleUrl = '#' + styleUrl;
                                    }

                                    if (isExternal && defined(sourceUri)) {
                                        styleUrl = sourceUri + styleUrl;
                                    }
                                    var base = styleCollection.getById(styleUrl);

                                    if (defined(base)) {
                                        styleEntity.merge(base);
                                    }
                                } else {
                                    node = queryFirstNode(pair, 'Style', namespaces.kml);
                                    applyStyle(dataSource, node, styleEntity, sourceUri, uriResolver);
                                }
                            }
                        } else {
                            console.log('KML - Unsupported StyleMap key: ' + key);
                        }
                    }
                }
            }
        }

        var externalStyleHash = {};
        var promises = [];
        var styleUrlNodes = kml.getElementsByTagName('styleUrl');
        var styleUrlNodesLength = styleUrlNodes.length;
        for (i = 0; i < styleUrlNodesLength; i++) {
            var styleReference = styleUrlNodes[i].textContent;
            if (styleReference[0] !== '#') {
                //According to the spec, all local styles should start with a #
                //and everything else is an external style that has a # seperating
                //the URL of the document and the style.  However, Google Earth
                //also accepts styleUrls without a # as meaning a local style.
                var tokens = styleReference.split('#');
                if (tokens.length === 2) {
                    var uri = tokens[0];
                    if (!defined(externalStyleHash[uri])) {
                        if (defined(sourceUri)) {
                            uri = getAbsoluteUri(uri, getAbsoluteUri(sourceUri));
                        }
                        promises.push(processExternalStyles(dataSource, uri, styleCollection, sourceUri));
                    }
                }
            }
        }

        return promises;
    }

    function createDropLine(entityCollection, entity, styleEntity) {
        var entityPosition = new ReferenceProperty(entityCollection, entity.id, ['position']);
        var surfacePosition = new ScaledPositionProperty(entity.position);
        entity.polyline = defined(styleEntity.polyline) ? styleEntity.polyline.clone() : new PolylineGraphics();
        entity.polyline.positions = new PositionPropertyArray([entityPosition, surfacePosition]);
    }

    function heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode) {
        if (!defined(altitudeMode) && !defined(gxAltitudeMode) || altitudeMode === 'clampToGround') {
            return HeightReference.CLAMP_TO_GROUND;
        }

        if (altitudeMode === 'relativeToGround') {
            return HeightReference.RELATIVE_TO_GROUND;
        }

        if (altitudeMode === 'absolute') {
            return HeightReference.NONE;
        }

        if (gxAltitudeMode === 'clampToSeaFloor') {
            console.log('KML - <gx:altitudeMode>:clampToSeaFloor is currently not supported, using <kml:altitudeMode>:clampToGround.');
            return HeightReference.CLAMP_TO_GROUND;
        }

        if (gxAltitudeMode === 'relativeToSeaFloor') {
            console.log('KML - <gx:altitudeMode>:relativeToSeaFloor is currently not supported, using <kml:altitudeMode>:relativeToGround.');
            return HeightReference.RELATIVE_TO_GROUND;
        }

        if (defined(altitudeMode)) {
            console.log('KML - Unknown <kml:altitudeMode>:' + altitudeMode + ', using <kml:altitudeMode>:CLAMP_TO_GROUND.');
        } else {
            console.log('KML - Unknown <gx:altitudeMode>:' + gxAltitudeMode + ', using <kml:altitudeMode>:CLAMP_TO_GROUND.');
        }

        // Clamp to ground is the default
        return HeightReference.CLAMP_TO_GROUND;
    }

    function createPositionPropertyFromAltitudeMode(property, altitudeMode, gxAltitudeMode) {
        if (gxAltitudeMode === 'relativeToSeaFloor' || altitudeMode === 'absolute' || altitudeMode === 'relativeToGround') {
            //Just return the ellipsoid referenced property until we support MSL
            return property;
        }

        if ((defined(altitudeMode) && altitudeMode !== 'clampToGround') || //
            (defined(gxAltitudeMode) && gxAltitudeMode !== 'clampToSeaFloor')) {
            console.log('KML - Unknown altitudeMode: ' + defaultValue(altitudeMode, gxAltitudeMode));
        }

        // Clamp to ground is the default
        return new ScaledPositionProperty(property);
    }

    function createPositionPropertyArrayFromAltitudeMode(properties, altitudeMode, gxAltitudeMode) {
        if (!defined(properties)) {
            return undefined;
        }

        if (gxAltitudeMode === 'relativeToSeaFloor' || altitudeMode === 'absolute' || altitudeMode === 'relativeToGround') {
            //Just return the ellipsoid referenced property until we support MSL
            return properties;
        }

        if ((defined(altitudeMode) && altitudeMode !== 'clampToGround') || //
            (defined(gxAltitudeMode) && gxAltitudeMode !== 'clampToSeaFloor')) {
            console.log('KML - Unknown altitudeMode: ' + defaultValue(altitudeMode, gxAltitudeMode));
        }

        // Clamp to ground is the default
        var propertiesLength = properties.length;
        for (var i = 0; i < propertiesLength; i++) {
            var property = properties[i];
            Ellipsoid.WGS84.scaleToGeodeticSurface(property, property);
        }
        return properties;
    }

    function processPositionGraphics(dataSource, entity, styleEntity, heightReference) {
        var label = entity.label;
        if (!defined(label)) {
            label = defined(styleEntity.label) ? styleEntity.label.clone() : createDefaultLabel();
            entity.label = label;
        }
        label.text = entity.name;

        var billboard = entity.billboard;
        if (!defined(billboard)) {
            billboard = defined(styleEntity.billboard) ? styleEntity.billboard.clone() : createDefaultBillboard();
            entity.billboard = billboard;
        }

        if (!defined(billboard.image)) {
            billboard.image = dataSource._pinBuilder.fromColor(Color.YELLOW, 64);
        }

        var scale = 1.0;
        if (defined(billboard.scale)) {
            scale = billboard.scale.getValue();
            if (scale !== 0) {
                label.pixelOffset = new Cartesian2((scale * 16) + 1, 0);
            } else {
                //Minor tweaks to better match Google Earth.
                label.pixelOffset = undefined;
                label.horizontalOrigin = undefined;
            }
        }

        if (defined(heightReference) && dataSource._clampToGround) {
            billboard.heightReference = heightReference;
            label.heightReference = heightReference;
        }
    }

    function processPathGraphics(dataSource, entity, styleEntity) {
        var path = entity.path;
        if (!defined(path)) {
            path = new PathGraphics();
            path.leadTime = 0;
            entity.path = path;
        }

        var polyline = styleEntity.polyline;
        if (defined(polyline)) {
            path.material = polyline.material;
            path.width = polyline.width;
        }
    }

    function processPoint(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        var coordinatesString = queryStringValue(geometryNode, 'coordinates', namespaces.kml);
        var altitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.kml);
        var gxAltitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.gx);
        var extrude = queryBooleanValue(geometryNode, 'extrude', namespaces.kml);

        var position = readCoordinate(coordinatesString);

        entity.position = position;
        processPositionGraphics(dataSource, entity, styleEntity, heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode));

        if (extrude && isExtrudable(altitudeMode, gxAltitudeMode)) {
            createDropLine(entityCollection, entity, styleEntity);
        }

        return true;
    }

    function processLineStringOrLinearRing(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        var coordinatesNode = queryFirstNode(geometryNode, 'coordinates', namespaces.kml);
        var altitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.kml);
        var gxAltitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.gx);
        var extrude = queryBooleanValue(geometryNode, 'extrude', namespaces.kml);
        var tessellate = queryBooleanValue(geometryNode, 'tessellate', namespaces.kml);
        var canExtrude = isExtrudable(altitudeMode, gxAltitudeMode);

        if (defined(queryNumericValue(geometryNode, 'drawOrder', namespaces.gx))) {
            console.log('KML - gx:drawOrder is not supported in LineStrings');
        }

        var coordinates = readCoordinates(coordinatesNode);
        var polyline = styleEntity.polyline;
        if (canExtrude && extrude) {
            var wall = new WallGraphics();
            entity.wall = wall;
            wall.positions = coordinates;
            var polygon = styleEntity.polygon;

            if (defined(polygon)) {
                wall.fill = polygon.fill;
                wall.material = polygon.material;
            }

            //Always outline walls so they show up in 2D.
            wall.outline = true;
            if (defined(polyline)) {
                wall.outlineColor = defined(polyline.material) ? polyline.material.color : Color.WHITE;
                wall.outlineWidth = polyline.width;
            } else if (defined(polygon)) {
                wall.outlineColor = defined(polygon.material) ? polygon.material.color : Color.WHITE;
            }
        } else {
            if (dataSource._clampToGround && !canExtrude && tessellate) {
                var corridor = new CorridorGraphics();
                entity.corridor = corridor;
                corridor.positions = coordinates;
                if (defined(polyline)) {
                    corridor.material = defined(polyline.material) ? polyline.material.color.getValue(Iso8601.MINIMUM_VALUE) : Color.WHITE;
                    corridor.width = defaultValue(polyline.width, 1.0);
                } else {
                    corridor.material = Color.WHITE;
                    corridor.width = 1.0;
                }
            } else {
                polyline = defined(polyline) ? polyline.clone() : new PolylineGraphics();
                entity.polyline = polyline;
                polyline.positions = createPositionPropertyArrayFromAltitudeMode(coordinates, altitudeMode, gxAltitudeMode);
                if (!tessellate || canExtrude) {
                    polyline.followSurface = false;
                }
            }
        }

        return true;
    }

    function processPolygon(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        var outerBoundaryIsNode = queryFirstNode(geometryNode, 'outerBoundaryIs', namespaces.kml);
        var linearRingNode = queryFirstNode(outerBoundaryIsNode, 'LinearRing', namespaces.kml);
        var coordinatesNode = queryFirstNode(linearRingNode, 'coordinates', namespaces.kml);
        var coordinates = readCoordinates(coordinatesNode);
        var extrude = queryBooleanValue(geometryNode, 'extrude', namespaces.kml);
        var altitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.kml);
        var gxAltitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.gx);
        var canExtrude = isExtrudable(altitudeMode, gxAltitudeMode);

        var polygon = defined(styleEntity.polygon) ? styleEntity.polygon.clone() : createDefaultPolygon();

        var polyline = styleEntity.polyline;
        if (defined(polyline)) {
            polygon.outlineColor = defined(polyline.material) ? polyline.material.color : Color.WHITE;
            polygon.outlineWidth = polyline.width;
        }
        entity.polygon = polygon;

        if (canExtrude) {
            polygon.perPositionHeight = true;
            polygon.extrudedHeight = extrude ? 0 : undefined;
        } else if (!dataSource._clampToGround) {
            polygon.height = 0;
        }

        if (defined(coordinates)) {
            var hierarchy = new PolygonHierarchy(coordinates);
            var innerBoundaryIsNodes = queryChildNodes(geometryNode, 'innerBoundaryIs', namespaces.kml);
            for (var j = 0; j < innerBoundaryIsNodes.length; j++) {
                linearRingNode = queryChildNodes(innerBoundaryIsNodes[j], 'LinearRing', namespaces.kml);
                for (var k = 0; k < linearRingNode.length; k++) {
                    coordinatesNode = queryFirstNode(linearRingNode[k], 'coordinates', namespaces.kml);
                    coordinates = readCoordinates(coordinatesNode);
                    if (defined(coordinates)) {
                        hierarchy.holes.push(new PolygonHierarchy(coordinates));
                    }
                }
            }
            polygon.hierarchy = hierarchy;
        }

        return true;
    }

    function processTrack(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        var altitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.kml);
        var gxAltitudeMode = queryStringValue(geometryNode, 'altitudeMode', namespaces.gx);
        var coordNodes = queryChildNodes(geometryNode, 'coord', namespaces.gx);
        var angleNodes = queryChildNodes(geometryNode, 'angles', namespaces.gx);
        var timeNodes = queryChildNodes(geometryNode, 'when', namespaces.kml);
        var extrude = queryBooleanValue(geometryNode, 'extrude', namespaces.kml);
        var canExtrude = isExtrudable(altitudeMode, gxAltitudeMode);

        if (angleNodes.length > 0) {
            console.log('KML - gx:angles are not supported in gx:Tracks');
        }

        var length = Math.min(coordNodes.length, timeNodes.length);
        var coordinates = [];
        var times = [];
        for (var i = 0; i < length; i++) {
            var position = readCoordinate(coordNodes[i].textContent);
            coordinates.push(position);
            times.push(JulianDate.fromIso8601(timeNodes[i].textContent));
        }
        var property = new SampledPositionProperty();
        property.addSamples(times, coordinates);
        entity.position = property;
        processPositionGraphics(dataSource, entity, styleEntity, heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode));
        processPathGraphics(dataSource, entity, styleEntity);

        entity.availability = new TimeIntervalCollection();

        if (timeNodes.length > 0) {
            entity.availability.addInterval(new TimeInterval({
                start : times[0],
                stop : times[times.length - 1]
            }));
        }

        if (canExtrude && extrude) {
            createDropLine(entityCollection, entity, styleEntity);
        }

        return true;
    }

    function addToMultiTrack(times, positions, composite, availability, dropShowProperty, extrude, altitudeMode, gxAltitudeMode, includeEndPoints) {
        var start = times[0];
        var stop = times[times.length - 1];

        var data = new SampledPositionProperty();
        data.addSamples(times, positions);

        composite.intervals.addInterval(new TimeInterval({
            start : start,
            stop : stop,
            isStartIncluded : includeEndPoints,
            isStopIncluded : includeEndPoints,
            data : createPositionPropertyFromAltitudeMode(data, altitudeMode, gxAltitudeMode)
        }));
        availability.addInterval(new TimeInterval({
            start : start,
            stop : stop,
            isStartIncluded : includeEndPoints,
            isStopIncluded : includeEndPoints
        }));
        dropShowProperty.intervals.addInterval(new TimeInterval({
            start : start,
            stop : stop,
            isStartIncluded : includeEndPoints,
            isStopIncluded : includeEndPoints,
            data : extrude
        }));
    }

    function processMultiTrack(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        // Multitrack options do not work in GE as detailed in the spec,
        // rather than altitudeMode being at the MultiTrack level,
        // GE just defers all settings to the underlying track.

        var interpolate = queryBooleanValue(geometryNode, 'interpolate', namespaces.gx);
        var trackNodes = queryChildNodes(geometryNode, 'Track', namespaces.gx);

        var times;
        var lastStop;
        var lastStopPosition;
        var needDropLine = false;
        var dropShowProperty = new TimeIntervalCollectionProperty();
        var availability = new TimeIntervalCollection();
        var composite = new CompositePositionProperty();
        for (var i = 0, len = trackNodes.length; i < len; i++) {
            var trackNode = trackNodes[i];
            var timeNodes = queryChildNodes(trackNode, 'when', namespaces.kml);
            var coordNodes = queryChildNodes(trackNode, 'coord', namespaces.gx);
            var altitudeMode = queryStringValue(trackNode, 'altitudeMode', namespaces.kml);
            var gxAltitudeMode = queryStringValue(trackNode, 'altitudeMode', namespaces.gx);
            var canExtrude = isExtrudable(altitudeMode, gxAltitudeMode);
            var extrude = queryBooleanValue(trackNode, 'extrude', namespaces.kml);

            var length = Math.min(coordNodes.length, timeNodes.length);

            var positions = [];
            times = [];
            for (var x = 0; x < length; x++) {
                var position = readCoordinate(coordNodes[x].textContent);
                positions.push(position);
                times.push(JulianDate.fromIso8601(timeNodes[x].textContent));
            }

            if (interpolate) {
                //If we are interpolating, then we need to fill in the end of
                //the last track and the beginning of this one with a sampled
                //property.  From testing in Google Earth, this property
                //is never extruded and always absolute.
                if (defined(lastStop)) {
                    addToMultiTrack([lastStop, times[0]], [lastStopPosition, positions[0]], composite, availability, dropShowProperty, false, 'absolute', undefined, false);
                }
                lastStop = times[length - 1];
                lastStopPosition = positions[positions.length - 1];
            }

            addToMultiTrack(times, positions, composite, availability, dropShowProperty, canExtrude && extrude, altitudeMode, gxAltitudeMode, true);
            needDropLine = needDropLine || (canExtrude && extrude);
        }

        entity.availability = availability;
        entity.position = composite;
        processPositionGraphics(dataSource, entity, styleEntity);
        processPathGraphics(dataSource, entity, styleEntity);
        if (needDropLine) {
            createDropLine(entityCollection, entity, styleEntity);
            entity.polyline.show = dropShowProperty;
        }

        return true;
    }

    function processMultiGeometry(dataSource, entityCollection, geometryNode, entity, styleEntity, context) {
        var childNodes = geometryNode.childNodes;
        var hasGeometry = false;
        for (var i = 0, len = childNodes.length; i < len; i++) {
            var childNode = childNodes.item(i);
            var geometryProcessor = geometryTypes[childNode.localName];
            if (defined(geometryProcessor)) {
                var childEntity = createEntity(childNode, entityCollection, context);
                childEntity.parent = entity;
                childEntity.name = entity.name;
                childEntity.availability = entity.availability;
                childEntity.description = entity.description;
                childEntity.kml = entity.kml;
                if (geometryProcessor(dataSource, entityCollection, childNode, childEntity, styleEntity)) {
                    hasGeometry = true;
                }
            }
        }

        return hasGeometry;
    }
    function processUnsupportedGeometry(dataSource, entityCollection, geometryNode, entity, styleEntity) {
        console.log('KML - Unsupported geometry: ' + geometryNode.localName);
        return false;
    }

    function processExtendedData(node, entity) {
        var extendedDataNode = queryFirstNode(node, 'ExtendedData', namespaces.kml);

        if (!defined(extendedDataNode)) {
            return undefined;
        }

        if (defined(queryFirstNode(extendedDataNode, 'SchemaData', namespaces.kml))) {
            console.log('KML - SchemaData is unsupported');
        }
        if (defined(queryStringAttribute(extendedDataNode, 'xmlns:prefix'))) {
            console.log('KML - ExtendedData with xmlns:prefix is unsupported');
        }

        var result = {};
        var dataNodes = queryChildNodes(extendedDataNode, 'Data', namespaces.kml);
        if (defined(dataNodes)) {
            var length = dataNodes.length;
            for (var i = 0; i < length; i++) {
                var dataNode = dataNodes[i];
                var name = queryStringAttribute(dataNode, 'name');
                if (defined(name)) {
                    result[name] = {
                        displayName : queryStringValue(dataNode, 'displayName', namespaces.kml),
                        value : queryStringValue(dataNode, 'value', namespaces.kml)
                    };
                }
            }
        }
        entity.kml.extendedData = result;
    }

    var scratchDiv = document.createElement('div');

    function processDescription(node, entity, styleEntity, uriResolver) {
        var i;
        var key;
        var keys;

        var kmlData = entity.kml;
        var extendedData = kmlData.extendedData;
        var description = queryStringValue(node, 'description', namespaces.kml);

        var balloonStyle = defaultValue(entity.balloonStyle, styleEntity.balloonStyle);

        var background = Color.WHITE;
        var foreground = Color.BLACK;
        var text = description;

        if (defined(balloonStyle)) {
            background = defaultValue(balloonStyle.bgColor, Color.WHITE);
            foreground = defaultValue(balloonStyle.textColor, Color.BLACK);
            text = defaultValue(balloonStyle.text, description);
        }

        var value;
        if (defined(text)) {
            text = text.replace('$[name]', defaultValue(entity.name, ''));
            text = text.replace('$[description]', defaultValue(description, ''));
            text = text.replace('$[address]', defaultValue(kmlData.address, ''));
            text = text.replace('$[Snippet]', defaultValue(kmlData.snippet, ''));
            text = text.replace('$[id]', entity.id);

            //While not explicitly defined by the OGC spec, in Google Earth
            //The appearance of geDirections adds the directions to/from links
            //We simply replace this string with nothing.
            text = text.replace('$[geDirections]', '');

            if (defined(extendedData)) {
                var matches = text.match(/\$\[.+?\]/g);
                if (matches !== null) {
                    for (i = 0; i < matches.length; i++) {
                        var token = matches[i];
                        var propertyName = token.substr(2, token.length - 3);
                        var isDisplayName = /\/displayName$/.test(propertyName);
                        propertyName = propertyName.replace(/\/displayName$/, '');

                        value = extendedData[propertyName];
                        if (defined(value)) {
                            value = isDisplayName ? value.displayName : value.value;
                        }
                        if (defined(value)) {
                            text = text.replace(token, defaultValue(value, ''));
                        }
                    }
                }
            }
        } else if (defined(extendedData)) {
            //If no description exists, build a table out of the extended data
            keys = Object.keys(extendedData);
            if (keys.length > 0) {
                text = '<table class="cesium-infoBox-defaultTable cesium-infoBox-defaultTable-lighter"><tbody>';
                for (i = 0; i < keys.length; i++) {
                    key = keys[i];
                    value = extendedData[key];
                    text += '<tr><th>' + defaultValue(value.displayName, key) + '</th><td>' + defaultValue(value.value, '') + '</td></tr>';
                }
                text += '</tbody></table>';
            }
        }

        if (!defined(text)) {
            //No description
            return;
        }

        //Turns non-explicit links into clickable links.
        text = autolinker.link(text);

        //Use a temporary div to manipulate the links
        //so that they open in a new window.
        scratchDiv.innerHTML = text;
        var links = scratchDiv.querySelectorAll('a');
        for (i = 0; i < links.length; i++) {
            links[i].setAttribute('target', '_blank');
        }

        //Rewrite any KMZ embedded urls
        if (defined(uriResolver) && uriResolver.keys.length > 1) {
            replaceAttributes(scratchDiv, 'a', 'href', uriResolver);
            replaceAttributes(scratchDiv, 'img', 'src', uriResolver);
        }

        var tmp = '<div class="cesium-infoBox-description-lighter" style="';
        tmp += 'overflow:auto;';
        tmp += 'word-wrap:break-word;';
        tmp += 'background-color:' + background.toCssColorString() + ';';
        tmp += 'color:' + foreground.toCssColorString() + ';';
        tmp += '">';
        tmp += scratchDiv.innerHTML + '</div>';
        scratchDiv.innerHTML = '';

        //Set the final HTML as the description.
        entity.description = tmp;
    }

    function processFeature(dataSource, parent, featureNode, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var entity = createEntity(featureNode, entityCollection, context);
        var kmlData = entity.kml;
        var styleEntity = computeFinalStyle(entity, dataSource, featureNode, styleCollection, sourceUri, uriResolver);

        var name = queryStringValue(featureNode, 'name', namespaces.kml);
        entity.name = name;
        entity.parent = parent;

        var availability = processTimeSpan(featureNode);
        if (!defined(availability)) {
            availability = processTimeStamp(featureNode);
        }
        entity.availability = availability;

        mergeAvailabilityWithParent(entity);

        // Per KML spec "A Feature is visible only if it and all its ancestors are visible."
        function ancestryIsVisible(parentEntity) {
            if (!parentEntity) {
              return true;
            }
            return parentEntity.show && ancestryIsVisible(parentEntity.parent);
        }

        var visibility = queryBooleanValue(featureNode, 'visibility', namespaces.kml);
        entity.show = ancestryIsVisible(parent) && defaultValue(visibility, true);
        //var open = queryBooleanValue(featureNode, 'open', namespaces.kml);

        var authorNode = queryFirstNode(featureNode, 'author', namespaces.atom);
        var author = kmlData.author;
        author.name = queryStringValue(authorNode, 'name', namespaces.atom);
        author.uri = queryStringValue(authorNode, 'uri', namespaces.atom);
        author.email = queryStringValue(authorNode, 'email', namespaces.atom);

        var linkNode = queryFirstNode(featureNode, 'link', namespaces.atom);
        var link = kmlData.link;
        link.href = queryStringAttribute(linkNode, 'href');
        link.hreflang = queryStringAttribute(linkNode, 'hreflang');
        link.rel = queryStringAttribute(linkNode, 'rel');
        link.type = queryStringAttribute(linkNode, 'type');
        link.title = queryStringAttribute(linkNode, 'title');
        link.length = queryStringAttribute(linkNode, 'length');

        kmlData.address = queryStringValue(featureNode, 'address', namespaces.kml);
        kmlData.phoneNumber = queryStringValue(featureNode, 'phoneNumber', namespaces.kml);
        kmlData.snippet = queryStringValue(featureNode, 'Snippet', namespaces.kml);

        processExtendedData(featureNode, entity);
        processDescription(featureNode, entity, styleEntity, uriResolver);

        if (defined(queryFirstNode(featureNode, 'Camera', namespaces.kml))) {
            console.log('KML - Unsupported view: Camera');
        }
        if (defined(queryFirstNode(featureNode, 'LookAt', namespaces.kml))) {
            console.log('KML - Unsupported view: LookAt');
        }
        if (defined(queryFirstNode(featureNode, 'Region', namespaces.kml))) {
            console.log('KML - Placemark Regions are unsupported');
        }

        return {
            entity : entity,
            styleEntity : styleEntity
        };
    }

    var geometryTypes = {
        Point : processPoint,
        LineString : processLineStringOrLinearRing,
        LinearRing : processLineStringOrLinearRing,
        Polygon : processPolygon,
        Track : processTrack,
        MultiTrack : processMultiTrack,
        MultiGeometry : processMultiGeometry,
        Model : processUnsupportedGeometry
    };

    function processDocument(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var featureTypeNames = Object.keys(featureTypes);
        var featureTypeNamesLength = featureTypeNames.length;

        for (var i = 0; i < featureTypeNamesLength; i++) {
            var featureName = featureTypeNames[i];
            var processFeatureNode = featureTypes[featureName];

            var childNodes = node.childNodes;
            var length = childNodes.length;
            for (var q = 0; q < length; q++) {
                var child = childNodes[q];
                if (child.localName === featureName &&
                    ((namespaces.kml.indexOf(child.namespaceURI) !== -1) || (namespaces.gx.indexOf(child.namespaceURI) !== -1))) {
                    processFeatureNode(dataSource, parent, child, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
                }
            }
        }
    }

    function processFolder(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var r = processFeature(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        processDocument(dataSource, r.entity, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
    }

    function processPlacemark(dataSource, parent, placemark, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var r = processFeature(dataSource, parent, placemark, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        var entity = r.entity;
        var styleEntity = r.styleEntity;

        var hasGeometry = false;
        var childNodes = placemark.childNodes;
        for (var i = 0, len = childNodes.length; i < len && !hasGeometry; i++) {
            var childNode = childNodes.item(i);
            var geometryProcessor = geometryTypes[childNode.localName];
            if (defined(geometryProcessor)) {
                // pass the placemark entity id as a context for case of defining multiple child entities together to handle case
                // where some malformed kmls reuse the same id across placemarks, which works in GE, but is not technically to spec.
                geometryProcessor(dataSource, entityCollection, childNode, entity, styleEntity, entity.id);
                hasGeometry = true;
            }
        }

        if (!hasGeometry) {
            entity.merge(styleEntity);
            processPositionGraphics(dataSource, entity, styleEntity);
        }
    }

    function processGroundOverlay(dataSource, parent, groundOverlay, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var r = processFeature(dataSource, parent, groundOverlay, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        var entity = r.entity;

        var geometry;
        var isLatLonQuad = false;

        var positions = readCoordinates(queryFirstNode(groundOverlay, 'LatLonQuad', namespaces.gx));
        if (defined(positions)) {
            geometry = createDefaultPolygon();
            geometry.hierarchy = new PolygonHierarchy(positions);
            entity.polygon = geometry;
            isLatLonQuad = true;
        } else {
            geometry = new RectangleGraphics();
            entity.rectangle = geometry;

            var latLonBox = queryFirstNode(groundOverlay, 'LatLonBox', namespaces.kml);
            if (defined(latLonBox)) {
                var west = queryNumericValue(latLonBox, 'west', namespaces.kml);
                var south = queryNumericValue(latLonBox, 'south', namespaces.kml);
                var east = queryNumericValue(latLonBox, 'east', namespaces.kml);
                var north = queryNumericValue(latLonBox, 'north', namespaces.kml);

                if (defined(west)) {
                    west = CesiumMath.negativePiToPi(CesiumMath.toRadians(west));
                }
                if (defined(south)) {
                    south = CesiumMath.clampToLatitudeRange(CesiumMath.toRadians(south));
                }
                if (defined(east)) {
                    east = CesiumMath.negativePiToPi(CesiumMath.toRadians(east));
                }
                if (defined(north)) {
                    north = CesiumMath.clampToLatitudeRange(CesiumMath.toRadians(north));
                }
                geometry.coordinates = new Rectangle(west, south, east, north);

                var rotation = queryNumericValue(latLonBox, 'rotation', namespaces.kml);
                if (defined(rotation)) {
                    geometry.rotation = CesiumMath.toRadians(rotation);
                }
            }
        }

        var iconNode = queryFirstNode(groundOverlay, 'Icon', namespaces.kml);
        var href = getIconHref(iconNode, dataSource, sourceUri, uriResolver, true);
        if (defined(href)) {
            if (isLatLonQuad) {
                console.log('KML - gx:LatLonQuad Icon does not support texture projection.');
            }
            var x = queryNumericValue(iconNode, 'x', namespaces.gx);
            var y = queryNumericValue(iconNode, 'y', namespaces.gx);
            var w = queryNumericValue(iconNode, 'w', namespaces.gx);
            var h = queryNumericValue(iconNode, 'h', namespaces.gx);

            if (defined(x) || defined(y) || defined(w) || defined(h)) {
                console.log('KML - gx:x, gx:y, gx:w, gx:h aren\'t supported for GroundOverlays');
            }

            geometry.material = href;
            geometry.material.color = queryColorValue(groundOverlay, 'color', namespaces.kml);
            geometry.material.transparent = true;
        } else {
            geometry.material = queryColorValue(groundOverlay, 'color', namespaces.kml);
        }

        var altitudeMode = queryStringValue(groundOverlay, 'altitudeMode', namespaces.kml);

        if (defined(altitudeMode)) {
            if (altitudeMode === 'absolute') {
                //Use height above ellipsoid until we support MSL.
                geometry.height = queryNumericValue(groundOverlay, 'altitude', namespaces.kml);
            } else if (altitudeMode !== 'clampToGround'){
                console.log('KML - Unknown altitudeMode: ' + altitudeMode);
            }
            // else just use the default of 0 until we support 'clampToGround'
        } else {
            altitudeMode = queryStringValue(groundOverlay, 'altitudeMode', namespaces.gx);
            if (altitudeMode === 'relativeToSeaFloor') {
                console.log('KML - altitudeMode relativeToSeaFloor is currently not supported, treating as absolute.');
                geometry.height = queryNumericValue(groundOverlay, 'altitude', namespaces.kml);
            } else if (altitudeMode === 'clampToSeaFloor') {
                console.log('KML - altitudeMode clampToSeaFloor is currently not supported, treating as clampToGround.');
            } else if (defined(altitudeMode)) {
                console.log('KML - Unknown altitudeMode: ' + altitudeMode);
            }
        }
    }

    function processUnsupportedFeature(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        dataSource._unsupportedNode.raiseEvent(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver);
        console.log('KML - Unsupported feature: ' + node.localName);
    }

    var RefreshMode = {
        INTERVAL : 0,
        EXPIRE : 1,
        STOP : 2
    };


    function cleanupString(s) {
        if (!defined(s) || s.length === 0) {
            return '';
        }

        var sFirst = s[0];
        if (sFirst === '&') {
            s.splice(0, 1);
        }

        if(sFirst !== '?') {
            s = '?' + s;
        }

        return s;
    }

    function makeQueryString(string1, string2) {
        var result = '';
        if ((defined(string1) && string1.length > 0) || (defined(string2) && string2.length > 0)) {
            result += joinUrls(cleanupString(string1), cleanupString(string2), false);
        }

        return result;
    }

    var zeroRectangle = new Rectangle();
    var scratchCartographic = new Cartographic();
    var scratchCartesian2 = new Cartesian2();
    var scratchCartesian3 = new Cartesian3();
    function processNetworkLinkQueryString(camera, canvas, queryString, viewBoundScale, bbox) {
        function fixLatitude(value) {
            if (value < -CesiumMath.PI_OVER_TWO) {
                return -CesiumMath.PI_OVER_TWO;
            } else if (value > CesiumMath.PI_OVER_TWO) {
                return CesiumMath.PI_OVER_TWO;
            }
            return value;
        }

        function fixLongitude(value) {
            if (value > CesiumMath.PI) {
                return value - CesiumMath.TWO_PI;
            } else if (value < -CesiumMath.PI) {
                return value + CesiumMath.TWO_PI;
            }

            return value;
        }

        if (defined(camera) && camera._mode !== SceneMode.MORPHING) {
            var wgs84 = Ellipsoid.WGS84;
            var centerCartesian;
            var centerCartographic;

            bbox = defaultValue(bbox, zeroRectangle);
            if (defined(canvas)) {
                scratchCartesian2.x = canvas.clientWidth * 0.5;
                scratchCartesian2.y = canvas.clientHeight * 0.5;
                centerCartesian = camera.pickEllipsoid(scratchCartesian2, wgs84, scratchCartesian3);
            }

            if (defined(centerCartesian)) {
                centerCartographic = wgs84.cartesianToCartographic(centerCartesian, scratchCartographic);
            } else {
                centerCartographic = Rectangle.center(bbox, scratchCartographic);
                centerCartesian = wgs84.cartographicToCartesian(centerCartographic);
            }


            if (defined(viewBoundScale) && !CesiumMath.equalsEpsilon(viewBoundScale, 1.0, CesiumMath.EPSILON9)) {
                var newHalfWidth = bbox.width * viewBoundScale * 0.5;
                var newHalfHeight = bbox.height * viewBoundScale * 0.5;
                bbox = new Rectangle(fixLongitude(centerCartographic.longitude - newHalfWidth),
                    fixLatitude(centerCartographic.latitude - newHalfHeight),
                    fixLongitude(centerCartographic.longitude + newHalfWidth),
                    fixLatitude(centerCartographic.latitude + newHalfHeight)
                );
            }

            queryString = queryString.replace('[bboxWest]', CesiumMath.toDegrees(bbox.west).toString());
            queryString = queryString.replace('[bboxSouth]', CesiumMath.toDegrees(bbox.south).toString());
            queryString = queryString.replace('[bboxEast]', CesiumMath.toDegrees(bbox.east).toString());
            queryString = queryString.replace('[bboxNorth]', CesiumMath.toDegrees(bbox.north).toString());

            var lon = CesiumMath.toDegrees(centerCartographic.longitude).toString();
            var lat = CesiumMath.toDegrees(centerCartographic.latitude).toString();
            queryString = queryString.replace('[lookatLon]', lon);
            queryString = queryString.replace('[lookatLat]', lat);
            queryString = queryString.replace('[lookatTilt]', CesiumMath.toDegrees(camera.pitch).toString());
            queryString = queryString.replace('[lookatHeading]', CesiumMath.toDegrees(camera.heading).toString());
            queryString = queryString.replace('[lookatRange]', Cartesian3.distance(camera.positionWC, centerCartesian));
            queryString = queryString.replace('[lookatTerrainLon]', lon);
            queryString = queryString.replace('[lookatTerrainLat]', lat);
            queryString = queryString.replace('[lookatTerrainAlt]', centerCartographic.height.toString());

            wgs84.cartesianToCartographic(camera.positionWC, scratchCartographic);
            queryString = queryString.replace('[cameraLon]', CesiumMath.toDegrees(scratchCartographic.longitude).toString());
            queryString = queryString.replace('[cameraLat]', CesiumMath.toDegrees(scratchCartographic.latitude).toString());
            queryString = queryString.replace('[cameraAlt]', CesiumMath.toDegrees(scratchCartographic.height).toString());

            var frustum = camera.frustum;
            var aspectRatio = frustum.aspectRatio;
            var horizFov = '';
            var vertFov = '';
            if (defined(aspectRatio)) {
                var fov = CesiumMath.toDegrees(frustum.fov);
                if (aspectRatio > 1.0) {
                    horizFov = fov;
                    vertFov = fov / aspectRatio;
                } else {
                    vertFov = fov;
                    horizFov = fov * aspectRatio;
                }
            }
            queryString = queryString.replace('[horizFov]', horizFov.toString());
            queryString = queryString.replace('[vertFov]', vertFov.toString());
        } else {
            queryString = queryString.replace('[bboxWest]', '-180');
            queryString = queryString.replace('[bboxSouth]', '-90');
            queryString = queryString.replace('[bboxEast]', '180');
            queryString = queryString.replace('[bboxNorth]', '90');

            queryString = queryString.replace('[lookatLon]', '');
            queryString = queryString.replace('[lookatLat]', '');
            queryString = queryString.replace('[lookatRange]', '');
            queryString = queryString.replace('[lookatTilt]', '');
            queryString = queryString.replace('[lookatHeading]', '');
            queryString = queryString.replace('[lookatTerrainLon]', '');
            queryString = queryString.replace('[lookatTerrainLat]', '');
            queryString = queryString.replace('[lookatTerrainAlt]', '');

            queryString = queryString.replace('[cameraLon]', '');
            queryString = queryString.replace('[cameraLat]', '');
            queryString = queryString.replace('[cameraAlt]', '');
            queryString = queryString.replace('[horizFov]', '');
            queryString = queryString.replace('[vertFov]', '');
        }

        if (defined(canvas)) {
            queryString = queryString.replace('[horizPixels]', canvas.clientWidth);
            queryString = queryString.replace('[vertPixels]', canvas.clientHeight);
        } else {
            queryString = queryString.replace('[horizPixels]', '');
            queryString = queryString.replace('[vertPixels]', '');
        }

        queryString = queryString.replace('[terrainEnabled]', '1');
        queryString = queryString.replace('[clientVersion]', '1');
        queryString = queryString.replace('[kmlVersion]', '2.2');
        queryString = queryString.replace('[clientName]', 'Cesium');
        queryString = queryString.replace('[language]', 'English');

        return queryString;
    }

    function processNetworkLink(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var r = processFeature(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        var networkEntity = r.entity;

        var link = queryFirstNode(node, 'Link', namespaces.kml);

        if(!defined(link)){
            link = queryFirstNode(node, 'Url', namespaces.kml);
        }
        if (defined(link)) {
            var href = queryStringValue(link, 'href', namespaces.kml);
            if (defined(href)) {
                var newSourceUri = href;
                href = resolveHref(href, undefined, sourceUri, uriResolver);
                var linkUrl;

                // We need to pass in the original path if resolveHref returns a data uri because the network link
                //  references a document in a KMZ archive
                if (/^data:/.test(href)) {
                    // No need to build a query string for a data uri, just use as is
                    linkUrl = href;

                    // So if sourceUri isn't the kmz file, then its another kml in the archive, so resolve it
                    if (!/\.kmz/i.test(sourceUri)) {
                        newSourceUri = getAbsoluteUri(newSourceUri, sourceUri);
                    }
                } else {
                    newSourceUri = href; // Not a data uri so use the fully qualified uri
                    var viewRefreshMode = queryStringValue(link, 'viewRefreshMode', namespaces.kml);
                    var viewBoundScale = defaultValue(queryStringValue(link, 'viewBoundScale', namespaces.kml), 1.0);
                    var defaultViewFormat = (viewRefreshMode === 'onStop') ? 'BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]' : '';
                    var viewFormat = defaultValue(queryStringValue(link, 'viewFormat', namespaces.kml), defaultViewFormat);
                    var httpQuery = queryStringValue(link, 'httpQuery', namespaces.kml);
                    var queryString = makeQueryString(viewFormat, httpQuery);

                    linkUrl = processNetworkLinkQueryString(dataSource._camera, dataSource._canvas, joinUrls(href, queryString, false),
                        viewBoundScale, dataSource._lastCameraView.bbox);
                }

                var options = {
                    sourceUri : newSourceUri,
                    uriResolver : uriResolver,
                    context : networkEntity.id
                };
                var networkLinkCollection = new EntityCollection();
                var promise = load(dataSource, networkLinkCollection, linkUrl, options).then(function(rootElement) {
                    var entities = dataSource._entityCollection;
                    var newEntities = networkLinkCollection.values;
                    entities.suspendEvents();
                    for (var i = 0; i < newEntities.length; i++) {
                        var newEntity = newEntities[i];
                        if (!defined(newEntity.parent)) {
                            newEntity.parent = networkEntity;
                            mergeAvailabilityWithParent(newEntity);
                        }

                        entities.add(newEntity);
                    }
                    entities.resumeEvents();

                    // Add network links to a list if we need they will need to be updated
                    var refreshMode = queryStringValue(link, 'refreshMode', namespaces.kml);
                    var refreshInterval = defaultValue(queryNumericValue(link, 'refreshInterval', namespaces.kml), 0);
                    if ((refreshMode === 'onInterval' && refreshInterval > 0 ) || (refreshMode === 'onExpire') || (viewRefreshMode === 'onStop')) {
                        var networkLinkControl = queryFirstNode(rootElement, 'NetworkLinkControl', namespaces.kml);
                        var hasNetworkLinkControl = defined(networkLinkControl);

                        var now = JulianDate.now();
                        var networkLinkInfo = {
                            id : createGuid(),
                            href : href,
                            cookie : '',
                            queryString : queryString,
                            lastUpdated : now,
                            updating : false,
                            entity : networkEntity,
                            viewBoundScale : viewBoundScale,
                            needsUpdate : false,
                            cameraUpdateTime : now
                        };

                        var minRefreshPeriod = 0;
                        if (hasNetworkLinkControl) {
                            networkLinkInfo.cookie = defaultValue(queryStringValue(networkLinkControl, 'cookie', namespaces.kml), '');
                            minRefreshPeriod = defaultValue(queryNumericValue(networkLinkControl, 'minRefreshPeriod', namespaces.kml), 0);
                        }

                        if (refreshMode === 'onInterval') {
                            if (hasNetworkLinkControl) {
                                refreshInterval = Math.max(minRefreshPeriod, refreshInterval);
                            }
                            networkLinkInfo.refreshMode = RefreshMode.INTERVAL;
                            networkLinkInfo.time = refreshInterval;
                        } else if (refreshMode === 'onExpire') {
                            var expires;
                            if (hasNetworkLinkControl) {
                                expires = queryStringValue(networkLinkControl, 'expires', namespaces.kml);
                            }
                            if (defined(expires)) {
                                try {
                                    var date = JulianDate.fromIso8601(expires);
                                    var diff = JulianDate.secondsDifference(date, now);
                                    if (diff > 0 && diff < minRefreshPeriod) {
                                        JulianDate.addSeconds(now, minRefreshPeriod, date);
                                    }
                                    networkLinkInfo.refreshMode = RefreshMode.EXPIRE;
                                    networkLinkInfo.time = date;
                                } catch (e) {
                                    console.log('KML - NetworkLinkControl expires is not a valid date');
                                }
                            } else {
                                console.log('KML - refreshMode of onExpire requires the NetworkLinkControl to have an expires element');
                            }
                        } else {
                            if (dataSource._camera) { // Only allow onStop refreshes if we have a camera
                                networkLinkInfo.refreshMode = RefreshMode.STOP;
                                networkLinkInfo.time = defaultValue(queryNumericValue(link, 'viewRefreshTime', namespaces.kml), 0);
                            } else {
                                console.log('A NetworkLink with viewRefreshMode=onStop requires a camera be passed in when creating the KmlDataSource');
                            }
                        }

                        if (defined(networkLinkInfo.refreshMode)) {
                            dataSource._networkLinks.set(networkLinkInfo.id, networkLinkInfo);
                        }
                    } else if (viewRefreshMode === 'onRegion'){
                        console.log('KML - Unsupported viewRefreshMode: onRegion');
                    }
                });

                promises.push(promise);
            }
        }
    }

    // Ensure Specs/Data/KML/unsupported.kml is kept up to date with these supported types
    var featureTypes = {
        Document : processDocument,
        Folder : processFolder,
        Placemark : processPlacemark,
        NetworkLink : processNetworkLink,
        GroundOverlay : processGroundOverlay,
        PhotoOverlay : processUnsupportedFeature,
        ScreenOverlay : processUnsupportedFeature,
        Tour : processUnsupportedFeature
    };

    function processFeatureNode(dataSource, node, parent, entityCollection, styleCollection, sourceUri, uriResolver, promises, context) {
        var featureProcessor = featureTypes[node.localName];
        if (defined(featureProcessor)) {
            featureProcessor(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        } else {
            processUnsupportedFeature(dataSource, parent, node, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
        }
    }

    function loadKml(dataSource, entityCollection, kml, sourceUri, uriResolver, context) {
        entityCollection.removeAll();

        var promises = [];
        var documentElement = kml.documentElement;
        var document = documentElement.localName === 'Document' ? documentElement : queryFirstNode(documentElement, 'Document', namespaces.kml);
        var name = queryStringValue(document, 'name', namespaces.kml);
        if (!defined(name) && defined(sourceUri)) {
            name = getFilenameFromUri(sourceUri);
        }

        // Only set the name from the root document
        if (!defined(dataSource._name)) {
            dataSource._name = name;
        }

        var styleCollection = new EntityCollection(dataSource);
        return when.all(processStyles(dataSource, kml, styleCollection, sourceUri, false, uriResolver, context)).then(function() {
            var element = kml.documentElement;
            if (element.localName === 'kml') {
                var childNodes = element.childNodes;
                for (var i = 0; i < childNodes.length; i++) {
                    var tmp = childNodes[i];
                    if (defined(featureTypes[tmp.localName])) {
                        element = tmp;
                        break;
                    }
                }
            }
            entityCollection.suspendEvents();
            processFeatureNode(dataSource, element, undefined, entityCollection, styleCollection, sourceUri, uriResolver, promises, context);
            entityCollection.resumeEvents();

            return when.all(promises).then(function() {
                return kml.documentElement;
            });
        });
    }

    function loadKmz(dataSource, entityCollection, blob, sourceUri) {
        var deferred = when.defer();
        zip.createReader(new zip.BlobReader(blob), function(reader) {
            reader.getEntries(function(entries) {
                var promises = [];
                var uriResolver = {};
                var docEntry;
                var docDefer;
                for (var i = 0; i < entries.length; i++) {
                    var entry = entries[i];
                    if (!entry.directory) {
                        var innerDefer = when.defer();
                        promises.push(innerDefer.promise);
                        if (/\.kml$/i.test(entry.filename)) {
                            // We use the first KML document we come across
                            //  https://developers.google.com/kml/documentation/kmzarchives
                            // Unless we come across a .kml file at the root of the archive because GE does this
                            if (!defined(docEntry) || !/\//i.test(entry.filename)) {
                                if (defined(docEntry)) {
                                    // We found one at the root so load the initial kml as a data uri
                                    loadDataUriFromZip(reader, docEntry, uriResolver, docDefer);
                                }
                                docEntry = entry;
                                docDefer = innerDefer;
                            } else {
                                // Wasn't the first kml and wasn't at the root
                                loadDataUriFromZip(reader, entry, uriResolver, innerDefer);
                            }
                        } else {
                            loadDataUriFromZip(reader, entry, uriResolver, innerDefer);
                        }
                    }
                }

                // Now load the root KML document
                if (defined(docEntry)) {
                    loadXmlFromZip(reader, docEntry, uriResolver, docDefer);
                }
                when.all(promises).then(function() {
                    reader.close();
                    if (!defined(uriResolver.kml)) {
                        deferred.reject(new RuntimeError('KMZ file does not contain a KML document.'));
                        return;
                    }
                    uriResolver.keys = Object.keys(uriResolver);
                    return loadKml(dataSource, entityCollection, uriResolver.kml, sourceUri, uriResolver);
                }).then(deferred.resolve).otherwise(deferred.reject);
            });
        }, function(e) {
            deferred.reject(e);
        });

        return deferred.promise;
    }

    function load(dataSource, entityCollection, data, options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);
        var sourceUri = options.sourceUri;
        var uriResolver = options.uriResolver;
        var context = options.context;

        var promise = data;
        if (typeof data === 'string') {
            promise = loadBlob(proxyUrl(data, dataSource._proxy));
            sourceUri = defaultValue(sourceUri, data);
        }

        return when(promise)
            .then(function(dataToLoad) {
                if (dataToLoad instanceof Blob) {
                    return isZipFile(dataToLoad).then(function(isZip) {
                        if (isZip) {
                            return loadKmz(dataSource, entityCollection, dataToLoad, sourceUri);
                        }
                        return readBlobAsText(dataToLoad).then(function(text) {
                            //There's no official way to validate if a parse was successful.
                            //The following check detects the error on various browsers.

                            //IE raises an exception
                            var kml;
                            var error;
                            try {
                                kml = parser.parseFromString(text, 'application/xml');
                            } catch (e) {
                                error = e.toString();
                            }

                            //The parse succeeds on Chrome and Firefox, but the error
                            //handling is different in each.
                            if (defined(error) || kml.body || kml.documentElement.tagName === 'parsererror') {
                                //Firefox has error information as the firstChild nodeValue.
                                var msg = defined(error) ? error : kml.documentElement.firstChild.nodeValue;

                                //Chrome has it in the body text.
                                if (!msg) {
                                    msg = kml.body.innerText;
                                }

                                //Return the error
                                throw new RuntimeError(msg);
                            }
                            return loadKml(dataSource, entityCollection, kml, sourceUri, uriResolver, context);
                        });
                    });
                } else {
                    return loadKml(dataSource, entityCollection, dataToLoad, sourceUri, uriResolver, context);
                }
            })
            .otherwise(function(error) {
                dataSource._error.raiseEvent(dataSource, error);
                console.log(error);
                return when.reject(error);
            });
    }

    /**
     * A {@link DataSource} which processes Keyhole Markup Language 2.2 (KML).
     * <p>
     * KML support in Cesium is incomplete, but a large amount of the standard,
     * as well as Google's <code>gx</code> extension namespace, is supported. See Github issue
     * {@link https://github.com/AnalyticalGraphicsInc/cesium/issues/873|#873} for a
     * detailed list of what is and isn't support. Cesium will also write information to the
     * console when it encounters most unsupported features.
     * </p>
     * <p>
     * Non visual feature data, such as <code>atom:author</code> and <code>ExtendedData</code>
     * is exposed via an instance of {@link KmlFeatureData}, which is added to each {@link Entity}
     * under the <code>kml</code> property.
     * </p>
     *
     * @alias KmlDataSource
     * @constructor
     *
     * @param {Camera} options.camera The camera that is used for viewRefreshModes and sending camera properties to network links.
     * @param {Canvas} options.canvas The canvas that is used for sending viewer properties to network links.
     * @param {DefaultProxy} [options.proxy] A proxy to be used for loading external data.
     *
     * @see {@link http://www.opengeospatial.org/standards/kml/|Open Geospatial Consortium KML Standard}
     * @see {@link https://developers.google.com/kml/|Google KML Documentation}
     *
     * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=KML.html|Cesium Sandcastle KML Demo}
     *
     * @example
     * var viewer = new Cesium.Viewer('cesiumContainer');
     * viewer.dataSources.add(Cesium.KmlDataSource.load('../../SampleData/facilities.kmz',
     *      {
     *           camera: viewer.scene.camera,
     *           canvas: viewer.scene.canvas
     *      })
     * );
     */
    function KmlDataSource(options) {
        options = defaultValue(options, {});
        var camera = options.camera;
        var canvas = options.canvas;

        //>>includeStart('debug', pragmas.debug);
        if (!defined(camera)) {
            throw new DeveloperError('options.camera is required.');
        }
        if (!defined(canvas)) {
            throw new DeveloperError('options.canvas is required.');
        }
        //>>includeEnd('debug');

        this._changed = new Event();
        this._error = new Event();
        this._loading = new Event();
        this._refresh = new Event();
        this._unsupportedNode = new Event();

        this._clock = undefined;
        this._entityCollection = new EntityCollection(this);
        this._name = undefined;
        this._isLoading = false;
        this._proxy = options.proxy;
        this._pinBuilder = new PinBuilder();
        this._networkLinks = new AssociativeArray();
        this._entityCluster = new EntityCluster();

        this._canvas = canvas;
        this._camera = camera;
        this._lastCameraView = {
            position : defined(camera) ? Cartesian3.clone(camera.positionWC) : undefined,
            direction : defined(camera) ? Cartesian3.clone(camera.directionWC) : undefined,
            up : defined(camera) ? Cartesian3.clone(camera.upWC) : undefined,
            bbox : defined(camera) ? camera.computeViewRectangle() : Rectangle.clone(Rectangle.MAX_VALUE)
        };
    }

    /**
     * Creates a Promise to a new instance loaded with the provided KML data.
     *
     * @param {String|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document.
     * @param {Object} [options] An object with the following properties:
     * @param {Camera} options.camera The camera that is used for viewRefreshModes and sending camera properties to network links.
     * @param {Canvas} options.canvas The canvas that is used for sending viewer properties to network links.
     * @param {DefaultProxy} [options.proxy] A proxy to be used for loading external data.
     * @param {String} [options.sourceUri] Overrides the url to use for resolving relative links and other KML network features.
     * @param {Boolean} [options.clampToGround=false] true if we want the geometry features (Polygons, LineStrings and LinearRings) clamped to the ground. If true, lines will use corridors so use Entity.corridor instead of Entity.polyline.
     *
     * @returns {Promise.<KmlDataSource>} A promise that will resolve to a new KmlDataSource instance once the KML is loaded.
     */
    KmlDataSource.load = function(data, options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);
        var dataSource = new KmlDataSource(options);
        return dataSource.load(data, options);
    };

    defineProperties(KmlDataSource.prototype, {
        /**
         * Gets a human-readable name for this instance.
         * This will be automatically be set to the KML document name on load.
         * @memberof KmlDataSource.prototype
         * @type {String}
         */
        name : {
            get : function() {
                return this._name;
            }
        },
        /**
         * Gets the clock settings defined by the loaded KML. This represents the total
         * availability interval for all time-dynamic data. If the KML does not contain
         * time-dynamic data, this value is undefined.
         * @memberof KmlDataSource.prototype
         * @type {DataSourceClock}
         */
        clock : {
            get : function() {
                return this._clock;
            }
        },
        /**
         * Gets the collection of {@link Entity} instances.
         * @memberof KmlDataSource.prototype
         * @type {EntityCollection}
         */
        entities : {
            get : function() {
                return this._entityCollection;
            }
        },
        /**
         * Gets a value indicating if the data source is currently loading data.
         * @memberof KmlDataSource.prototype
         * @type {Boolean}
         */
        isLoading : {
            get : function() {
                return this._isLoading;
            }
        },
        /**
         * Gets an event that will be raised when the underlying data changes.
         * @memberof KmlDataSource.prototype
         * @type {Event}
         */
        changedEvent : {
            get : function() {
                return this._changed;
            }
        },
        /**
         * Gets an event that will be raised if an error is encountered during processing.
         * @memberof KmlDataSource.prototype
         * @type {Event}
         */
        errorEvent : {
            get : function() {
                return this._error;
            }
        },
        /**
         * Gets an event that will be raised when the data source either starts or stops loading.
         * @memberof KmlDataSource.prototype
         * @type {Event}
         */
        loadingEvent : {
            get : function() {
                return this._loading;
            }
        },
        /**
         * Gets an event that will be raised when the data source refreshes a network link.
         * @memberof KmlDataSource.prototype
         * @type {Event}
         */
        refreshEvent : {
            get : function() {
                return this._refresh;
            }
        },
        /**
         * Gets an event that will be raised when the data source finds an unsupported node type.
         * @memberof KmlDataSource.prototype
         * @type {Event}
         */
        unsupportedNodeEvent : {
            get : function() {
                return this._unsupportedNode;
            }
        },
        /**
         * Gets whether or not this data source should be displayed.
         * @memberof KmlDataSource.prototype
         * @type {Boolean}
         */
        show : {
            get : function() {
                return this._entityCollection.show;
            },
            set : function(value) {
                this._entityCollection.show = value;
            }
        },

        /**
         * Gets or sets the clustering options for this data source. This object can be shared between multiple data sources.
         *
         * @memberof KmlDataSource.prototype
         * @type {EntityCluster}
         */
        clustering : {
            get : function() {
                return this._entityCluster;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value must be defined.');
                }
                //>>includeEnd('debug');
                this._entityCluster = value;
            }
        }
    });

    /**
     * Asynchronously loads the provided KML data, replacing any existing data.
     *
     * @param {String|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document.
     * @param {Object} [options] An object with the following properties:
     * @param {Number} [options.sourceUri] Overrides the url to use for resolving relative links and other KML network features.
     * @returns {Promise.<KmlDataSource>} A promise that will resolve to this instances once the KML is loaded.
     * @param {Boolean} [options.clampToGround=false] true if we want the geometry features (Polygons, LineStrings and LinearRings) clamped to the ground. If true, lines will use corridors so use Entity.corridor instead of Entity.polyline.
     */
    KmlDataSource.prototype.load = function(data, options) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(data)) {
            throw new DeveloperError('data is required.');
        }
        //>>includeEnd('debug');

        options = defaultValue(options, {});
        DataSource.setLoading(this, true);

        var oldName = this._name;
        this._name = undefined;
        this._clampToGround = defaultValue(options.clampToGround, false);

        var that = this;
        return load(this, this._entityCollection, data, options).then(function() {
            var clock;

            var availability = that._entityCollection.computeAvailability();

            var start = availability.start;
            var stop = availability.stop;
            var isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
            var isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
            if (!isMinStart || !isMaxStop) {
                var date;

                //If start is min time just start at midnight this morning, local time
                if (isMinStart) {
                    date = new Date();
                    date.setHours(0, 0, 0, 0);
                    start = JulianDate.fromDate(date);
                }

                //If stop is max value just stop at midnight tonight, local time
                if (isMaxStop) {
                    date = new Date();
                    date.setHours(24, 0, 0, 0);
                    stop = JulianDate.fromDate(date);
                }

                clock = new DataSourceClock();
                clock.startTime = start;
                clock.stopTime = stop;
                clock.currentTime = JulianDate.clone(start);
                clock.clockRange = ClockRange.LOOP_STOP;
                clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
                clock.multiplier = Math.round(Math.min(Math.max(JulianDate.secondsDifference(stop, start) / 60, 1), 3.15569e7));
            }

            var changed = false;
            if (clock !== that._clock) {
                that._clock = clock;
                changed = true;
            }

            if (oldName !== that._name) {
                changed = true;
            }

            if (changed) {
                that._changed.raiseEvent(that);
            }

            DataSource.setLoading(that, false);

            return that;
        }).otherwise(function(error) {
            DataSource.setLoading(that, false);
            that._error.raiseEvent(that, error);
            console.log(error);
            return when.reject(error);
        });
    };

    function mergeAvailabilityWithParent(child) {
        var parent = child.parent;
        if (defined(parent)) {
            var parentAvailability = parent.availability;
            if (defined(parentAvailability)) {
                var childAvailability = child.availability;
                if (defined(childAvailability)) {
                    childAvailability.intersect(parentAvailability);
                } else {
                    child.availability = parentAvailability;
                }
            }
        }
    }

    function getNetworkLinkUpdateCallback(dataSource, networkLink, newEntityCollection, networkLinks, processedHref) {
        return function(rootElement) {
            if (!networkLinks.contains(networkLink.id)) {
                // Got into the odd case where a parent network link was updated while a child
                //  network link update was in flight, so just throw it away.
                return;
            }
            var remove = false;
            var networkLinkControl = queryFirstNode(rootElement, 'NetworkLinkControl', namespaces.kml);
            var hasNetworkLinkControl = defined(networkLinkControl);

            var minRefreshPeriod = 0;
            if (hasNetworkLinkControl) {
                if (defined(queryFirstNode(networkLinkControl, 'Update', namespaces.kml))) {
                    console.log('KML - NetworkLinkControl updates aren\'t supported.');
                    networkLink.updating = false;
                    networkLinks.remove(networkLink.id);
                    return;
                }
                networkLink.cookie = defaultValue(queryStringValue(networkLinkControl, 'cookie', namespaces.kml), '');
                minRefreshPeriod = defaultValue(queryNumericValue(networkLinkControl, 'minRefreshPeriod', namespaces.kml), 0);
            }

            var now = JulianDate.now();
            var refreshMode = networkLink.refreshMode;
            if (refreshMode === RefreshMode.INTERVAL) {
                if (defined(networkLinkControl)) {
                    networkLink.time = Math.max(minRefreshPeriod, networkLink.time);
                }
            } else if (refreshMode === RefreshMode.EXPIRE) {
                var expires;
                if (defined(networkLinkControl)) {
                    expires = queryStringValue(networkLinkControl, 'expires', namespaces.kml);
                }
                if (defined(expires)) {
                    try {
                        var date = JulianDate.fromIso8601(expires);
                        var diff = JulianDate.secondsDifference(date, now);
                        if (diff > 0 && diff < minRefreshPeriod) {
                            JulianDate.addSeconds(now, minRefreshPeriod, date);
                        }
                        networkLink.time = date;
                    } catch (e) {
                        console.log('KML - NetworkLinkControl expires is not a valid date');
                        remove = true;
                    }
                } else {
                    console.log('KML - refreshMode of onExpire requires the NetworkLinkControl to have an expires element');
                    remove = true;
                }
            }

            var networkLinkEntity = networkLink.entity;
            var entityCollection = dataSource._entityCollection;
            var newEntities = newEntityCollection.values;

            function removeChildren(entity) {
                entityCollection.remove(entity);
                var children = entity._children;
                var count = children.length;
                for(var i=0;i<count;++i) {
                    removeChildren(children[i]);
                }
            }

            // Remove old entities
            entityCollection.suspendEvents();
            var entitiesCopy = entityCollection.values.slice();
            for (var i=0;i<entitiesCopy.length;++i) {
                var entityToRemove = entitiesCopy[i];
                if (entityToRemove.parent === networkLinkEntity) {
                    entityToRemove.parent = undefined;
                    removeChildren(entityToRemove);
                }
            }
            entityCollection.resumeEvents();

            // Add new entities
            entityCollection.suspendEvents();
            for (i = 0; i < newEntities.length; i++) {
                var newEntity = newEntities[i];
                if (!defined(newEntity.parent)) {
                    newEntity.parent = networkLinkEntity;
                    mergeAvailabilityWithParent(newEntity);
                }
                entityCollection.add(newEntity);
            }
            entityCollection.resumeEvents();

            // No refresh information remove it, otherwise update lastUpdate time
            if (remove) {
                networkLinks.remove(networkLink.id);
            } else {
                networkLink.lastUpdated = now;
            }

            var availability = entityCollection.computeAvailability();

            var start = availability.start;
            var stop = availability.stop;
            var isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE);
            var isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE);
            if (!isMinStart || !isMaxStop) {
                var clock = dataSource._clock;

                if (clock.startTime !== start ||  clock.stopTime !== stop) {
                    clock.startTime = start;
                    clock.stopTime = stop;
                    dataSource._changed.raiseEvent(dataSource);
                }
            }

            networkLink.updating = false;
            networkLink.needsUpdate = false;
            dataSource._refresh.raiseEvent(dataSource, processedHref);
        };
    }

    var entitiesToIgnore = new AssociativeArray();

    /**
     * Updates any NetworkLink that require updating
     * @function
     *
     * @param {JulianDate} time The simulation time.
     * @returns {Boolean} True if this data source is ready to be displayed at the provided time, false otherwise.
     */
    KmlDataSource.prototype.update = function(time) {
        var networkLinks = this._networkLinks;
        if (networkLinks.length === 0) {
            return true;
        }

        var now = JulianDate.now();
        var that = this;

        entitiesToIgnore.removeAll();
        function recurseIgnoreEntities(entity) {
            var children = entity._children;
            var count = children.length;
            for (var i=0;i<count;++i) {
                var child = children[i];
                entitiesToIgnore.set(child.id, child);
                recurseIgnoreEntities(child);
            }
        }

        var cameraViewUpdate = false;
        var lastCameraView = this._lastCameraView;
        var camera = this._camera;
        if (defined(camera) &&
            !(camera.positionWC.equalsEpsilon(lastCameraView.position, CesiumMath.EPSILON7) &&
              camera.directionWC.equalsEpsilon(lastCameraView.direction, CesiumMath.EPSILON7) &&
              camera.upWC.equalsEpsilon(lastCameraView.up, CesiumMath.EPSILON7))) {

            // Camera has changed so update the last view
            lastCameraView.position = Cartesian3.clone(camera.positionWC);
            lastCameraView.direction = Cartesian3.clone(camera.directionWC);
            lastCameraView.up = Cartesian3.clone(camera.upWC);
            lastCameraView.bbox = camera.computeViewRectangle();
            cameraViewUpdate = true;
        }

        var newNetworkLinks = new AssociativeArray();
        var changed = false;
        networkLinks.values.forEach(function(networkLink) {
            var entity = networkLink.entity;
            if (entitiesToIgnore.contains(entity.id)) {
                return;
            }

            if (!networkLink.updating) {
                var doUpdate = false;
                if (networkLink.refreshMode === RefreshMode.INTERVAL) {
                    if (JulianDate.secondsDifference(now, networkLink.lastUpdated) > networkLink.time) {
                        doUpdate = true;
                    }
                }
                else if (networkLink.refreshMode === RefreshMode.EXPIRE) {
                    if (JulianDate.greaterThan(now, networkLink.time)) {
                        doUpdate = true;
                    }

                } else if (networkLink.refreshMode === RefreshMode.STOP) {
                    if (cameraViewUpdate) {
                        networkLink.needsUpdate = true;
                        networkLink.cameraUpdateTime = now;
                    }

                    if (networkLink.needsUpdate && JulianDate.secondsDifference(now, networkLink.cameraUpdateTime) >= networkLink.time) {
                        doUpdate = true;
                    }
                }

                if (doUpdate) {
                    recurseIgnoreEntities(entity);
                    networkLink.updating = true;
                    var newEntityCollection = new EntityCollection();
                    var href = joinUrls(networkLink.href, makeQueryString(networkLink.cookie, networkLink.queryString), false);
                    href = processNetworkLinkQueryString(that._camera, that._canvas, href, networkLink.viewBoundScale, lastCameraView.bbox);
                    load(that, newEntityCollection, href, {context: entity.id})
                        .then(getNetworkLinkUpdateCallback(that, networkLink, newEntityCollection, newNetworkLinks, href))
                        .otherwise(function(error) {
                            var msg = 'NetworkLink ' + networkLink.href + ' refresh failed: ' + error;
                            console.log(msg);
                            that._error.raiseEvent(that, msg);
                        });
                    changed = true;
                }
            }
            newNetworkLinks.set(networkLink.id, networkLink);
        });

        if (changed) {
            this._networkLinks = newNetworkLinks;
            this._changed.raiseEvent(this);
        }

        return true;
    };

    /**
     * Contains KML Feature data loaded into the <code>Entity.kml</code> property by {@link KmlDataSource}.
     * @alias KmlFeatureData
     * @constructor
     */
    function KmlFeatureData() {
        /**
         * Gets the atom syndication format author field.
         * @type Object
         */
        this.author = {
            /**
             * Gets the name.
             * @type String
             * @alias author.name
             * @memberof! KmlFeatureData#
             * @property author.name
             */
            name : undefined,
            /**
             * Gets the URI.
             * @type String
             * @alias author.uri
             * @memberof! KmlFeatureData#
             * @property author.uri
             */
            uri : undefined,
            /**
             * Gets the email.
             * @type String
             * @alias author.email
             * @memberof! KmlFeatureData#
             * @property author.email
             */
            email : undefined
        };

        /**
         * Gets the link.
         * @type Object
         */
        this.link = {
            /**
             * Gets the href.
             * @type String
             * @alias link.href
             * @memberof! KmlFeatureData#
             * @property link.href
             */
            href : undefined,
            /**
             * Gets the language of the linked resource.
             * @type String
             * @alias link.hreflang
             * @memberof! KmlFeatureData#
             * @property link.hreflang
             */
            hreflang : undefined,
            /**
             * Gets the link relation.
             * @type String
             * @alias link.rel
             * @memberof! KmlFeatureData#
             * @property link.rel
             */
            rel : undefined,
            /**
             * Gets the link type.
             * @type String
             * @alias link.type
             * @memberof! KmlFeatureData#
             * @property link.type
             */
            type : undefined,
            /**
             * Gets the link title.
             * @type String
             * @alias link.title
             * @memberof! KmlFeatureData#
             * @property link.title
             */
            title : undefined,
            /**
             * Gets the link length.
             * @type String
             * @alias link.length
             * @memberof! KmlFeatureData#
             * @property link.length
             */
            length : undefined
        };

        /**
         * Gets the unstructured address field.
         * @type String
         */
        this.address = undefined;
        /**
         * Gets the phone number.
         * @type String
         */
        this.phoneNumber = undefined;
        /**
         * Gets the snippet.
         * @type String
         */
        this.snippet = undefined;
        /**
         * Gets the extended data, parsed into a JSON object.
         * Currently only the <code>Data</code> property is supported.
         * <code>SchemaData</code> and custom data are ignored.
         * @type String
         */
        this.extendedData = undefined;
    }

    return KmlDataSource;
});
