exportKml.js 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  1. import Cartesian2 from '../Core/Cartesian2.js';
  2. import Cartesian3 from '../Core/Cartesian3.js';
  3. import Cartographic from '../Core/Cartographic.js';
  4. import Color from '../Core/Color.js';
  5. import createGuid from '../Core/createGuid.js';
  6. import defaultValue from '../Core/defaultValue.js';
  7. import defined from '../Core/defined.js';
  8. import defineProperties from '../Core/defineProperties.js';
  9. import DeveloperError from '../Core/DeveloperError.js';
  10. import Ellipsoid from '../Core/Ellipsoid.js';
  11. import isArray from '../Core/isArray.js';
  12. import Iso8601 from '../Core/Iso8601.js';
  13. import JulianDate from '../Core/JulianDate.js';
  14. import CesiumMath from '../Core/Math.js';
  15. import Rectangle from '../Core/Rectangle.js';
  16. import ReferenceFrame from '../Core/ReferenceFrame.js';
  17. import Resource from '../Core/Resource.js';
  18. import RuntimeError from '../Core/RuntimeError.js';
  19. import TimeInterval from '../Core/TimeInterval.js';
  20. import TimeIntervalCollection from '../Core/TimeIntervalCollection.js';
  21. import HeightReference from '../Scene/HeightReference.js';
  22. import HorizontalOrigin from '../Scene/HorizontalOrigin.js';
  23. import VerticalOrigin from '../Scene/VerticalOrigin.js';
  24. import when from '../ThirdParty/when.js';
  25. import zip from '../ThirdParty/zip.js';
  26. import BillboardGraphics from './BillboardGraphics.js';
  27. import CompositePositionProperty from './CompositePositionProperty.js';
  28. import ModelGraphics from './ModelGraphics.js';
  29. import RectangleGraphics from './RectangleGraphics.js';
  30. import SampledPositionProperty from './SampledPositionProperty.js';
  31. import SampledProperty from './SampledProperty.js';
  32. import ScaledPositionProperty from './ScaledPositionProperty.js';
  33. var BILLBOARD_SIZE = 32;
  34. var kmlNamespace = 'http://www.opengis.net/kml/2.2';
  35. var gxNamespace = 'http://www.google.com/kml/ext/2.2';
  36. var xmlnsNamespace = 'http://www.w3.org/2000/xmlns/';
  37. //
  38. // Handles files external to the KML (eg. textures and models)
  39. //
  40. function ExternalFileHandler(modelCallback) {
  41. this._files = {};
  42. this._promises = [];
  43. this._count = 0;
  44. this._modelCallback = modelCallback;
  45. }
  46. var imageTypeRegex = /^data:image\/([^,;]+)/;
  47. ExternalFileHandler.prototype.texture = function(texture) {
  48. var that = this;
  49. var filename;
  50. if ((typeof texture === 'string') || (texture instanceof Resource)) {
  51. texture = Resource.createIfNeeded(texture);
  52. if (!texture.isDataUri) {
  53. return texture.url;
  54. }
  55. // If its a data URI try and get the correct extension and then fetch the blob
  56. var regexResult = texture.url.match(imageTypeRegex);
  57. filename = 'texture_' + (++this._count);
  58. if (defined(regexResult)) {
  59. filename += '.' + regexResult[1];
  60. }
  61. var promise = texture.fetchBlob()
  62. .then(function(blob) {
  63. that._files[filename] = blob;
  64. });
  65. this._promises.push(promise);
  66. return filename;
  67. }
  68. if (texture instanceof HTMLCanvasElement) {
  69. var deferred = when.defer();
  70. this._promises.push(deferred.promise);
  71. filename = 'texture_' + (++this._count) + '.png';
  72. texture.toBlob(function(blob) {
  73. that._files[filename] = blob;
  74. deferred.resolve();
  75. });
  76. return filename;
  77. }
  78. return '';
  79. };
  80. function getModelBlobHander(that, filename) {
  81. return function (blob) {
  82. that._files[filename] = blob;
  83. };
  84. }
  85. ExternalFileHandler.prototype.model = function(model, time) {
  86. var modelCallback = this._modelCallback;
  87. if (!defined(modelCallback)) {
  88. throw new RuntimeError('Encountered a model entity while exporting to KML, but no model callback was supplied.');
  89. }
  90. var externalFiles = {};
  91. var url = modelCallback(model, time, externalFiles);
  92. // Iterate through external files and add them to our list once the promise resolves
  93. for (var filename in externalFiles) {
  94. if(externalFiles.hasOwnProperty(filename)) {
  95. var promise = when(externalFiles[filename]);
  96. this._promises.push(promise);
  97. promise.then(getModelBlobHander(this, filename));
  98. }
  99. }
  100. return url;
  101. };
  102. defineProperties(ExternalFileHandler.prototype, {
  103. promise : {
  104. get : function() {
  105. return when.all(this._promises);
  106. }
  107. },
  108. files : {
  109. get : function() {
  110. return this._files;
  111. }
  112. }
  113. });
  114. //
  115. // Handles getting values from properties taking the desired time and default values into account
  116. //
  117. function ValueGetter(time) {
  118. this._time = time;
  119. }
  120. ValueGetter.prototype.get = function(property, defaultVal, result) {
  121. var value;
  122. if (defined(property)) {
  123. value = defined(property.getValue) ? property.getValue(this._time, result) : property;
  124. }
  125. return defaultValue(value, defaultVal);
  126. };
  127. ValueGetter.prototype.getColor = function(property, defaultVal) {
  128. var result = this.get(property, defaultVal);
  129. if (defined(result)) {
  130. return colorToString(result);
  131. }
  132. };
  133. ValueGetter.prototype.getMaterialType = function(property) {
  134. if (!defined(property)) {
  135. return;
  136. }
  137. return property.getType(this._time);
  138. };
  139. //
  140. // Caches styles so we don't generate a ton of duplicate styles
  141. //
  142. function StyleCache() {
  143. this._ids = {};
  144. this._styles = {};
  145. this._count = 0;
  146. }
  147. StyleCache.prototype.get = function(element) {
  148. var ids = this._ids;
  149. var key = element.innerHTML;
  150. if (defined(ids[key])) {
  151. return ids[key];
  152. }
  153. var styleId = 'style-' + (++this._count);
  154. element.setAttribute('id', styleId);
  155. // Store with #
  156. styleId = '#' + styleId;
  157. ids[key] = styleId;
  158. this._styles[key] = element;
  159. return styleId;
  160. };
  161. StyleCache.prototype.save = function(parentElement) {
  162. var styles = this._styles;
  163. var firstElement = parentElement.childNodes[0];
  164. for (var key in styles) {
  165. if (styles.hasOwnProperty(key)) {
  166. parentElement.insertBefore(styles[key], firstElement);
  167. }
  168. }
  169. };
  170. //
  171. // Manages the generation of IDs because an entity may have geometry and a Folder for children
  172. //
  173. function IdManager() {
  174. this._ids = {};
  175. }
  176. IdManager.prototype.get = function(id) {
  177. if (!defined(id)) {
  178. return this.get(createGuid());
  179. }
  180. var ids = this._ids;
  181. if (!defined(ids[id])) {
  182. ids[id] = 0;
  183. return id;
  184. }
  185. return id.toString() + '-' + (++ids[id]);
  186. };
  187. /**
  188. * Exports an EntityCollection as a KML document. Only Point, Billboard, Model, Path, Polygon, Polyline geometries
  189. * will be exported. Note that there is not a 1 to 1 mapping of Entity properties to KML Feature properties. For
  190. * example, entity properties that are time dynamic but cannot be dynamic in KML are exported with their values at
  191. * options.time or the beginning of the EntityCollection's time interval if not specified. For time-dynamic properties
  192. * that are supported in KML, we use the samples if it is a {@link SampledProperty} otherwise we sample the value using
  193. * the options.sampleDuration. Point, Billboard, Model and Path geometries with time-dynamic positions will be exported
  194. * as gx:Track Features. Not all Materials are representable in KML, so for more advanced Materials just the primary
  195. * color is used. Canvas objects are exported as PNG images.
  196. *
  197. * @exports exportKml
  198. *
  199. * @param {Object} options An object with the following properties:
  200. * @param {EntityCollection} options.entities The EntityCollection to export as KML.
  201. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file.
  202. * @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.
  203. * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML.
  204. * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability.
  205. * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML.
  206. * @param {Boolean} [options.kmz=false] If true KML and external files will be compressed into a kmz file.
  207. *
  208. * @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.
  209. * @demo {@link https://sandcastle.cesium.com/index.html?src=Export%20KML.html|Cesium Sandcastle KML Export Demo}
  210. * @example
  211. * Cesium.exportKml({
  212. * entities: entityCollection
  213. * })
  214. * .then(function(result) {
  215. * // The XML string is in result.kml
  216. *
  217. * var externalFiles = result.externalFiles
  218. * for(var file in externalFiles) {
  219. * // file is the name of the file used in the KML document as the href
  220. * // externalFiles[file] is a blob with the contents of the file
  221. * }
  222. * });
  223. *
  224. */
  225. function exportKml(options) {
  226. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  227. var entities = options.entities;
  228. var kmz = defaultValue(options.kmz, false);
  229. //>>includeStart('debug', pragmas.debug);
  230. if (!defined(entities)) {
  231. throw new DeveloperError('entities is required.');
  232. }
  233. //>>includeEnd('debug');
  234. // Get the state that is passed around during the recursion
  235. // This is separated out for testing.
  236. var state = exportKml._createState(options);
  237. // Filter EntityCollection so we only have top level entities
  238. var rootEntities = entities.values.filter(function(entity) {
  239. return !defined(entity.parent);
  240. });
  241. // Add the <Document>
  242. var kmlDoc = state.kmlDoc;
  243. var kmlElement = kmlDoc.documentElement;
  244. kmlElement.setAttributeNS(xmlnsNamespace, 'xmlns:gx', gxNamespace);
  245. var kmlDocumentElement = kmlDoc.createElement('Document');
  246. kmlElement.appendChild(kmlDocumentElement);
  247. // Create the KML Hierarchy
  248. recurseEntities(state, kmlDocumentElement, rootEntities);
  249. // Write out the <Style> elements
  250. state.styleCache.save(kmlDocumentElement);
  251. // Once all the blobs have resolved return the KML string along with the blob collection
  252. var externalFileHandler = state.externalFileHandler;
  253. return externalFileHandler.promise
  254. .then(function() {
  255. var serializer = new XMLSerializer();
  256. var kmlString = serializer.serializeToString(state.kmlDoc);
  257. if (kmz) {
  258. return createKmz(kmlString, externalFileHandler.files);
  259. }
  260. return {
  261. kml: kmlString,
  262. externalFiles: externalFileHandler.files
  263. };
  264. });
  265. }
  266. function createKmz(kmlString, externalFiles) {
  267. var deferred = when.defer();
  268. zip.createWriter(new zip.BlobWriter(), function(writer) {
  269. // We need to only write one file at a time so the zip doesn't get corrupted
  270. addKmlToZip(writer, kmlString)
  271. .then(function() {
  272. var keys = Object.keys(externalFiles);
  273. return addExternalFilesToZip(writer, keys, externalFiles, 0);
  274. })
  275. .then(function() {
  276. writer.close(function(blob) {
  277. deferred.resolve({
  278. kmz: blob
  279. });
  280. });
  281. });
  282. });
  283. return deferred.promise;
  284. }
  285. function addKmlToZip(writer, kmlString) {
  286. var deferred = when.defer();
  287. writer.add('doc.kml', new zip.TextReader(kmlString), function() {
  288. deferred.resolve();
  289. });
  290. return deferred.promise;
  291. }
  292. function addExternalFilesToZip(writer, keys, externalFiles, index) {
  293. if (keys.length === index) {
  294. return;
  295. }
  296. var filename = keys[index];
  297. var deferred = when.defer();
  298. writer.add(filename, new zip.BlobReader(externalFiles[filename]), function() {
  299. deferred.resolve();
  300. });
  301. return deferred.promise
  302. .then(function() {
  303. return addExternalFilesToZip(writer, keys, externalFiles, index+1);
  304. });
  305. }
  306. exportKml._createState = function(options) {
  307. var entities = options.entities;
  308. var styleCache = new StyleCache();
  309. // Use the start time as the default because just in case they define
  310. // properties with an interval even if they don't change.
  311. var entityAvailability = entities.computeAvailability();
  312. var time = (defined(options.time) ? options.time : entityAvailability.start);
  313. // Figure out how we will sample dynamic position properties
  314. var defaultAvailability = defaultValue(options.defaultAvailability, entityAvailability);
  315. var sampleDuration = defaultValue(options.sampleDuration, 60);
  316. // Make sure we don't have infinite availability if we need to sample
  317. if (defaultAvailability.start === Iso8601.MINIMUM_VALUE) {
  318. if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) {
  319. // Infinite, so just use the default
  320. defaultAvailability = new TimeInterval();
  321. } else {
  322. // No start time, so just sample 10 times before the stop
  323. JulianDate.addSeconds(defaultAvailability.stop, -10 * sampleDuration, defaultAvailability.start);
  324. }
  325. } else if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) {
  326. // No stop time, so just sample 10 times after the start
  327. JulianDate.addSeconds(defaultAvailability.start, 10 * sampleDuration, defaultAvailability.stop);
  328. }
  329. var externalFileHandler = new ExternalFileHandler(options.modelCallback);
  330. var kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml');
  331. return {
  332. kmlDoc: kmlDoc,
  333. ellipsoid: defaultValue(options.ellipsoid, Ellipsoid.WGS84),
  334. idManager: new IdManager(),
  335. styleCache: styleCache,
  336. externalFileHandler: externalFileHandler,
  337. time: time,
  338. valueGetter: new ValueGetter(time),
  339. sampleDuration: sampleDuration,
  340. // Wrap it in a TimeIntervalCollection because that is what entity.availability is
  341. defaultAvailability: new TimeIntervalCollection([defaultAvailability])
  342. };
  343. };
  344. function recurseEntities(state, parentNode, entities) {
  345. var kmlDoc = state.kmlDoc;
  346. var styleCache = state.styleCache;
  347. var valueGetter = state.valueGetter;
  348. var idManager = state.idManager;
  349. var count = entities.length;
  350. var overlays;
  351. var geometries;
  352. var styles;
  353. for (var i = 0; i < count; ++i) {
  354. var entity = entities[i];
  355. overlays = [];
  356. geometries = [];
  357. styles = [];
  358. createPoint(state, entity, geometries, styles);
  359. createLineString(state, entity.polyline, geometries, styles);
  360. createPolygon(state, entity.rectangle, geometries, styles, overlays);
  361. createPolygon(state, entity.polygon, geometries, styles, overlays);
  362. createModel(state, entity, entity.model, geometries, styles);
  363. var timeSpan;
  364. var availability = entity.availability;
  365. if (defined(availability)) {
  366. timeSpan = kmlDoc.createElement('TimeSpan');
  367. if (!JulianDate.equals(availability.start, Iso8601.MINIMUM_VALUE)) {
  368. timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin',
  369. JulianDate.toIso8601(availability.start)));
  370. }
  371. if (!JulianDate.equals(availability.stop, Iso8601.MAXIMUM_VALUE)) {
  372. timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end',
  373. JulianDate.toIso8601(availability.stop)));
  374. }
  375. }
  376. for (var overlayIndex = 0; overlayIndex < overlays.length; ++overlayIndex) {
  377. var overlay = overlays[overlayIndex];
  378. overlay.setAttribute('id', idManager.get(entity.id));
  379. overlay.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name));
  380. overlay.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
  381. overlay.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
  382. if (defined(timeSpan)) {
  383. overlay.appendChild(timeSpan);
  384. }
  385. parentNode.appendChild(overlay);
  386. }
  387. var geometryCount = geometries.length;
  388. if (geometryCount > 0) {
  389. var placemark = kmlDoc.createElement('Placemark');
  390. placemark.setAttribute('id', idManager.get(entity.id));
  391. var name = entity.name;
  392. var labelGraphics = entity.label;
  393. if (defined(labelGraphics)) {
  394. var labelStyle = kmlDoc.createElement('LabelStyle');
  395. // KML only shows the name as a label, so just change the name if we need to show a label
  396. var text = valueGetter.get(labelGraphics.text);
  397. name = (defined(text) && text.length > 0) ? text : name;
  398. var color = valueGetter.getColor(labelGraphics.fillColor);
  399. if (defined(color)) {
  400. labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
  401. labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
  402. }
  403. var scale = valueGetter.get(labelGraphics.scale);
  404. if (defined(scale)) {
  405. labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale));
  406. }
  407. styles.push(labelStyle);
  408. }
  409. placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', name));
  410. placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
  411. placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
  412. if (defined(timeSpan)) {
  413. placemark.appendChild(timeSpan);
  414. }
  415. parentNode.appendChild(placemark);
  416. var styleCount = styles.length;
  417. if (styleCount > 0) {
  418. var style = kmlDoc.createElement('Style');
  419. for (var styleIndex = 0; styleIndex < styleCount; ++styleIndex) {
  420. style.appendChild(styles[styleIndex]);
  421. }
  422. placemark.appendChild(createBasicElementWithText(kmlDoc, 'styleUrl', styleCache.get(style)));
  423. }
  424. if (geometries.length === 1) {
  425. placemark.appendChild(geometries[0]);
  426. } else if (geometries.length > 1) {
  427. var multigeometry = kmlDoc.createElement('MultiGeometry');
  428. for (var geometryIndex = 0; geometryIndex < geometryCount; ++geometryIndex) {
  429. multigeometry.appendChild(geometries[geometryIndex]);
  430. }
  431. placemark.appendChild(multigeometry);
  432. }
  433. }
  434. var children = entity._children;
  435. if (children.length > 0) {
  436. var folderNode = kmlDoc.createElement('Folder');
  437. folderNode.setAttribute('id', idManager.get(entity.id));
  438. folderNode.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name));
  439. folderNode.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show));
  440. folderNode.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description));
  441. parentNode.appendChild(folderNode);
  442. recurseEntities(state, folderNode, children);
  443. }
  444. }
  445. }
  446. var scratchCartesian3 = new Cartesian3();
  447. var scratchCartographic = new Cartographic();
  448. var scratchJulianDate = new JulianDate();
  449. function createPoint(state, entity, geometries, styles) {
  450. var kmlDoc = state.kmlDoc;
  451. var ellipsoid = state.ellipsoid;
  452. var valueGetter = state.valueGetter;
  453. var pointGraphics = defaultValue(entity.billboard, entity.point);
  454. if (!defined(pointGraphics) && !defined(entity.path)) {
  455. return;
  456. }
  457. // If the point isn't constant then create gx:Track or gx:MultiTrack
  458. var entityPositionProperty = entity.position;
  459. if (!entityPositionProperty.isConstant) {
  460. createTracks(state, entity, pointGraphics, geometries, styles);
  461. return;
  462. }
  463. valueGetter.get(entityPositionProperty, undefined, scratchCartesian3);
  464. var coordinates = createBasicElementWithText(kmlDoc, 'coordinates',
  465. getCoordinates(scratchCartesian3, ellipsoid));
  466. var pointGeometry = kmlDoc.createElement('Point');
  467. // Set altitude mode
  468. var altitudeMode = kmlDoc.createElement('altitudeMode');
  469. altitudeMode.appendChild(getAltitudeMode(state, pointGraphics.heightReference));
  470. pointGeometry.appendChild(altitudeMode);
  471. pointGeometry.appendChild(coordinates);
  472. geometries.push(pointGeometry);
  473. // Create style
  474. var iconStyle = (pointGraphics instanceof BillboardGraphics) ?
  475. createIconStyleFromBillboard(state, pointGraphics) : createIconStyleFromPoint(state, pointGraphics);
  476. styles.push(iconStyle);
  477. }
  478. function createTracks(state, entity, pointGraphics, geometries, styles) {
  479. var kmlDoc = state.kmlDoc;
  480. var ellipsoid = state.ellipsoid;
  481. var valueGetter = state.valueGetter;
  482. var intervals;
  483. var entityPositionProperty = entity.position;
  484. var useEntityPositionProperty = true;
  485. if (entityPositionProperty instanceof CompositePositionProperty) {
  486. intervals = entityPositionProperty.intervals;
  487. useEntityPositionProperty = false;
  488. } else {
  489. intervals = defaultValue(entity.availability, state.defaultAvailability);
  490. }
  491. var isModel = (pointGraphics instanceof ModelGraphics);
  492. var i, j, times;
  493. var tracks = [];
  494. for (i = 0; i < intervals.length; ++i) {
  495. var interval = intervals.get(i);
  496. var positionProperty = useEntityPositionProperty ? entityPositionProperty : interval.data;
  497. var trackAltitudeMode = kmlDoc.createElement('altitudeMode');
  498. // This is something that KML importing uses to handle clampToGround,
  499. // so just extract the internal property and set the altitudeMode.
  500. if (positionProperty instanceof ScaledPositionProperty) {
  501. positionProperty = positionProperty._value;
  502. trackAltitudeMode.appendChild(getAltitudeMode(state, HeightReference.CLAMP_TO_GROUND));
  503. } else if (defined(pointGraphics)) {
  504. trackAltitudeMode.appendChild(getAltitudeMode(state, pointGraphics.heightReference));
  505. } else {
  506. // Path graphics only, which has no height reference
  507. trackAltitudeMode.appendChild(getAltitudeMode(state, HeightReference.NONE));
  508. }
  509. var positionTimes = [];
  510. var positionValues = [];
  511. if (positionProperty.isConstant) {
  512. valueGetter.get(positionProperty, undefined, scratchCartesian3);
  513. var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates',
  514. getCoordinates(scratchCartesian3, ellipsoid));
  515. // This interval is constant so add a track with the same position
  516. positionTimes.push(JulianDate.toIso8601(interval.start));
  517. positionValues.push(constCoordinates);
  518. positionTimes.push(JulianDate.toIso8601(interval.stop));
  519. positionValues.push(constCoordinates);
  520. } else if (positionProperty instanceof SampledPositionProperty) {
  521. times = positionProperty._property._times;
  522. for (j = 0; j < times.length; ++j) {
  523. positionTimes.push(JulianDate.toIso8601(times[j]));
  524. positionProperty.getValueInReferenceFrame(times[j], ReferenceFrame.FIXED, scratchCartesian3);
  525. positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
  526. }
  527. } else if (positionProperty instanceof SampledProperty) {
  528. times = positionProperty._times;
  529. var values = positionProperty._values;
  530. for (j = 0; j < times.length; ++j) {
  531. positionTimes.push(JulianDate.toIso8601(times[j]));
  532. Cartesian3.fromArray(values, j * 3, scratchCartesian3);
  533. positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
  534. }
  535. } else {
  536. var duration = state.sampleDuration;
  537. interval.start.clone(scratchJulianDate);
  538. if (!interval.isStartIncluded) {
  539. JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate);
  540. }
  541. var stopDate = interval.stop;
  542. while (JulianDate.lessThan(scratchJulianDate, stopDate)) {
  543. positionProperty.getValue(scratchJulianDate, scratchCartesian3);
  544. positionTimes.push(JulianDate.toIso8601(scratchJulianDate));
  545. positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
  546. JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate);
  547. }
  548. if (interval.isStopIncluded && JulianDate.equals(scratchJulianDate, stopDate)) {
  549. positionProperty.getValue(scratchJulianDate, scratchCartesian3);
  550. positionTimes.push(JulianDate.toIso8601(scratchJulianDate));
  551. positionValues.push(getCoordinates(scratchCartesian3, ellipsoid));
  552. }
  553. }
  554. var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track');
  555. trackGeometry.appendChild(trackAltitudeMode);
  556. for (var k = 0; k < positionTimes.length; ++k) {
  557. var when = createBasicElementWithText(kmlDoc, 'when', positionTimes[k]);
  558. var coord = createBasicElementWithText(kmlDoc, 'coord', positionValues[k], gxNamespace);
  559. trackGeometry.appendChild(when);
  560. trackGeometry.appendChild(coord);
  561. }
  562. if (isModel) {
  563. trackGeometry.appendChild(createModelGeometry(state, pointGraphics));
  564. }
  565. tracks.push(trackGeometry);
  566. }
  567. // If one track, then use it otherwise combine into a multitrack
  568. if (tracks.length === 1) {
  569. geometries.push(tracks[0]);
  570. } else if (tracks.length > 1) {
  571. var multiTrackGeometry = kmlDoc.createElementNS(gxNamespace, 'MultiTrack');
  572. for (i = 0; i < tracks.length; ++i) {
  573. multiTrackGeometry.appendChild(tracks[i]);
  574. }
  575. geometries.push(multiTrackGeometry);
  576. }
  577. // Create style
  578. if (defined(pointGraphics) && !isModel) {
  579. var iconStyle = (pointGraphics instanceof BillboardGraphics) ?
  580. createIconStyleFromBillboard(state, pointGraphics) : createIconStyleFromPoint(state, pointGraphics);
  581. styles.push(iconStyle);
  582. }
  583. // See if we have a line that needs to be drawn
  584. var path = entity.path;
  585. if (defined(path)) {
  586. var width = valueGetter.get(path.width);
  587. var material = path.material;
  588. if (defined(material) || defined(width)) {
  589. var lineStyle = kmlDoc.createElement('LineStyle');
  590. if (defined(width)) {
  591. lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width));
  592. }
  593. processMaterial(state, material, lineStyle);
  594. styles.push(lineStyle);
  595. }
  596. }
  597. }
  598. function createIconStyleFromPoint(state, pointGraphics) {
  599. var kmlDoc = state.kmlDoc;
  600. var valueGetter = state.valueGetter;
  601. var iconStyle = kmlDoc.createElement('IconStyle');
  602. var color = valueGetter.getColor(pointGraphics.color);
  603. if (defined(color)) {
  604. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
  605. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
  606. }
  607. var pixelSize = valueGetter.get(pointGraphics.pixelSize);
  608. if (defined(pixelSize)) {
  609. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', pixelSize / BILLBOARD_SIZE));
  610. }
  611. return iconStyle;
  612. }
  613. function createIconStyleFromBillboard(state, billboardGraphics) {
  614. var kmlDoc = state.kmlDoc;
  615. var valueGetter = state.valueGetter;
  616. var externalFileHandler = state.externalFileHandler;
  617. var iconStyle = kmlDoc.createElement('IconStyle');
  618. var image = valueGetter.get(billboardGraphics.image);
  619. if (defined(image)) {
  620. image = externalFileHandler.texture(image);
  621. var icon = kmlDoc.createElement('Icon');
  622. icon.appendChild(createBasicElementWithText(kmlDoc, 'href', image));
  623. var imageSubRegion = valueGetter.get(billboardGraphics.imageSubRegion);
  624. if (defined(imageSubRegion)) {
  625. icon.appendChild(createBasicElementWithText(kmlDoc, 'x', imageSubRegion.x, gxNamespace));
  626. icon.appendChild(createBasicElementWithText(kmlDoc, 'y', imageSubRegion.y, gxNamespace));
  627. icon.appendChild(createBasicElementWithText(kmlDoc, 'w', imageSubRegion.width, gxNamespace));
  628. icon.appendChild(createBasicElementWithText(kmlDoc, 'h', imageSubRegion.height, gxNamespace));
  629. }
  630. iconStyle.appendChild(icon);
  631. }
  632. var color = valueGetter.getColor(billboardGraphics.color);
  633. if (defined(color)) {
  634. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
  635. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
  636. }
  637. var scale = valueGetter.get(billboardGraphics.scale);
  638. if (defined(scale)) {
  639. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale));
  640. }
  641. var pixelOffset = valueGetter.get(billboardGraphics.pixelOffset);
  642. if (defined(pixelOffset)) {
  643. scale = defaultValue(scale, 1.0);
  644. Cartesian2.divideByScalar(pixelOffset, scale, pixelOffset);
  645. var width = valueGetter.get(billboardGraphics.width, BILLBOARD_SIZE);
  646. var height = valueGetter.get(billboardGraphics.height, BILLBOARD_SIZE);
  647. // KML Hotspots are from the bottom left, but we work from the top left
  648. // Move to left
  649. var horizontalOrigin = valueGetter.get(billboardGraphics.horizontalOrigin, HorizontalOrigin.CENTER);
  650. if (horizontalOrigin === HorizontalOrigin.CENTER) {
  651. pixelOffset.x -= width * 0.5;
  652. } else if (horizontalOrigin === HorizontalOrigin.RIGHT) {
  653. pixelOffset.x -= width;
  654. }
  655. // Move to bottom
  656. var verticalOrigin = valueGetter.get(billboardGraphics.verticalOrigin, VerticalOrigin.CENTER);
  657. if (verticalOrigin === VerticalOrigin.TOP) {
  658. pixelOffset.y += height;
  659. } else if (verticalOrigin === VerticalOrigin.CENTER) {
  660. pixelOffset.y += height * 0.5;
  661. }
  662. var hotSpot = kmlDoc.createElement('hotSpot');
  663. hotSpot.setAttribute('x', -pixelOffset.x);
  664. hotSpot.setAttribute('y', pixelOffset.y);
  665. hotSpot.setAttribute('xunits', 'pixels');
  666. hotSpot.setAttribute('yunits', 'pixels');
  667. iconStyle.appendChild(hotSpot);
  668. }
  669. // We can only specify heading so if axis isn't Z, then we skip the rotation
  670. // GE treats a heading of zero as no heading but can still point north using a 360 degree angle
  671. var rotation = valueGetter.get(billboardGraphics.rotation);
  672. var alignedAxis = valueGetter.get(billboardGraphics.alignedAxis);
  673. if (defined(rotation) && Cartesian3.equals(Cartesian3.UNIT_Z, alignedAxis)) {
  674. rotation = CesiumMath.toDegrees(-rotation);
  675. if (rotation === 0) {
  676. rotation = 360;
  677. }
  678. iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'heading', rotation));
  679. }
  680. return iconStyle;
  681. }
  682. function createLineString(state, polylineGraphics, geometries, styles) {
  683. var kmlDoc = state.kmlDoc;
  684. var ellipsoid = state.ellipsoid;
  685. var valueGetter = state.valueGetter;
  686. if (!defined(polylineGraphics)) {
  687. return;
  688. }
  689. var lineStringGeometry = kmlDoc.createElement('LineString');
  690. // Set altitude mode
  691. var altitudeMode = kmlDoc.createElement('altitudeMode');
  692. var clampToGround = valueGetter.get(polylineGraphics.clampToGround, false);
  693. var altitudeModeText;
  694. if (clampToGround) {
  695. lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tessellate', true));
  696. altitudeModeText = kmlDoc.createTextNode('clampToGround');
  697. } else {
  698. altitudeModeText = kmlDoc.createTextNode('absolute');
  699. }
  700. altitudeMode.appendChild(altitudeModeText);
  701. lineStringGeometry.appendChild(altitudeMode);
  702. // Set coordinates
  703. var positionsProperty = polylineGraphics.positions;
  704. var cartesians = valueGetter.get(positionsProperty);
  705. var coordinates = createBasicElementWithText(kmlDoc, 'coordinates',
  706. getCoordinates(cartesians, ellipsoid));
  707. lineStringGeometry.appendChild(coordinates);
  708. // Set draw order
  709. var zIndex = valueGetter.get(polylineGraphics.zIndex);
  710. if (clampToGround && defined(zIndex)) {
  711. lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace));
  712. }
  713. geometries.push(lineStringGeometry);
  714. // Create style
  715. var lineStyle = kmlDoc.createElement('LineStyle');
  716. var width = valueGetter.get(polylineGraphics.width);
  717. if (defined(width)) {
  718. lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width));
  719. }
  720. processMaterial(state, polylineGraphics.material, lineStyle);
  721. styles.push(lineStyle);
  722. }
  723. function getRectangleBoundaries(state, rectangleGraphics, extrudedHeight) {
  724. var kmlDoc = state.kmlDoc;
  725. var valueGetter = state.valueGetter;
  726. var coordinates;
  727. var height = valueGetter.get(rectangleGraphics.height, 0.0);
  728. if (extrudedHeight > 0) {
  729. // We extrude up and KML extrudes down, so if we extrude, set the polygon height to
  730. // the extruded height so KML will look similar to Cesium
  731. height = extrudedHeight;
  732. }
  733. var coordinatesProperty = rectangleGraphics.coordinates;
  734. var rectangle = valueGetter.get(coordinatesProperty);
  735. var coordinateStrings = [];
  736. var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest];
  737. for (var i = 0; i < 4; ++i) {
  738. cornerFunction[i](rectangle, scratchCartographic);
  739. coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' +
  740. CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height);
  741. }
  742. coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' '));
  743. var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs');
  744. var linearRing = kmlDoc.createElement('LinearRing');
  745. linearRing.appendChild(coordinates);
  746. outerBoundaryIs.appendChild(linearRing);
  747. return [outerBoundaryIs];
  748. }
  749. function getLinearRing(state, positions, height, perPositionHeight) {
  750. var kmlDoc = state.kmlDoc;
  751. var ellipsoid = state.ellipsoid;
  752. var coordinateStrings = [];
  753. var positionCount = positions.length;
  754. for (var i = 0; i < positionCount; ++i) {
  755. Cartographic.fromCartesian(positions[i], ellipsoid, scratchCartographic);
  756. coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' +
  757. CesiumMath.toDegrees(scratchCartographic.latitude) + ',' +
  758. (perPositionHeight ? scratchCartographic.height : height));
  759. }
  760. var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' '));
  761. var linearRing = kmlDoc.createElement('LinearRing');
  762. linearRing.appendChild(coordinates);
  763. return linearRing;
  764. }
  765. function getPolygonBoundaries(state, polygonGraphics, extrudedHeight) {
  766. var kmlDoc = state.kmlDoc;
  767. var valueGetter = state.valueGetter;
  768. var height = valueGetter.get(polygonGraphics.height, 0.0);
  769. var perPositionHeight = valueGetter.get(polygonGraphics.perPositionHeight, false);
  770. if (!perPositionHeight && (extrudedHeight > 0)) {
  771. // We extrude up and KML extrudes down, so if we extrude, set the polygon height to
  772. // the extruded height so KML will look similar to Cesium
  773. height = extrudedHeight;
  774. }
  775. var boundaries = [];
  776. var hierarchyProperty = polygonGraphics.hierarchy;
  777. var hierarchy = valueGetter.get(hierarchyProperty);
  778. // Polygon hierarchy can sometimes just be an array of positions
  779. var positions = isArray(hierarchy) ? hierarchy : hierarchy.positions;
  780. // Polygon boundaries
  781. var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs');
  782. outerBoundaryIs.appendChild(getLinearRing(state, positions, height, perPositionHeight));
  783. boundaries.push(outerBoundaryIs);
  784. // Hole boundaries
  785. var holes = hierarchy.holes;
  786. if (defined(holes)) {
  787. var holeCount = holes.length;
  788. for (var i = 0; i < holeCount; ++i) {
  789. var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs');
  790. innerBoundaryIs.appendChild(getLinearRing(state, holes[i].positions, height, perPositionHeight));
  791. boundaries.push(innerBoundaryIs);
  792. }
  793. }
  794. return boundaries;
  795. }
  796. function createPolygon(state, geometry, geometries, styles, overlays) {
  797. var kmlDoc = state.kmlDoc;
  798. var valueGetter = state.valueGetter;
  799. if (!defined(geometry)) {
  800. return;
  801. }
  802. // Detect textured quads and use ground overlays instead
  803. var isRectangle = (geometry instanceof RectangleGraphics);
  804. if (isRectangle && valueGetter.getMaterialType(geometry.material) === 'Image') {
  805. createGroundOverlay(state, geometry, overlays);
  806. return;
  807. }
  808. var polygonGeometry = kmlDoc.createElement('Polygon');
  809. var extrudedHeight = valueGetter.get(geometry.extrudedHeight, 0.0);
  810. if (extrudedHeight > 0) {
  811. polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'extrude', true));
  812. }
  813. // Set boundaries
  814. var boundaries = isRectangle ? getRectangleBoundaries(state, geometry, extrudedHeight) :
  815. getPolygonBoundaries(state, geometry, extrudedHeight);
  816. var boundaryCount = boundaries.length;
  817. for (var i = 0; i < boundaryCount; ++i) {
  818. polygonGeometry.appendChild(boundaries[i]);
  819. }
  820. // Set altitude mode
  821. var altitudeMode = kmlDoc.createElement('altitudeMode');
  822. altitudeMode.appendChild(getAltitudeMode(state, geometry.heightReference));
  823. polygonGeometry.appendChild(altitudeMode);
  824. geometries.push(polygonGeometry);
  825. // Create style
  826. var polyStyle = kmlDoc.createElement('PolyStyle');
  827. var fill = valueGetter.get(geometry.fill, false);
  828. if (fill) {
  829. polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'fill', fill));
  830. }
  831. processMaterial(state, geometry.material, polyStyle);
  832. var outline = valueGetter.get(geometry.outline, false);
  833. if (outline) {
  834. polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'outline', outline));
  835. // Outline uses LineStyle
  836. var lineStyle = kmlDoc.createElement('LineStyle');
  837. var outlineWidth = valueGetter.get(geometry.outlineWidth, 1.0);
  838. lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', outlineWidth));
  839. var outlineColor = valueGetter.getColor(geometry.outlineColor, Color.BLACK);
  840. lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', outlineColor));
  841. lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
  842. styles.push(lineStyle);
  843. }
  844. styles.push(polyStyle);
  845. }
  846. function createGroundOverlay(state, rectangleGraphics, overlays) {
  847. var kmlDoc = state.kmlDoc;
  848. var valueGetter = state.valueGetter;
  849. var externalFileHandler = state.externalFileHandler;
  850. var groundOverlay = kmlDoc.createElement('GroundOverlay');
  851. // Set altitude mode
  852. var altitudeMode = kmlDoc.createElement('altitudeMode');
  853. altitudeMode.appendChild(getAltitudeMode(state, rectangleGraphics.heightReference));
  854. groundOverlay.appendChild(altitudeMode);
  855. var height = valueGetter.get(rectangleGraphics.height);
  856. if (defined(height)) {
  857. groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'altitude', height));
  858. }
  859. var rectangle = valueGetter.get(rectangleGraphics.coordinates);
  860. var latLonBox = kmlDoc.createElement('LatLonBox');
  861. latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'north', CesiumMath.toDegrees(rectangle.north)));
  862. latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'south', CesiumMath.toDegrees(rectangle.south)));
  863. latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'east', CesiumMath.toDegrees(rectangle.east)));
  864. latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'west', CesiumMath.toDegrees(rectangle.west)));
  865. groundOverlay.appendChild(latLonBox);
  866. // We should only end up here if we have an ImageMaterialProperty
  867. var material = valueGetter.get(rectangleGraphics.material);
  868. var href = externalFileHandler.texture(material.image);
  869. var icon = kmlDoc.createElement('Icon');
  870. icon.appendChild(createBasicElementWithText(kmlDoc, 'href', href));
  871. groundOverlay.appendChild(icon);
  872. var color = material.color;
  873. if (defined(color)) {
  874. groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'color', colorToString(material.color)));
  875. }
  876. overlays.push(groundOverlay);
  877. }
  878. function createModelGeometry(state, modelGraphics) {
  879. var kmlDoc = state.kmlDoc;
  880. var valueGetter = state.valueGetter;
  881. var externalFileHandler = state.externalFileHandler;
  882. var modelGeometry = kmlDoc.createElement('Model');
  883. var scale = valueGetter.get(modelGraphics.scale);
  884. if (defined(scale)) {
  885. var scaleElement = kmlDoc.createElement('scale');
  886. scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'x', scale));
  887. scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'y', scale));
  888. scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'z', scale));
  889. modelGeometry.appendChild(scaleElement);
  890. }
  891. var link = kmlDoc.createElement('Link');
  892. var uri = externalFileHandler.model(modelGraphics, state.time);
  893. link.appendChild(createBasicElementWithText(kmlDoc, 'href', uri));
  894. modelGeometry.appendChild(link);
  895. return modelGeometry;
  896. }
  897. function createModel(state, entity, modelGraphics, geometries, styles) {
  898. var kmlDoc = state.kmlDoc;
  899. var ellipsoid = state.ellipsoid;
  900. var valueGetter = state.valueGetter;
  901. if (!defined(modelGraphics)) {
  902. return;
  903. }
  904. // If the point isn't constant then create gx:Track or gx:MultiTrack
  905. var entityPositionProperty = entity.position;
  906. if (!entityPositionProperty.isConstant) {
  907. createTracks(state, entity, modelGraphics, geometries, styles);
  908. return;
  909. }
  910. var modelGeometry = createModelGeometry(state, modelGraphics);
  911. // Set altitude mode
  912. var altitudeMode = kmlDoc.createElement('altitudeMode');
  913. altitudeMode.appendChild(getAltitudeMode(state, modelGraphics.heightReference));
  914. modelGeometry.appendChild(altitudeMode);
  915. valueGetter.get(entityPositionProperty, undefined, scratchCartesian3);
  916. Cartographic.fromCartesian(scratchCartesian3, ellipsoid, scratchCartographic);
  917. var location = kmlDoc.createElement('Location');
  918. location.appendChild(createBasicElementWithText(kmlDoc, 'longitude', CesiumMath.toDegrees(scratchCartographic.longitude)));
  919. location.appendChild(createBasicElementWithText(kmlDoc, 'latitude', CesiumMath.toDegrees(scratchCartographic.latitude)));
  920. location.appendChild(createBasicElementWithText(kmlDoc, 'altitude', scratchCartographic.height));
  921. modelGeometry.appendChild(location);
  922. geometries.push(modelGeometry);
  923. }
  924. function processMaterial(state, materialProperty, style) {
  925. var kmlDoc = state.kmlDoc;
  926. var valueGetter = state.valueGetter;
  927. if (!defined(materialProperty)) {
  928. return;
  929. }
  930. var material = valueGetter.get(materialProperty);
  931. if (!defined(material)) {
  932. return;
  933. }
  934. var color;
  935. var type = valueGetter.getMaterialType(materialProperty);
  936. switch (type) {
  937. case 'Image':
  938. // Image materials are only able to be represented on rectangles, so if we make it
  939. // here we can't texture a generic polygon or polyline in KML, so just use white.
  940. color = colorToString(Color.WHITE);
  941. break;
  942. case 'Color':
  943. case 'Grid':
  944. case 'PolylineGlow':
  945. case 'PolylineArrow':
  946. case 'PolylineDash':
  947. color = colorToString(material.color);
  948. break;
  949. case 'PolylineOutline':
  950. color = colorToString(material.color);
  951. var outlineColor = colorToString(material.outlineColor);
  952. var outlineWidth = material.outlineWidth;
  953. style.appendChild(createBasicElementWithText(kmlDoc, 'outerColor', outlineColor, gxNamespace));
  954. style.appendChild(createBasicElementWithText(kmlDoc, 'outerWidth', outlineWidth, gxNamespace));
  955. break;
  956. case 'Stripe':
  957. color = colorToString(material.oddColor);
  958. break;
  959. }
  960. if (defined(color)) {
  961. style.appendChild(createBasicElementWithText(kmlDoc, 'color', color));
  962. style.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal'));
  963. }
  964. }
  965. function getAltitudeMode(state, heightReferenceProperty) {
  966. var kmlDoc = state.kmlDoc;
  967. var valueGetter = state.valueGetter;
  968. var heightReference = valueGetter.get(heightReferenceProperty, HeightReference.NONE);
  969. var altitudeModeText;
  970. switch (heightReference) {
  971. case HeightReference.NONE:
  972. altitudeModeText = kmlDoc.createTextNode('absolute');
  973. break;
  974. case HeightReference.CLAMP_TO_GROUND:
  975. altitudeModeText = kmlDoc.createTextNode('clampToGround');
  976. break;
  977. case HeightReference.RELATIVE_TO_GROUND:
  978. altitudeModeText = kmlDoc.createTextNode('relativeToGround');
  979. break;
  980. }
  981. return altitudeModeText;
  982. }
  983. function getCoordinates(coordinates, ellipsoid) {
  984. if (!isArray(coordinates)) {
  985. coordinates = [coordinates];
  986. }
  987. var count = coordinates.length;
  988. var coordinateStrings = [];
  989. for (var i = 0; i < count; ++i) {
  990. Cartographic.fromCartesian(coordinates[i], ellipsoid, scratchCartographic);
  991. coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' +
  992. CesiumMath.toDegrees(scratchCartographic.latitude) + ',' +
  993. scratchCartographic.height);
  994. }
  995. return coordinateStrings.join(' ');
  996. }
  997. function createBasicElementWithText(kmlDoc, elementName, elementValue, namespace) {
  998. elementValue = defaultValue(elementValue, '');
  999. if (typeof elementValue === 'boolean') {
  1000. elementValue = elementValue ? '1' : '0';
  1001. }
  1002. // Create element with optional namespace
  1003. var element = defined(namespace) ? kmlDoc.createElementNS(namespace, elementName) : kmlDoc.createElement(elementName);
  1004. // Wrap value in CDATA section if it contains HTML
  1005. var text = ((elementValue === 'string') && (elementValue.indexOf('<') !== -1)) ?
  1006. kmlDoc.createCDATASection(elementValue) : kmlDoc.createTextNode(elementValue);
  1007. element.appendChild(text);
  1008. return element;
  1009. }
  1010. function colorToString(color) {
  1011. var result = '';
  1012. var bytes = color.toBytes();
  1013. for (var i = 3; i >= 0; --i) {
  1014. result += (bytes[i] < 16) ? ('0' + bytes[i].toString(16)) : bytes[i].toString(16);
  1015. }
  1016. return result;
  1017. }
  1018. /**
  1019. * Since KML does not support glTF models, this callback is required to specify what URL to use for the model in the KML document.
  1020. * It can also be used to add additional files to the <code>externalFiles</code> object, which is the list of files embedded in the exported KMZ,
  1021. * or otherwise returned with the KML string when exporting.
  1022. *
  1023. * @callback exportKml~ModelCallback
  1024. *
  1025. * @param {ModelGraphics} model The ModelGraphics instance for an Entity.
  1026. * @param {JulianDate} time The time that any properties should use to get the value.
  1027. * @param {Object} externalFiles An object that maps a filename to a Blob or a Promise that resolves to a Blob.
  1028. * @returns {String} The URL to use for the href in the KML document.
  1029. */
  1030. export default exportKml;