cesium
Version:
CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.
1,128 lines (914 loc) • 56.1 kB
JavaScript
import Cartesian2 from '../Core/Cartesian2.js';
import Cartesian3 from '../Core/Cartesian3.js';
import Cartographic from '../Core/Cartographic.js';
import Color from '../Core/Color.js';
import createGuid from '../Core/createGuid.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 Iso8601 from '../Core/Iso8601.js';
import JulianDate from '../Core/JulianDate.js';
import CesiumMath from '../Core/Math.js';
import Rectangle from '../Core/Rectangle.js';
import ReferenceFrame from '../Core/ReferenceFrame.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 VerticalOrigin from '../Scene/VerticalOrigin.js';
import when from '../ThirdParty/when.js';
import zip from '../ThirdParty/zip.js';
import BillboardGraphics from './BillboardGraphics.js';
import CompositePositionProperty from './CompositePositionProperty.js';
import ModelGraphics from './ModelGraphics.js';
import RectangleGraphics from './RectangleGraphics.js';
import SampledPositionProperty from './SampledPositionProperty.js';
import SampledProperty from './SampledProperty.js';
import ScaledPositionProperty from './ScaledPositionProperty.js';
var BILLBOARD_SIZE = 32;
var kmlNamespace = 'http://www.opengis.net/kml/2.2';
var gxNamespace = 'http://www.google.com/kml/ext/2.2';
var xmlnsNamespace = 'http://www.w3.org/2000/xmlns/';
//
// Handles files external to the KML (eg. textures and models)
//
function ExternalFileHandler(modelCallback) {
this._files = {};
this._promises = [];
this._count = 0;
this._modelCallback = modelCallback;
}
var imageTypeRegex = /^data:image\/([^,;]+)/;
ExternalFileHandler.prototype.texture = function(texture) {
var that = this;
var filename;
if ((typeof texture === 'string') || (texture instanceof Resource)) {
texture = Resource.createIfNeeded(texture);
if (!texture.isDataUri) {
return texture.url;
}
// If its a data URI try and get the correct extension and then fetch the blob
var regexResult = texture.url.match(imageTypeRegex);
filename = 'texture_' + (++this._count);
if (defined(regexResult)) {
filename += '.' + regexResult[1];
}
var promise = texture.fetchBlob()
.then(function(blob) {
that._files[filename] = blob;
});
this._promises.push(promise);
return filename;
}
if (texture instanceof HTMLCanvasElement) {
var deferred = when.defer();
this._promises.push(deferred.promise);
filename = 'texture_' + (++this._count) + '.png';
texture.toBlob(function(blob) {
that._files[filename] = blob;
deferred.resolve();
});
return filename;
}
return '';
};
function getModelBlobHander(that, filename) {
return function (blob) {
that._files[filename] = blob;
};
}
ExternalFileHandler.prototype.model = function(model, time) {
var modelCallback = this._modelCallback;
if (!defined(modelCallback)) {
throw new RuntimeError('Encountered a model entity while exporting to KML, but no model callback was supplied.');
}
var externalFiles = {};
var url = modelCallback(model, time, externalFiles);
// Iterate through external files and add them to our list once the promise resolves
for (var filename in externalFiles) {
if(externalFiles.hasOwnProperty(filename)) {
var promise = when(externalFiles[filename]);
this._promises.push(promise);
promise.then(getModelBlobHander(this, filename));
}
}
return url;
};
Object.defineProperties(ExternalFileHandler.prototype, {
promise : {
get : function() {
return when.all(this._promises);
}
},
files : {
get : function() {
return this._files;
}
}
});
//
// Handles getting values from properties taking the desired time and default values into account
//
function ValueGetter(time) {
this._time = time;
}
ValueGetter.prototype.get = function(property, defaultVal, result) {
var value;
if (defined(property)) {
value = defined(property.getValue) ? property.getValue(this._time, result) : property;
}
return defaultValue(value, defaultVal);
};
ValueGetter.prototype.getColor = function(property, defaultVal) {
var result = this.get(property, defaultVal);
if (defined(result)) {
return colorToString(result);
}
};
ValueGetter.prototype.getMaterialType = function(property) {
if (!defined(property)) {
return;
}
return property.getType(this._time);
};
//
// Caches styles so we don't generate a ton of duplicate styles
//
function StyleCache() {
this._ids = {};
this._styles = {};
this._count = 0;
}
StyleCache.prototype.get = function(element) {
var ids = this._ids;
var key = element.innerHTML;
if (defined(ids[key])) {
return ids[key];
}
var styleId = 'style-' + (++this._count);
element.setAttribute('id', styleId);
// Store with #
styleId = '#' + styleId;
ids[key] = styleId;
this._styles[key] = element;
return styleId;
};
StyleCache.prototype.save = function(parentElement) {
var styles = this._styles;
var firstElement = parentElement.childNodes[0];
for (var key in styles) {
if (styles.hasOwnProperty(key)) {
parentElement.insertBefore(styles[key], firstElement);
}
}
};
//
// Manages the generation of IDs because an entity may have geometry and a Folder for children
//
function IdManager() {
this._ids = {};
}
IdManager.prototype.get = function(id) {
if (!defined(id)) {
return this.get(createGuid());
}
var ids = this._ids;
if (!defined(ids[id])) {
ids[id] = 0;
return id;
}
return id.toString() + '-' + (++ids[id]);
};
/**
* Exports an EntityCollection as a KML document. Only Point, Billboard, Model, Path, Polygon, Polyline geometries
* will be exported. Note that there is not a 1 to 1 mapping of Entity properties to KML Feature properties. For
* example, entity properties that are time dynamic but cannot be dynamic in KML are exported with their values at
* options.time or the beginning of the EntityCollection's time interval if not specified. For time-dynamic properties
* that are supported in KML, we use the samples if it is a {@link SampledProperty} otherwise we sample the value using
* the options.sampleDuration. Point, Billboard, Model and Path geometries with time-dynamic positions will be exported
* as gx:Track Features. Not all Materials are representable in KML, so for more advanced Materials just the primary
* color is used. Canvas objects are exported as PNG images.
*
* @exports exportKml
*
* @param {Object} options An object with the following properties:
* @param {EntityCollection} options.entities The EntityCollection to export as KML.
* @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file.
* @param {exportKml~ModelCallback} [options.modelCallback] A callback that will be called with a {@link ModelGraphics} instance and should return the URI to use in the KML. Required if a model exists in the entity collection.
* @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML.
* @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability.
* @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML.
* @param {Boolean} [options.kmz=false] If true KML and external files will be compressed into a kmz file.
*
* @returns {Promise<Object>} A promise that resolved to an object containing the KML string and a dictionary of external file blobs, or a kmz file as a blob if options.kmz is true.
* @demo {@link https://sandcastle.cesium.com/index.html?src=Export%20KML.html|Cesium Sandcastle KML Export Demo}
* @example
* Cesium.exportKml({
* entities: entityCollection
* })
* .then(function(result) {
* // The XML string is in result.kml
*
* var externalFiles = result.externalFiles
* for(var file in externalFiles) {
* // file is the name of the file used in the KML document as the href
* // externalFiles[file] is a blob with the contents of the file
* }
* });
*
*/
function exportKml(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
var entities = options.entities;
var kmz = defaultValue(options.kmz, false);
//>>includeStart('debug', pragmas.debug);
if (!defined(entities)) {
throw new DeveloperError('entities is required.');
}
//>>includeEnd('debug');
// Get the state that is passed around during the recursion
// This is separated out for testing.
var state = exportKml._createState(options);
// Filter EntityCollection so we only have top level entities
var rootEntities = entities.values.filter(function(entity) {
return !defined(entity.parent);
});
// Add the <Document>
var kmlDoc = state.kmlDoc;
var kmlElement = kmlDoc.documentElement;
kmlElement.setAttributeNS(xmlnsNamespace, 'xmlns:gx', gxNamespace);
var kmlDocumentElement = kmlDoc.createElement('Document');
kmlElement.appendChild(kmlDocumentElement);
// Create the KML Hierarchy
recurseEntities(state, kmlDocumentElement, rootEntities);
// Write out the <Style> elements
state.styleCache.save(kmlDocumentElement);
// Once all the blobs have resolved return the KML string along with the blob collection
var externalFileHandler = state.externalFileHandler;
return externalFileHandler.promise
.then(function() {
var serializer = new XMLSerializer();
var kmlString = serializer.serializeToString(state.kmlDoc);
if (kmz) {
return createKmz(kmlString, externalFileHandler.files);
}
return {
kml: kmlString,
externalFiles: externalFileHandler.files
};
});
}
function createKmz(kmlString, externalFiles) {
var deferred = when.defer();
zip.createWriter(new zip.BlobWriter(), function(writer) {
// We need to only write one file at a time so the zip doesn't get corrupted
addKmlToZip(writer, kmlString)
.then(function() {
var keys = Object.keys(externalFiles);
return addExternalFilesToZip(writer, keys, externalFiles, 0);
})
.then(function() {
writer.close(function(blob) {
deferred.resolve({
kmz: blob
});
});
});
});
return deferred.promise;
}
function addKmlToZip(writer, kmlString) {
var deferred = when.defer();
writer.add('doc.kml', new zip.TextReader(kmlString), function() {
deferred.resolve();
});
return deferred.promise;
}
function addExternalFilesToZip(writer, keys, externalFiles, index) {
if (keys.length === index) {
return;
}
var filename = keys[index];
var deferred = when.defer();
writer.add(filename, new zip.BlobReader(externalFiles[filename]), function() {
deferred.resolve();
});
return deferred.promise
.then(function() {
return addExternalFilesToZip(writer, keys, externalFiles, index+1);
});
}
exportKml._createState = function(options) {
var entities = options.entities;
var styleCache = new StyleCache();
// Use the start time as the default because just in case they define
// properties with an interval even if they don't change.
var entityAvailability = entities.computeAvailability();
var time = (defined(options.time) ? options.time : entityAvailability.start);
// Figure out how we will sample dynamic position properties
var defaultAvailability = defaultValue(options.defaultAvailability, entityAvailability);
var sampleDuration = defaultValue(options.sampleDuration, 60);
// Make sure we don't have infinite availability if we need to sample
if (defaultAvailability.start === Iso8601.MINIMUM_VALUE) {
if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) {
// Infinite, so just use the default
defaultAvailability = new TimeInterval();
} else {
// No start time, so just sample 10 times before the stop
JulianDate.addSeconds(defaultAvailability.stop, -10 * sampleDuration, defaultAvailability.start);
}
} else if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) {
// No stop time, so just sample 10 times after the start
JulianDate.addSeconds(defaultAvailability.start, 10 * sampleDuration, defaultAvailability.stop);
}
var externalFileHandler = new ExternalFileHandler(options.modelCallback);
var kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml');
return {
kmlDoc: kmlDoc,
ellipsoid: defaultValue(options.ellipsoid, Ellipsoid.WGS84),
idManager: new IdManager(),
styleCache: styleCache,
externalFileHandler: externalFileHandler,
time: time,
valueGetter: new ValueGetter(time),
sampleDuration: sampleDuration,
// Wrap it in a TimeIntervalCollection because that is what entity.availability is
defaultAvailability: new TimeIntervalCollection([defaultAvailability])
};
};
function recurseEntities(state, parentNode, entities) {
var kmlDoc = state.kmlDoc;
var styleCache = state.styleCache;
var valueGetter = state.valueGetter;
var idManager = state.idManager;
var count = entities.length;
var overlays;
var geometries;
var styles;
for (var i = 0; i < count; ++i) {
var entity = entities[i];
overlays = [];
geometries = [];
styles = [];
createPoint(state, entity, geometries, styles);
createLineString(state, entity.polyline, geometries, styles);
createPolygon(state, entity.rectangle, geometries, styles, overlays);
createPolygon(state, entity.polygon, geometries, styles, overlays);
createModel(state, entity, entity.model, geometries, styles);
var timeSpan;
var availability = entity.availability;
if (defined(availability)) {
timeSpan = kmlDoc.createElement('TimeSpan');
if (!JulianDate.equals(availability.start, Iso8601.MINIMUM_VALUE)) {
timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin',
JulianDate.toIso8601(availability.start)));
}
if (!JulianDate.equals(availability.stop, Iso8601.MAXIMUM_VALUE)) {
timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end',
JulianDate.toIso8601(availability.stop)));
}
}
for (var overlayIndex = 0; overlayIndex < overlays.length; ++overlayIndex) {
var overlay = overlays[overlayIndex];
overlay.setAttribute('id', idManager.get(entity.id));
overlay.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name));
overlay.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
overlay.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
if (defined(timeSpan)) {
overlay.appendChild(timeSpan);
}
parentNode.appendChild(overlay);
}
var geometryCount = geometries.length;
if (geometryCount > 0) {
var placemark = kmlDoc.createElement('Placemark');
placemark.setAttribute('id', idManager.get(entity.id));
var name = entity.name;
var labelGraphics = entity.label;
if (defined(labelGraphics)) {
var labelStyle = kmlDoc.createElement('LabelStyle');
// KML only shows the name as a label, so just change the name if we need to show a label
var text = valueGetter.get(labelGraphics.text);
name = (defined(text) && text.length > 0) ? text : name;
var color = valueGetter.getColor(labelGraphics.fillColor);
if (defined(color)) {
labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
}
var scale = valueGetter.get(labelGraphics.scale);
if (defined(scale)) {
labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale));
}
styles.push(labelStyle);
}
placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', name));
placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
if (defined(timeSpan)) {
placemark.appendChild(timeSpan);
}
parentNode.appendChild(placemark);
var styleCount = styles.length;
if (styleCount > 0) {
var style = kmlDoc.createElement('Style');
for (var styleIndex = 0; styleIndex < styleCount; ++styleIndex) {
style.appendChild(styles[styleIndex]);
}
placemark.appendChild(createBasicElementWithText(kmlDoc, 'styleUrl', styleCache.get(style)));
}
if (geometries.length === 1) {
placemark.appendChild(geometries[0]);
} else if (geometries.length > 1) {
var multigeometry = kmlDoc.createElement('MultiGeometry');
for (var geometryIndex = 0; geometryIndex < geometryCount; ++geometryIndex) {
multigeometry.appendChild(geometries[geometryIndex]);
}
placemark.appendChild(multigeometry);
}
}
var children = entity._children;
if (children.length > 0) {
var folderNode = kmlDoc.createElement('Folder');
folderNode.setAttribute('id', idManager.get(entity.id));
folderNode.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name));
folderNode.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
folderNode.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
parentNode.appendChild(folderNode);
recurseEntities(state, folderNode, children);
}
}
}
var scratchCartesian3 = new Cartesian3();
var scratchCartographic = new Cartographic();
var scratchJulianDate = new JulianDate();
function createPoint(state, entity, geometries, styles) {
var kmlDoc = state.kmlDoc;
var ellipsoid = state.ellipsoid;
var valueGetter = state.valueGetter;
var pointGraphics = defaultValue(entity.billboard, entity.point);
if (!defined(pointGraphics) && !defined(entity.path)) {
return;
}
// If the point isn't constant then create gx:Track or gx:MultiTrack
var entityPositionProperty = entity.position;
if (!entityPositionProperty.isConstant) {
createTracks(state, entity, pointGraphics, geometries, styles);
return;
}
valueGetter.get(entityPositionProperty, undefined, scratchCartesian3);
var coordinates = createBasicElementWithText(kmlDoc, 'coordinates',
getCoordinates(scratchCartesian3, ellipsoid));
var pointGeometry = kmlDoc.createElement('Point');
// Set altitude mode
var altitudeMode = kmlDoc.createElement('altitudeMode');
altitudeMode.appendChild(getAltitudeMode(state, pointGraphics.heightReference));
pointGeometry.appendChild(altitudeMode);
pointGeometry.appendChild(coordinates);
geometries.push(pointGeometry);
// Create style
var iconStyle = (pointGraphics instanceof BillboardGraphics) ?
createIconStyleFromBillboard(state, pointGraphics) : createIconStyleFromPoint(state, pointGraphics);
styles.push(iconStyle);
}
function createTracks(state, entity, pointGraphics, geometries, styles) {
var kmlDoc = state.kmlDoc;
var ellipsoid = state.ellipsoid;
var valueGetter = state.valueGetter;
var intervals;
var entityPositionProperty = entity.position;
var useEntityPositionProperty = true;
if (entityPositionProperty instanceof CompositePositionProperty) {
intervals = entityPositionProperty.intervals;
useEntityPositionProperty = false;
} else {
intervals = defaultValue(entity.availability, state.defaultAvailability);
}
var isModel = (pointGraphics instanceof ModelGraphics);
var i, j, times;
var tracks = [];
for (i = 0; i < intervals.length; ++i) {
var interval = intervals.get(i);
var positionProperty = useEntityPositionProperty ? entityPositionProperty : interval.data;
var trackAltitudeMode = kmlDoc.createElement('altitudeMode');
// This is something that KML importing uses to handle clampToGround,
// so just extract the internal property and set the altitudeMode.
if (positionProperty instanceof ScaledPositionProperty) {
positionProperty = positionProperty._value;
trackAltitudeMode.appendChild(getAltitudeMode(state, HeightReference.CLAMP_TO_GROUND));
} else if (defined(pointGraphics)) {
trackAltitudeMode.appendChild(getAltitudeMode(state, pointGraphics.heightReference));
} else {
// Path graphics only, which has no height reference
trackAltitudeMode.appendChild(getAltitudeMode(state, HeightReference.NONE));
}
var positionTimes = [];
var positionValues = [];
if (positionProperty.isConstant) {
valueGetter.get(positionProperty, undefined, scratchCartesian3);
var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates',
getCoordinates(scratchCartesian3, ellipsoid));
// This interval is constant so add a track with the same position
positionTimes.push(JulianDate.toIso8601(interval.start));
positionValues.push(constCoordinates);
positionTimes.push(JulianDate.toIso8601(interval.stop));
positionValues.push(constCoordinates);
} else if (positionProperty instanceof SampledPositionProperty) {
times = positionProperty._property._times;
for (j = 0; j < times.length; ++j) {
positionTimes.push(JulianDate.toIso8601(times[j]));
positionProperty.getValueInReferenceFrame(times[j], ReferenceFrame.FIXED, scratchCartesian3);
positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
}
} else if (positionProperty instanceof SampledProperty) {
times = positionProperty._times;
var values = positionProperty._values;
for (j = 0; j < times.length; ++j) {
positionTimes.push(JulianDate.toIso8601(times[j]));
Cartesian3.fromArray(values, j * 3, scratchCartesian3);
positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
}
} else {
var duration = state.sampleDuration;
interval.start.clone(scratchJulianDate);
if (!interval.isStartIncluded) {
JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate);
}
var stopDate = interval.stop;
while (JulianDate.lessThan(scratchJulianDate, stopDate)) {
positionProperty.getValue(scratchJulianDate, scratchCartesian3);
positionTimes.push(JulianDate.toIso8601(scratchJulianDate));
positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate);
}
if (interval.isStopIncluded && JulianDate.equals(scratchJulianDate, stopDate)) {
positionProperty.getValue(scratchJulianDate, scratchCartesian3);
positionTimes.push(JulianDate.toIso8601(scratchJulianDate));
positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
}
}
var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track');
trackGeometry.appendChild(trackAltitudeMode);
for (var k = 0; k < positionTimes.length; ++k) {
var when = createBasicElementWithText(kmlDoc, 'when', positionTimes[k]);
var coord = createBasicElementWithText(kmlDoc, 'coord', positionValues[k], gxNamespace);
trackGeometry.appendChild(when);
trackGeometry.appendChild(coord);
}
if (isModel) {
trackGeometry.appendChild(createModelGeometry(state, pointGraphics));
}
tracks.push(trackGeometry);
}
// If one track, then use it otherwise combine into a multitrack
if (tracks.length === 1) {
geometries.push(tracks[0]);
} else if (tracks.length > 1) {
var multiTrackGeometry = kmlDoc.createElementNS(gxNamespace, 'MultiTrack');
for (i = 0; i < tracks.length; ++i) {
multiTrackGeometry.appendChild(tracks[i]);
}
geometries.push(multiTrackGeometry);
}
// Create style
if (defined(pointGraphics) && !isModel) {
var iconStyle = (pointGraphics instanceof BillboardGraphics) ?
createIconStyleFromBillboard(state, pointGraphics) : createIconStyleFromPoint(state, pointGraphics);
styles.push(iconStyle);
}
// See if we have a line that needs to be drawn
var path = entity.path;
if (defined(path)) {
var width = valueGetter.get(path.width);
var material = path.material;
if (defined(material) || defined(width)) {
var lineStyle = kmlDoc.createElement('LineStyle');
if (defined(width)) {
lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width));
}
processMaterial(state, material, lineStyle);
styles.push(lineStyle);
}
}
}
function createIconStyleFromPoint(state, pointGraphics) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var iconStyle = kmlDoc.createElement('IconStyle');
var color = valueGetter.getColor(pointGraphics.color);
if (defined(color)) {
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
}
var pixelSize = valueGetter.get(pointGraphics.pixelSize);
if (defined(pixelSize)) {
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', pixelSize / BILLBOARD_SIZE));
}
return iconStyle;
}
function createIconStyleFromBillboard(state, billboardGraphics) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var externalFileHandler = state.externalFileHandler;
var iconStyle = kmlDoc.createElement('IconStyle');
var image = valueGetter.get(billboardGraphics.image);
if (defined(image)) {
image = externalFileHandler.texture(image);
var icon = kmlDoc.createElement('Icon');
icon.appendChild(createBasicElementWithText(kmlDoc, 'href', image));
var imageSubRegion = valueGetter.get(billboardGraphics.imageSubRegion);
if (defined(imageSubRegion)) {
icon.appendChild(createBasicElementWithText(kmlDoc, 'x', imageSubRegion.x, gxNamespace));
icon.appendChild(createBasicElementWithText(kmlDoc, 'y', imageSubRegion.y, gxNamespace));
icon.appendChild(createBasicElementWithText(kmlDoc, 'w', imageSubRegion.width, gxNamespace));
icon.appendChild(createBasicElementWithText(kmlDoc, 'h', imageSubRegion.height, gxNamespace));
}
iconStyle.appendChild(icon);
}
var color = valueGetter.getColor(billboardGraphics.color);
if (defined(color)) {
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
}
var scale = valueGetter.get(billboardGraphics.scale);
if (defined(scale)) {
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale));
}
var pixelOffset = valueGetter.get(billboardGraphics.pixelOffset);
if (defined(pixelOffset)) {
scale = defaultValue(scale, 1.0);
Cartesian2.divideByScalar(pixelOffset, scale, pixelOffset);
var width = valueGetter.get(billboardGraphics.width, BILLBOARD_SIZE);
var height = valueGetter.get(billboardGraphics.height, BILLBOARD_SIZE);
// KML Hotspots are from the bottom left, but we work from the top left
// Move to left
var horizontalOrigin = valueGetter.get(billboardGraphics.horizontalOrigin, HorizontalOrigin.CENTER);
if (horizontalOrigin === HorizontalOrigin.CENTER) {
pixelOffset.x -= width * 0.5;
} else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
pixelOffset.x -= width;
}
// Move to bottom
var verticalOrigin = valueGetter.get(billboardGraphics.verticalOrigin, VerticalOrigin.CENTER);
if (verticalOrigin === VerticalOrigin.TOP) {
pixelOffset.y += height;
} else if (verticalOrigin === VerticalOrigin.CENTER) {
pixelOffset.y += height * 0.5;
}
var hotSpot = kmlDoc.createElement('hotSpot');
hotSpot.setAttribute('x', -pixelOffset.x);
hotSpot.setAttribute('y', pixelOffset.y);
hotSpot.setAttribute('xunits', 'pixels');
hotSpot.setAttribute('yunits', 'pixels');
iconStyle.appendChild(hotSpot);
}
// We can only specify heading so if axis isn't Z, then we skip the rotation
// GE treats a heading of zero as no heading but can still point north using a 360 degree angle
var rotation = valueGetter.get(billboardGraphics.rotation);
var alignedAxis = valueGetter.get(billboardGraphics.alignedAxis);
if (defined(rotation) && Cartesian3.equals(Cartesian3.UNIT_Z, alignedAxis)) {
rotation = CesiumMath.toDegrees(-rotation);
if (rotation === 0) {
rotation = 360;
}
iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'heading', rotation));
}
return iconStyle;
}
function createLineString(state, polylineGraphics, geometries, styles) {
var kmlDoc = state.kmlDoc;
var ellipsoid = state.ellipsoid;
var valueGetter = state.valueGetter;
if (!defined(polylineGraphics)) {
return;
}
var lineStringGeometry = kmlDoc.createElement('LineString');
// Set altitude mode
var altitudeMode = kmlDoc.createElement('altitudeMode');
var clampToGround = valueGetter.get(polylineGraphics.clampToGround, false);
var altitudeModeText;
if (clampToGround) {
lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tessellate', true));
altitudeModeText = kmlDoc.createTextNode('clampToGround');
} else {
altitudeModeText = kmlDoc.createTextNode('absolute');
}
altitudeMode.appendChild(altitudeModeText);
lineStringGeometry.appendChild(altitudeMode);
// Set coordinates
var positionsProperty = polylineGraphics.positions;
var cartesians = valueGetter.get(positionsProperty);
var coordinates = createBasicElementWithText(kmlDoc, 'coordinates',
getCoordinates(cartesians, ellipsoid));
lineStringGeometry.appendChild(coordinates);
// Set draw order
var zIndex = valueGetter.get(polylineGraphics.zIndex);
if (clampToGround && defined(zIndex)) {
lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace));
}
geometries.push(lineStringGeometry);
// Create style
var lineStyle = kmlDoc.createElement('LineStyle');
var width = valueGetter.get(polylineGraphics.width);
if (defined(width)) {
lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width));
}
processMaterial(state, polylineGraphics.material, lineStyle);
styles.push(lineStyle);
}
function getRectangleBoundaries(state, rectangleGraphics, extrudedHeight) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var coordinates;
var height = valueGetter.get(rectangleGraphics.height, 0.0);
if (extrudedHeight > 0) {
// We extrude up and KML extrudes down, so if we extrude, set the polygon height to
// the extruded height so KML will look similar to Cesium
height = extrudedHeight;
}
var coordinatesProperty = rectangleGraphics.coordinates;
var rectangle = valueGetter.get(coordinatesProperty);
var coordinateStrings = [];
var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest];
for (var i = 0; i < 4; ++i) {
cornerFunction[i](rectangle, scratchCartographic);
coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' +
CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height);
}
coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' '));
var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs');
var linearRing = kmlDoc.createElement('LinearRing');
linearRing.appendChild(coordinates);
outerBoundaryIs.appendChild(linearRing);
return [outerBoundaryIs];
}
function getLinearRing(state, positions, height, perPositionHeight) {
var kmlDoc = state.kmlDoc;
var ellipsoid = state.ellipsoid;
var coordinateStrings = [];
var positionCount = positions.length;
for (var i = 0; i < positionCount; ++i) {
Cartographic.fromCartesian(positions[i], ellipsoid, scratchCartographic);
coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' +
CesiumMath.toDegrees(scratchCartographic.latitude) + ',' +
(perPositionHeight ? scratchCartographic.height : height));
}
var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' '));
var linearRing = kmlDoc.createElement('LinearRing');
linearRing.appendChild(coordinates);
return linearRing;
}
function getPolygonBoundaries(state, polygonGraphics, extrudedHeight) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var height = valueGetter.get(polygonGraphics.height, 0.0);
var perPositionHeight = valueGetter.get(polygonGraphics.perPositionHeight, false);
if (!perPositionHeight && (extrudedHeight > 0)) {
// We extrude up and KML extrudes down, so if we extrude, set the polygon height to
// the extruded height so KML will look similar to Cesium
height = extrudedHeight;
}
var boundaries = [];
var hierarchyProperty = polygonGraphics.hierarchy;
var hierarchy = valueGetter.get(hierarchyProperty);
// Polygon hierarchy can sometimes just be an array of positions
var positions = Array.isArray(hierarchy) ? hierarchy : hierarchy.positions;
// Polygon boundaries
var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs');
outerBoundaryIs.appendChild(getLinearRing(state, positions, height, perPositionHeight));
boundaries.push(outerBoundaryIs);
// Hole boundaries
var holes = hierarchy.holes;
if (defined(holes)) {
var holeCount = holes.length;
for (var i = 0; i < holeCount; ++i) {
var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs');
innerBoundaryIs.appendChild(getLinearRing(state, holes[i].positions, height, perPositionHeight));
boundaries.push(innerBoundaryIs);
}
}
return boundaries;
}
function createPolygon(state, geometry, geometries, styles, overlays) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
if (!defined(geometry)) {
return;
}
// Detect textured quads and use ground overlays instead
var isRectangle = (geometry instanceof RectangleGraphics);
if (isRectangle && valueGetter.getMaterialType(geometry.material) === 'Image') {
createGroundOverlay(state, geometry, overlays);
return;
}
var polygonGeometry = kmlDoc.createElement('Polygon');
var extrudedHeight = valueGetter.get(geometry.extrudedHeight, 0.0);
if (extrudedHeight > 0) {
polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'extrude', true));
}
// Set boundaries
var boundaries = isRectangle ? getRectangleBoundaries(state, geometry, extrudedHeight) :
getPolygonBoundaries(state, geometry, extrudedHeight);
var boundaryCount = boundaries.length;
for (var i = 0; i < boundaryCount; ++i) {
polygonGeometry.appendChild(boundaries[i]);
}
// Set altitude mode
var altitudeMode = kmlDoc.createElement('altitudeMode');
altitudeMode.appendChild(getAltitudeMode(state, geometry.heightReference));
polygonGeometry.appendChild(altitudeMode);
geometries.push(polygonGeometry);
// Create style
var polyStyle = kmlDoc.createElement('PolyStyle');
var fill = valueGetter.get(geometry.fill, false);
if (fill) {
polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'fill', fill));
}
processMaterial(state, geometry.material, polyStyle);
var outline = valueGetter.get(geometry.outline, false);
if (outline) {
polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'outline', outline));
// Outline uses LineStyle
var lineStyle = kmlDoc.createElement('LineStyle');
var outlineWidth = valueGetter.get(geometry.outlineWidth, 1.0);
lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', outlineWidth));
var outlineColor = valueGetter.getColor(geometry.outlineColor, Color.BLACK);
lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', outlineColor));
lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
styles.push(lineStyle);
}
styles.push(polyStyle);
}
function createGroundOverlay(state, rectangleGraphics, overlays) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var externalFileHandler = state.externalFileHandler;
var groundOverlay = kmlDoc.createElement('GroundOverlay');
// Set altitude mode
var altitudeMode = kmlDoc.createElement('altitudeMode');
altitudeMode.appendChild(getAltitudeMode(state, rectangleGraphics.heightReference));
groundOverlay.appendChild(altitudeMode);
var height = valueGetter.get(rectangleGraphics.height);
if (defined(height)) {
groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'altitude', height));
}
var rectangle = valueGetter.get(rectangleGraphics.coordinates);
var latLonBox = kmlDoc.createElement('LatLonBox');
latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'north', CesiumMath.toDegrees(rectangle.north)));
latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'south', CesiumMath.toDegrees(rectangle.south)));
latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'east', CesiumMath.toDegrees(rectangle.east)));
latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'west', CesiumMath.toDegrees(rectangle.west)));
groundOverlay.appendChild(latLonBox);
// We should only end up here if we have an ImageMaterialProperty
var material = valueGetter.get(rectangleGraphics.material);
var href = externalFileHandler.texture(material.image);
var icon = kmlDoc.createElement('Icon');
icon.appendChild(createBasicElementWithText(kmlDoc, 'href', href));
groundOverlay.appendChild(icon);
var color = material.color;
if (defined(color)) {
groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'color', colorToString(material.color)));
}
overlays.push(groundOverlay);
}
function createModelGeometry(state, modelGraphics) {
var kmlDoc = state.kmlDoc;
var valueGetter = state.valueGetter;
var externalFileHandler = state.externalFileHandler;
var modelGeometry = kmlDoc.createElement('Model');
var scale = valueGetter.get(modelGraphics.scale);
if (defined(scale)) {
var scaleElement = kmlDoc.createElement('scale');
scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'x', scale));
scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'y', scale));
scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'z', scale));
modelGeometry.appendChild(scaleElement);
}
var link = kmlDoc.createElement('Link');
var uri = externalFileHandler.model(modelGraphics, state.time);
link.appendChild(createBasicElementWithText(kmlDoc, 'href', uri));
modelGeometry.appendChild(link);
return modelGeometry;
}
function createModel(state, entity, modelGraphics, geometries, styles) {
var kmlDoc = state.kmlDoc;
var ellipsoid = state.ellipsoid;
var valueGetter = state.valueGetter;
if (!defined(modelGraphics)) {
return;
}
// If the point isn't constant then create gx:Track or gx:MultiTrack
var entityPositionProperty = entity.position;
if (!entityPositionProperty.isConstant) {
createTracks(state, entity, modelGraphics, geometries, styles);
return;
}
var modelGeometry = createModelGeometry(state, modelGraphics);
// Set altitude mode
var altitudeMode = kmlDoc.createElement('altitudeMode');
altitudeMode.appendChild(getAltitudeMode(state, modelGraphics.heightReference));
modelGeometry.appendChild(altitudeMode);
valueGetter.get(entityPositionProperty, undefined, scratchCartesian3);
Cartographic.fromCar