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 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 * as zip from "@zip.js/zip.js/lib/zip-no-worker.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) { filename = `texture_${++this._count}.png`; const promise = new Promise((resolve) => { texture.toBlob(function (blob) { that._files[filename] = blob; resolve(); }); }); this._promises.push(promise); 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} 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} 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 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