UNPKG

cesium

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,237 lines (1,102 loc) 136 kB
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/getExtensionFromUri', '../Core/getFilenameFromUri', '../Core/HeadingPitchRange', '../Core/HeadingPitchRoll', '../Core/Iso8601', '../Core/JulianDate', '../Core/Math', '../Core/NearFarScalar', '../Core/objectToQuery', '../Core/oneTimeWarning', '../Core/PinBuilder', '../Core/PolygonHierarchy', '../Core/queryToObject', '../Core/Rectangle', '../Core/Resource', '../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', './KmlCamera', './KmlLookAt', './KmlTour', './KmlTourFlyTo', './KmlTourWait', './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, getExtensionFromUri, getFilenameFromUri, HeadingPitchRange, HeadingPitchRoll, Iso8601, JulianDate, CesiumMath, NearFarScalar, objectToQuery, oneTimeWarning, PinBuilder, PolygonHierarchy, queryToObject, Rectangle, Resource, RuntimeError, TimeInterval, TimeIntervalCollection, HeightReference, HorizontalOrigin, LabelStyle, SceneMode, Autolinker, Uri, when, zip, BillboardGraphics, CompositePositionProperty, CorridorGraphics, DataSource, DataSourceClock, Entity, EntityCluster, EntityCollection, KmlCamera, KmlLookAt, KmlTour, KmlTourFlyTo, KmlTourWait, 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 insertNamespaces(text) { var namespaceMap = { xsi : 'http://www.w3.org/2001/XMLSchema-instance' }; var firstPart, lastPart, reg, declaration; for (var key in namespaceMap) { if (namespaceMap.hasOwnProperty(key)) { reg = RegExp('[< ]' + key + ':'); declaration = 'xmlns:' + key + '='; if (reg.test(text) && text.indexOf(declaration) === -1) { if (!defined(firstPart)) { firstPart = text.substr(0, text.indexOf('<kml') + 4); lastPart = text.substr(firstPart.length); } firstPart += ' ' + declaration + '"' + namespaceMap[key] + '"'; } } } if (defined(firstPart)) { text = firstPart + lastPart; } return text; } function removeDuplicateNamespaces(text) { var index = text.indexOf('xmlns:'); var endDeclaration = text.indexOf('>', index); var namespace, startIndex, endIndex; while ((index !== -1) && (index < endDeclaration)) { namespace = text.slice(index, text.indexOf('\"', index)); startIndex = index; index = text.indexOf(namespace, index + 1); if (index !== -1) { endIndex = text.indexOf('\"', (text.indexOf('\"', index) + 1)); text = text.slice(0, index -1) + text.slice(endIndex + 1, text.length); index = text.indexOf('xmlns:', startIndex - 1); } else { index = text.indexOf('xmlns:', startIndex + 1); } } return text; } function loadXmlFromZip(entry, uriResolver, deferred) { entry.getData(new zip.TextWriter(), function(text) { text = insertNamespaces(text); text = removeDuplicateNamespaces(text); uriResolver.kml = parser.parseFromString(text, 'application/xml'); deferred.resolve(); }); } function loadDataUriFromZip(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, sourceResource) { var elements = div.querySelectorAll(elementType); for (var i = 0; i < elements.length; i++) { var element = elements[i]; var value = element.getAttribute(attributeName); var resource = resolveHref(value, sourceResource); element.setAttribute(attributeName, resource.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, ellipsoid) { //Google Earth treats empty or missing coordinates as 0. if (!defined(value)) { return Cartesian3.fromDegrees(0, 0, 0, ellipsoid); } var digits = value.match(/[^\s,\n]+/g); if (!defined(digits)) { return Cartesian3.fromDegrees(0, 0, 0, ellipsoid); } 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, ellipsoid); } function readCoordinates(element, ellipsoid) { 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], ellipsoid); } 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, sourceResource, uriResolver) { if (!defined(href)) { return undefined; } var resource; if (defined(uriResolver)) { var blob = uriResolver[href]; if (defined(blob)) { resource = new Resource({ url: blob }); } else { // Needed for multiple levels of KML files in a KMZ var baseUri = new Uri(sourceResource.getUrlComponent()); var uri = new Uri(href); blob = uriResolver[uri.resolve(baseUri)]; if (defined(blob)) { resource = new Resource({ url: blob }); } } } if (!defined(resource)) { resource = sourceResource.getDerivedResource({ url: href }); } return resource; } 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, sourceResource, 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'; } var hrefResource = resolveHref(href, sourceResource, uriResolver); if (canRefresh) { var refreshMode = queryStringValue(iconNode, 'refreshMode', namespaces.kml); var viewRefreshMode = queryStringValue(iconNode, 'viewRefreshMode', namespaces.kml); if (refreshMode === 'onInterval' || refreshMode === 'onExpire') { oneTimeWarning('kml-refreshMode-' + refreshMode, 'KML - Unsupported Icon refreshMode: ' + refreshMode); } else if (viewRefreshMode === 'onStop' || viewRefreshMode === 'onRegion') { oneTimeWarning('kml-refreshMode-' + viewRefreshMode, '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); if (defined(viewFormat)) { hrefResource.setQueryParameters(queryToObject(cleanupString(viewFormat))); } if (defined(httpQuery)) { hrefResource.setQueryParameters(queryToObject(cleanupString(httpQuery))); } var ellipsoid = dataSource._ellipsoid; processNetworkLinkQueryString(hrefResource, dataSource._camera, dataSource._canvas, viewBoundScale, dataSource._lastCameraView.bbox, ellipsoid); return hrefResource; } return hrefResource; } function processBillboardIcon(dataSource, node, targetEntity, sourceResource, 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, sourceResource, uriResolver, false); // If icon tags are present but blank, we do not want to show an icon if (defined(iconNode) && !defined(icon)) { icon = 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, sourceResource, 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, sourceResource, 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))) { oneTimeWarning('kml-gx:outerColor', 'KML - gx:outerColor is not supported in a LineStyle'); } if (defined(queryNumericValue(node, 'outerWidth', namespaces.gx))) { oneTimeWarning('kml-gx:outerWidth', 'KML - gx:outerWidth is not supported in a LineStyle'); } if (defined(queryNumericValue(node, 'physicalWidth', namespaces.gx))) { oneTimeWarning('kml-gx:physicalWidth', 'KML - gx:physicalWidth is not supported in a LineStyle'); } if (defined(queryBooleanValue(node, 'labelVisibility', namespaces.gx))) { oneTimeWarning('kml-gx:labelVisibility', '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') { oneTimeWarning('kml-listStyle-' + listItemType, 'KML - Unsupported ListStyle with listItemType: ' + listItemType); } } } } //Processes and merges any inline styles for the provided node into the provided entity. function computeFinalStyle(dataSource, placeMark, styleCollection, sourceResource, 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, sourceResource, 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, sourceResource, uriResolver); } } else { oneTimeWarning('kml-styleMap-' + key, '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]; var resource = sourceResource.getDerivedResource({ url: uri }); id = resource.getUrlComponent() + '#' + 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, resource, styleCollection) { return resource.fetchXML().then(function(styleKml) { return processStyles(dataSource, styleKml, styleCollection, resource, 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, sourceResource, 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(sourceResource)) { id = sourceResource.getUrlComponent() + id; } if (!defined(styleCollection.getById(id))) { styleEntity = new Entity({ id : id }); styleCollection.add(styleEntity); applyStyle(dataSource, node, styleEntity, sourceResource, 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(sourceResource)) { id = sourceResource.getUrlComponent() + 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(sourceResource)) { styleUrl = sourceResource.getUrlComponent() + styleUrl; } var base = styleCollection.getById(styleUrl); if (defined(base)) { styleEntity.merge(base); } } else { node = queryFirstNode(pair, 'Style', namespaces.kml); applyStyle(dataSource, node, styleEntity, sourceResource, uriResolver); } } } else { oneTimeWarning('kml-styleMap-' + key, 'KML - Unsupported StyleMap key: ' + key); } } } } } 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]; var resource = sourceResource.getDerivedResource({ url: uri }); promises.push(processExternalStyles(dataSource, resource, styleCollection)); } } } 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') { oneTimeWarning('kml-gx:altitudeMode-clampToSeaFloor', 'KML - <gx:altitudeMode>:clampToSeaFloor is currently not supported, using <kml:altitudeMode>:clampToGround.'); return HeightReference.CLAMP_TO_GROUND; } if (gxAltitudeMode === 'relativeToSeaFloor') { oneTimeWarning('kml-gx:altitudeMode-relativeToSeaFloor', 'KML - <gx:altitudeMode>:relativeToSeaFloor is currently not supported, using <kml:altitudeMode>:relativeToGround.'); return HeightReference.RELATIVE_TO_GROUND; } if (defined(altitudeMode)) { oneTimeWarning('kml-altitudeMode-unknown', 'KML - Unknown <kml:altitudeMode>:' + altitudeMode + ', using <kml:altitudeMode>:CLAMP_TO_GROUND.'); } else { oneTimeWarning('kml-gx:altitudeMode-unknown', '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')) { oneTimeWarning('kml-altitudeMode-unknown', 'KML - Unknown altitudeMode: ' + defaultValue(altitudeMode, gxAltitudeMode)); } // Clamp to ground is the default return new ScaledPositionProperty(property); } function createPositionPropertyArrayFromAltitudeMode(properties, altitudeMode, gxAltitudeMode, ellipsoid) { 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')) { oneTimeWarning('kml-altitudeMode-unknown', '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.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); // If there were empty <Icon> tags in the KML, then billboard.image was set to false above // However, in this case, the false value would have been converted to a property afterwards // Thus, we check if billboard.image is defined with value of false } else if (!billboard.image.getValue()) { billboard.image = undefined; } 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(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 ellipsoid = dataSource._ellipsoid; var position = readCoordinate(coordinatesString, ellipsoid); 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); var zIndex = queryNumericValue(geometryNode, 'drawOrder', namespaces.gx); var ellipsoid = dataSource._ellipsoid; var coordinates = readCoordinates(coordinatesNode, ellipsoid); var polyline =