exportKml.js 46 KB

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