UNPKG

cesium

Version:

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

1,243 lines (1,098 loc) 138 kB
import ArcType from '../Core/ArcType.js'; import AssociativeArray from '../Core/AssociativeArray.js'; import BoundingRectangle from '../Core/BoundingRectangle.js'; import Cartesian2 from '../Core/Cartesian2.js'; import Cartesian3 from '../Core/Cartesian3.js'; import Cartographic from '../Core/Cartographic.js'; import ClockRange from '../Core/ClockRange.js'; import ClockStep from '../Core/ClockStep.js'; import clone from '../Core/clone.js'; import Color from '../Core/Color.js'; import createGuid from '../Core/createGuid.js'; import Credit from '../Core/Credit.js'; import defaultValue from '../Core/defaultValue.js'; import defined from '../Core/defined.js'; import DeveloperError from '../Core/DeveloperError.js'; import Ellipsoid from '../Core/Ellipsoid.js'; import Event from '../Core/Event.js'; import getExtensionFromUri from '../Core/getExtensionFromUri.js'; import getFilenameFromUri from '../Core/getFilenameFromUri.js'; import getTimestamp from '../Core/getTimestamp.js'; import HeadingPitchRange from '../Core/HeadingPitchRange.js'; import HeadingPitchRoll from '../Core/HeadingPitchRoll.js'; import Iso8601 from '../Core/Iso8601.js'; import JulianDate from '../Core/JulianDate.js'; import CesiumMath from '../Core/Math.js'; import NearFarScalar from '../Core/NearFarScalar.js'; import objectToQuery from '../Core/objectToQuery.js'; import oneTimeWarning from '../Core/oneTimeWarning.js'; import PinBuilder from '../Core/PinBuilder.js'; import PolygonHierarchy from '../Core/PolygonHierarchy.js'; import queryToObject from '../Core/queryToObject.js'; import Rectangle from '../Core/Rectangle.js'; import Resource from '../Core/Resource.js'; import RuntimeError from '../Core/RuntimeError.js'; import TimeInterval from '../Core/TimeInterval.js'; import TimeIntervalCollection from '../Core/TimeIntervalCollection.js'; import HeightReference from '../Scene/HeightReference.js'; import HorizontalOrigin from '../Scene/HorizontalOrigin.js'; import LabelStyle from '../Scene/LabelStyle.js'; import SceneMode from '../Scene/SceneMode.js'; import Autolinker from '../ThirdParty/Autolinker.js'; import Uri from '../ThirdParty/Uri.js'; import when from '../ThirdParty/when.js'; import zip from '../ThirdParty/zip.js'; import BillboardGraphics from './BillboardGraphics.js'; import CompositePositionProperty from './CompositePositionProperty.js'; import DataSource from './DataSource.js'; import DataSourceClock from './DataSourceClock.js'; import Entity from './Entity.js'; import EntityCluster from './EntityCluster.js'; import EntityCollection from './EntityCollection.js'; import KmlCamera from './KmlCamera.js'; import KmlLookAt from './KmlLookAt.js'; import KmlTour from './KmlTour.js'; import KmlTourFlyTo from './KmlTourFlyTo.js'; import KmlTourWait from './KmlTourWait.js'; import LabelGraphics from './LabelGraphics.js'; import PathGraphics from './PathGraphics.js'; import PolygonGraphics from './PolygonGraphics.js'; import PolylineGraphics from './PolylineGraphics.js'; import PositionPropertyArray from './PositionPropertyArray.js'; import RectangleGraphics from './RectangleGraphics.js'; import ReferenceProperty from './ReferenceProperty.js'; import SampledPositionProperty from './SampledPositionProperty.js'; import ScaledPositionProperty from './ScaledPositionProperty.js'; import TimeIntervalCollectionProperty from './TimeIntervalCollectionProperty.js'; import WallGraphics from './WallGraphics.js'; //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; if (typeof DOMParser !== 'undefined') { parser = new DOMParser(); } var autolinker = new Autolinker({ stripPrefix : false, email : false, replaceFn : function(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; 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) }; // 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 : processTour }; function DeferredLoading(dataSource) { this._dataSource = dataSource; this._deferred = when.defer(); this._stack = []; this._promises = []; this._timeoutSet = false; this._used = false; this._started = 0; this._timeThreshold = 1000; // Initial load is 1 second } Object.defineProperties(DeferredLoading.prototype, { dataSource : { get : function() { return this._dataSource; } } }); DeferredLoading.prototype.addNodes = function(nodes, processingData) { this._stack.push({ nodes: nodes, index: 0, processingData: processingData }); this._used = true; }; DeferredLoading.prototype.addPromise = function(promise) { this._promises.push(promise); }; DeferredLoading.prototype.wait = function() { // Case where we had a non-document/folder as the root var deferred = this._deferred; if (!this._used) { deferred.resolve(); } return when.join(deferred.promise, when.all(this._promises)); }; DeferredLoading.prototype.process = function() { var isFirstCall = (this._stack.length === 1); if (isFirstCall) { this._started = KmlDataSource._getTimestamp(); } return this._process(isFirstCall); }; DeferredLoading.prototype._giveUpTime = function() { if (this._timeoutSet) { // Timeout was already set so just return return; } this._timeoutSet = true; this._timeThreshold = 50; // After the first load lower threshold to 0.5 seconds var that = this; setTimeout(function() { that._timeoutSet = false; that._started = KmlDataSource._getTimestamp(); that._process(true); }, 0); }; DeferredLoading.prototype._nextNode = function() { var stack = this._stack; var top = stack[stack.length-1]; var index = top.index; var nodes = top.nodes; if (index === nodes.length) { return; } ++top.index; return nodes[index]; }; DeferredLoading.prototype._pop = function() { var stack = this._stack; stack.pop(); // Return false if we are done if (stack.length === 0) { this._deferred.resolve(); return false; } return true; }; DeferredLoading.prototype._process = function(isFirstCall) { var dataSource = this.dataSource; var processingData = this._stack[this._stack.length-1].processingData; var child = this._nextNode(); while(defined(child)) { var featureProcessor = featureTypes[child.localName]; if(defined(featureProcessor) && ((namespaces.kml.indexOf(child.namespaceURI) !== -1) || (namespaces.gx.indexOf(child.namespaceURI) !== -1))) { featureProcessor(dataSource, child, processingData, this); // Give up time and continue loading later if (this._timeoutSet || (KmlDataSource._getTimestamp() > (this._started + this._timeThreshold))) { this._giveUpTime(); return; } } child = this._nextNode(); } // If we are a recursive call from a subfolder, just return so the parent folder can continue processing // If we aren't then make another call to processNodes because there is stuff still left in the queue if (this._pop() && isFirstCall) { this._process(true); } }; 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; } 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)) { // To resolve issues with KML sources defined in Windows style paths. href = href.replace(/\\/g, '/'); 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 = { maximumRed : undefined, red : undefined, maximumGreen : undefined, green : undefined, maximumBlue : undefined, blue : undefined }; 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; colorOptions.red = undefined; } else { colorOptions.maximumRed = undefined; colorOptions.red = 0; } if (green > 0) { colorOptions.maximumGreen = green; colorOptions.green = undefined; } else { colorOptions.maximumGreen = undefined; colorOptions.green = 0; } if (blue > 0) { colorOptions.maximumBlue = blue; colorOptions.blue = undefined; } else { colorOptions.maximumBlue = undefined; 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;