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
JavaScript
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;