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