exportKml.js 46 KB

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