text: /*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 embedDataUris(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 applyBasePath(div, elementType, attributeName, proxy, sourceUri) {
          var elements = div.querySelectorAll(elementType);
          for (var i = 0; i < elements.length; i++) {
              var element = elements[i];
              var value = element.getAttribute(attributeName);
              var uri = resolveHref(value, proxy, sourceUri);
              element.setAttribute(attributeName, uri);
          }
      }
  
      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, proxy, sourceUri) {
          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) {
              embedDataUris(scratchDiv, 'a', 'href', uriResolver);
              embedDataUris(scratchDiv, 'img', 'src', uriResolver);
          }
  
          //Make relative urls absolute using the sourceUri
          applyBasePath(scratchDiv, 'a', 'href', proxy, sourceUri);
          applyBasePath(scratchDiv, 'img', 'src', proxy, sourceUri);
  
          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, dataSource._proxy, sourceUri);
  
          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;
  });
  