import ArcType from "../Core/ArcType.js"; import AssociativeArray from "../Core/AssociativeArray.js"; import BoundingRectangle from "../Core/BoundingRectangle.js"; 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 ClockRange from "../Core/ClockRange.js"; import ClockStep from "../Core/ClockStep.js"; import clone from "../Core/clone.js"; import Color from "../Core/Color.js"; import createGuid from "../Core/createGuid.js"; import Credit from "../Core/Credit.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 Event from "../Core/Event.js"; import getExtensionFromUri from "../Core/getExtensionFromUri.js"; import getFilenameFromUri from "../Core/getFilenameFromUri.js"; import getTimestamp from "../Core/getTimestamp.js"; import HeadingPitchRange from "../Core/HeadingPitchRange.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import Iso8601 from "../Core/Iso8601.js"; import JulianDate from "../Core/JulianDate.js"; import CesiumMath from "../Core/Math.js"; import NearFarScalar from "../Core/NearFarScalar.js"; import objectToQuery from "../Core/objectToQuery.js"; import oneTimeWarning from "../Core/oneTimeWarning.js"; import PinBuilder from "../Core/PinBuilder.js"; import PolygonHierarchy from "../Core/PolygonHierarchy.js"; import queryToObject from "../Core/queryToObject.js"; import Rectangle from "../Core/Rectangle.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 LabelStyle from "../Scene/LabelStyle.js"; import SceneMode from "../Scene/SceneMode.js"; import Autolinker from "autolinker"; import Uri from "urijs"; import * as zip from "@zip.js/zip.js/lib/zip-no-worker.js"; import getElement from "./getElement.js"; import BillboardGraphics from "./BillboardGraphics.js"; import CompositePositionProperty from "./CompositePositionProperty.js"; import DataSource from "./DataSource.js"; import DataSourceClock from "./DataSourceClock.js"; import Entity from "./Entity.js"; import EntityCluster from "./EntityCluster.js"; import EntityCollection from "./EntityCollection.js"; import KmlCamera from "./KmlCamera.js"; import KmlLookAt from "./KmlLookAt.js"; import KmlTour from "./KmlTour.js"; import KmlTourFlyTo from "./KmlTourFlyTo.js"; import KmlTourWait from "./KmlTourWait.js"; import LabelGraphics from "./LabelGraphics.js"; import PathGraphics from "./PathGraphics.js"; import PolygonGraphics from "./PolygonGraphics.js"; import PolylineGraphics from "./PolylineGraphics.js"; import PositionPropertyArray from "./PositionPropertyArray.js"; import RectangleGraphics from "./RectangleGraphics.js"; import ReferenceProperty from "./ReferenceProperty.js"; import SampledPositionProperty from "./SampledPositionProperty.js"; import ScaledPositionProperty from "./ScaledPositionProperty.js"; import TimeIntervalCollectionProperty from "./TimeIntervalCollectionProperty.js"; import WallGraphics from "./WallGraphics.js"; //This is by no means an exhaustive list of MIME types. //The purpose of this list is to be able to accurately identify content embedded //in KMZ files. Eventually, we can make this configurable by the end user so they can add //there own content types if they have KMZ files that require it. const MimeTypes = { avi: "video/x-msvideo", bmp: "image/bmp", bz2: "application/x-bzip2", chm: "application/vnd.ms-htmlhelp", css: "text/css", csv: "text/csv", doc: "application/msword", dvi: "application/x-dvi", eps: "application/postscript", flv: "video/x-flv", gif: "image/gif", gz: "application/x-gzip", htm: "text/html", html: "text/html", ico: "image/vnd.microsoft.icon", jnlp: "application/x-java-jnlp-file", jpeg: "image/jpeg", jpg: "image/jpeg", m3u: "audio/x-mpegurl", m4v: "video/mp4", mathml: "application/mathml+xml", mid: "audio/midi", midi: "audio/midi", mov: "video/quicktime", mp3: "audio/mpeg", mp4: "video/mp4", mp4v: "video/mp4", mpeg: "video/mpeg", mpg: "video/mpeg", odp: "application/vnd.oasis.opendocument.presentation", ods: "application/vnd.oasis.opendocument.spreadsheet", odt: "application/vnd.oasis.opendocument.text", ogg: "application/ogg", pdf: "application/pdf", png: "image/png", pps: "application/vnd.ms-powerpoint", ppt: "application/vnd.ms-powerpoint", ps: "application/postscript", qt: "video/quicktime", rdf: "application/rdf+xml", rss: "application/rss+xml", rtf: "application/rtf", svg: "image/svg+xml", swf: "application/x-shockwave-flash", text: "text/plain", tif: "image/tiff", tiff: "image/tiff", txt: "text/plain", wav: "audio/x-wav", wma: "audio/x-ms-wma", wmv: "video/x-ms-wmv", xml: "application/xml", zip: "application/zip", detectFromFilename: function (filename) { let ext = filename.toLowerCase(); ext = getExtensionFromUri(ext); return MimeTypes[ext]; }, }; let parser; if (typeof DOMParser !== "undefined") { parser = new DOMParser(); } const autolinker = new Autolinker({ stripPrefix: false, email: false, replaceFn: function (match) { //Prevent matching of non-explicit urls. //i.e. foo.id won't match but http://foo.id will return match.urlMatchType === "scheme" || match.urlMatchType === "www"; }, }); const BILLBOARD_SIZE = 32; const BILLBOARD_NEAR_DISTANCE = 2414016; const BILLBOARD_NEAR_RATIO = 1.0; const BILLBOARD_FAR_DISTANCE = 1.6093e7; const BILLBOARD_FAR_RATIO = 0.1; const kmlNamespaces = [ null, undefined, "http://www.opengis.net/kml/2.2", "http://earth.google.com/kml/2.2", "http://earth.google.com/kml/2.1", "http://earth.google.com/kml/2.0", ]; const gxNamespaces = ["http://www.google.com/kml/ext/2.2"]; const atomNamespaces = ["http://www.w3.org/2005/Atom"]; const namespaces = { kml: kmlNamespaces, gx: gxNamespaces, atom: atomNamespaces, kmlgx: kmlNamespaces.concat(gxNamespaces), }; // Ensure Specs/Data/KML/unsupported.kml is kept up to date with these supported types const featureTypes = { Document: processDocument, Folder: processFolder, Placemark: processPlacemark, NetworkLink: processNetworkLink, GroundOverlay: processGroundOverlay, PhotoOverlay: processUnsupportedFeature, ScreenOverlay: processScreenOverlay, Tour: processTour, }; function DeferredLoading(dataSource) { this._dataSource = dataSource; this._deferred = defer(); this._stack = []; this._promises = []; this._timeoutSet = false; this._used = false; this._started = 0; this._timeThreshold = 1000; // Initial load is 1 second } Object.defineProperties(DeferredLoading.prototype, { dataSource: { get: function () { return this._dataSource; }, }, }); DeferredLoading.prototype.addNodes = function (nodes, processingData) { this._stack.push({ nodes: nodes, index: 0, processingData: processingData, }); this._used = true; }; DeferredLoading.prototype.addPromise = function (promise) { this._promises.push(promise); }; DeferredLoading.prototype.wait = function () { // Case where we had a non-document/folder as the root const deferred = this._deferred; if (!this._used) { deferred.resolve(); } return Promise.all([deferred.promise, Promise.all(this._promises)]); }; DeferredLoading.prototype.process = function () { const isFirstCall = this._stack.length === 1; if (isFirstCall) { this._started = KmlDataSource._getTimestamp(); } return this._process(isFirstCall); }; DeferredLoading.prototype._giveUpTime = function () { if (this._timeoutSet) { // Timeout was already set so just return return; } this._timeoutSet = true; this._timeThreshold = 50; // After the first load lower threshold to 0.5 seconds const that = this; setTimeout(function () { that._timeoutSet = false; that._started = KmlDataSource._getTimestamp(); that._process(true); }, 0); }; DeferredLoading.prototype._nextNode = function () { const stack = this._stack; const top = stack[stack.length - 1]; const index = top.index; const nodes = top.nodes; if (index === nodes.length) { return; } ++top.index; return nodes[index]; }; DeferredLoading.prototype._pop = function () { const stack = this._stack; stack.pop(); // Return false if we are done if (stack.length === 0) { this._deferred.resolve(); return false; } return true; }; DeferredLoading.prototype._process = function (isFirstCall) { const dataSource = this.dataSource; const processingData = this._stack[this._stack.length - 1].processingData; let child = this._nextNode(); while (defined(child)) { const featureProcessor = featureTypes[child.localName]; if ( defined(featureProcessor) && (namespaces.kml.indexOf(child.namespaceURI) !== -1 || namespaces.gx.indexOf(child.namespaceURI) !== -1) ) { featureProcessor(dataSource, child, processingData, this); // Give up time and continue loading later if ( this._timeoutSet || KmlDataSource._getTimestamp() > this._started + this._timeThreshold ) { this._giveUpTime(); return; } } child = this._nextNode(); } // If we are a recursive call from a subfolder, just return so the parent folder can continue processing // If we aren't then make another call to processNodes because there is stuff still left in the queue if (this._pop() && isFirstCall) { this._process(true); } }; function isZipFile(blob) { const magicBlob = blob.slice(0, Math.min(4, blob.size)); const deferred = defer(); const reader = new FileReader(); reader.addEventListener("load", function () { deferred.resolve( new DataView(reader.result).getUint32(0, false) === 0x504b0304 ); }); reader.addEventListener("error", function () { deferred.reject(reader.error); }); reader.readAsArrayBuffer(magicBlob); return deferred.promise; } function readBlobAsText(blob) { const deferred = defer(); const reader = new FileReader(); reader.addEventListener("load", function () { deferred.resolve(reader.result); }); reader.addEventListener("error", function () { deferred.reject(reader.error); }); reader.readAsText(blob); return deferred.promise; } function insertNamespaces(text) { const namespaceMap = { xsi: "http://www.w3.org/2001/XMLSchema-instance", }; let firstPart, lastPart, reg, declaration; for (const key in namespaceMap) { if (namespaceMap.hasOwnProperty(key)) { reg = RegExp(`[< ]${key}:`); declaration = `xmlns:${key}=`; if (reg.test(text) && text.indexOf(declaration) === -1) { if (!defined(firstPart)) { firstPart = text.substr(0, text.indexOf("", index); let namespace, startIndex, endIndex; while (index !== -1 && index < endDeclaration) { namespace = text.slice(index, text.indexOf('"', index)); startIndex = index; index = text.indexOf(namespace, index + 1); if (index !== -1) { endIndex = text.indexOf('"', text.indexOf('"', index) + 1); text = text.slice(0, index - 1) + text.slice(endIndex + 1, text.length); index = text.indexOf("xmlns:", startIndex - 1); } else { index = text.indexOf("xmlns:", startIndex + 1); } } return text; } function loadXmlFromZip(entry, uriResolver) { return Promise.resolve(entry.getData(new zip.TextWriter())).then(function ( text ) { text = insertNamespaces(text); text = removeDuplicateNamespaces(text); uriResolver.kml = parser.parseFromString(text, "application/xml"); }); } function loadDataUriFromZip(entry, uriResolver) { const mimeType = defaultValue( MimeTypes.detectFromFilename(entry.filename), "application/octet-stream" ); return Promise.resolve(entry.getData(new zip.Data64URIWriter(mimeType))).then( function (dataUri) { uriResolver[entry.filename] = dataUri; } ); } function embedDataUris(div, elementType, attributeName, uriResolver) { const keys = uriResolver.keys; const baseUri = new Uri("."); const elements = div.querySelectorAll(elementType); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const value = element.getAttribute(attributeName); if (defined(value)) { const relativeUri = new Uri(value); const uri = relativeUri.absoluteTo(baseUri).toString(); const index = keys.indexOf(uri); if (index !== -1) { const key = keys[index]; element.setAttribute(attributeName, uriResolver[key]); if (elementType === "a" && element.getAttribute("download") === null) { element.setAttribute("download", key); } } } } } function applyBasePath(div, elementType, attributeName, sourceResource) { const elements = div.querySelectorAll(elementType); for (let i = 0; i < elements.length; i++) { const element = elements[i]; const value = element.getAttribute(attributeName); const resource = resolveHref(value, sourceResource); if (defined(resource)) { element.setAttribute(attributeName, resource.url); } } } // an optional context is passed to allow for some malformed kmls (those with multiple geometries with same ids) to still parse // correctly, as they do in Google Earth. function createEntity(node, entityCollection, context) { let id = queryStringAttribute(node, "id"); id = defined(id) && id.length !== 0 ? id : createGuid(); if (defined(context)) { id = context + id; } // If we have a duplicate ID just generate one. // This isn't valid KML but Google Earth handles this case. let entity = entityCollection.getById(id); if (defined(entity)) { id = createGuid(); if (defined(context)) { id = context + id; } } entity = entityCollection.add(new Entity({ id: id })); if (!defined(entity.kml)) { entity.addProperty("kml"); entity.kml = new KmlFeatureData(); } return entity; } function isExtrudable(altitudeMode, gxAltitudeMode) { return ( altitudeMode === "absolute" || altitudeMode === "relativeToGround" || gxAltitudeMode === "relativeToSeaFloor" ); } function readCoordinate(value, ellipsoid) { //Google Earth treats empty or missing coordinates as 0. if (!defined(value)) { return Cartesian3.fromDegrees(0, 0, 0, ellipsoid); } const digits = value.match(/[^\s,\n]+/g); if (!defined(digits)) { return Cartesian3.fromDegrees(0, 0, 0, ellipsoid); } let longitude = parseFloat(digits[0]); let latitude = parseFloat(digits[1]); let height = parseFloat(digits[2]); longitude = isNaN(longitude) ? 0.0 : longitude; latitude = isNaN(latitude) ? 0.0 : latitude; height = isNaN(height) ? 0.0 : height; return Cartesian3.fromDegrees(longitude, latitude, height, ellipsoid); } function readCoordinates(element, ellipsoid) { if (!defined(element)) { return undefined; } const tuples = element.textContent.match(/[^\s\n]+/g); if (!defined(tuples)) { return undefined; } const length = tuples.length; const result = new Array(length); let resultIndex = 0; for (let i = 0; i < length; i++) { result[resultIndex++] = readCoordinate(tuples[i], ellipsoid); } return result; } function queryNumericAttribute(node, attributeName) { if (!defined(node)) { return undefined; } const value = node.getAttribute(attributeName); if (value !== null) { const result = parseFloat(value); return !isNaN(result) ? result : undefined; } return undefined; } function queryStringAttribute(node, attributeName) { if (!defined(node)) { return undefined; } const value = node.getAttribute(attributeName); return value !== null ? value : undefined; } function queryFirstNode(node, tagName, namespace) { if (!defined(node)) { return undefined; } const childNodes = node.childNodes; const length = childNodes.length; for (let q = 0; q < length; q++) { const child = childNodes[q]; if ( child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1 ) { return child; } } return undefined; } function queryNodes(node, tagName, namespace) { if (!defined(node)) { return undefined; } const result = []; const childNodes = node.getElementsByTagNameNS("*", tagName); const length = childNodes.length; for (let q = 0; q < length; q++) { const child = childNodes[q]; if ( child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1 ) { result.push(child); } } return result; } function queryChildNodes(node, tagName, namespace) { if (!defined(node)) { return []; } const result = []; const childNodes = node.childNodes; const length = childNodes.length; for (let q = 0; q < length; q++) { const child = childNodes[q]; if ( child.localName === tagName && namespace.indexOf(child.namespaceURI) !== -1 ) { result.push(child); } } return result; } function queryNumericValue(node, tagName, namespace) { const resultNode = queryFirstNode(node, tagName, namespace); if (defined(resultNode)) { const result = parseFloat(resultNode.textContent); return !isNaN(result) ? result : undefined; } return undefined; } function queryStringValue(node, tagName, namespace) { const result = queryFirstNode(node, tagName, namespace); if (defined(result)) { return result.textContent.trim(); } return undefined; } function queryBooleanValue(node, tagName, namespace) { const result = queryFirstNode(node, tagName, namespace); if (defined(result)) { const value = result.textContent.trim(); return value === "1" || /^true$/i.test(value); } return undefined; } function resolveHref(href, sourceResource, uriResolver) { if (!defined(href)) { return undefined; } let resource; if (defined(uriResolver)) { // To resolve issues with KML sources defined in Windows style paths. href = href.replace(/\\/g, "/"); let blob = uriResolver[href]; if (defined(blob)) { resource = new Resource({ url: blob, }); } else { // Needed for multiple levels of KML files in a KMZ const baseUri = new Uri(sourceResource.getUrlComponent()); const uri = new Uri(href); blob = uriResolver[uri.absoluteTo(baseUri)]; if (defined(blob)) { resource = new Resource({ url: blob, }); } } } if (!defined(resource)) { resource = sourceResource.getDerivedResource({ url: href, }); } return resource; } const colorOptions = { maximumRed: undefined, red: undefined, maximumGreen: undefined, green: undefined, maximumBlue: undefined, blue: undefined, }; function parseColorString(value, isRandom) { if (!defined(value) || /^\s*$/gm.test(value)) { return undefined; } if (value[0] === "#") { value = value.substring(1); } const alpha = parseInt(value.substring(0, 2), 16) / 255.0; const blue = parseInt(value.substring(2, 4), 16) / 255.0; const green = parseInt(value.substring(4, 6), 16) / 255.0; const red = parseInt(value.substring(6, 8), 16) / 255.0; if (!isRandom) { return new Color(red, green, blue, alpha); } if (red > 0) { colorOptions.maximumRed = red; colorOptions.red = undefined; } else { colorOptions.maximumRed = undefined; colorOptions.red = 0; } if (green > 0) { colorOptions.maximumGreen = green; colorOptions.green = undefined; } else { colorOptions.maximumGreen = undefined; colorOptions.green = 0; } if (blue > 0) { colorOptions.maximumBlue = blue; colorOptions.blue = undefined; } else { colorOptions.maximumBlue = undefined; colorOptions.blue = 0; } colorOptions.alpha = alpha; return Color.fromRandom(colorOptions); } function queryColorValue(node, tagName, namespace) { const value = queryStringValue(node, tagName, namespace); if (!defined(value)) { return undefined; } return parseColorString( value, queryStringValue(node, "colorMode", namespace) === "random" ); } function processTimeStamp(featureNode) { const node = queryFirstNode(featureNode, "TimeStamp", namespaces.kmlgx); const whenString = queryStringValue(node, "when", namespaces.kmlgx); if (!defined(node) || !defined(whenString) || whenString.length === 0) { return undefined; } //According to the KML spec, a TimeStamp represents a "single moment in time" //However, since Cesium animates much differently than Google Earth, that doesn't //Make much sense here. Instead, we use the TimeStamp as the moment the feature //comes into existence. This works much better and gives a similar feel to //GE's experience. const when = JulianDate.fromIso8601(whenString); const result = new TimeIntervalCollection(); result.addInterval( new TimeInterval({ start: when, stop: Iso8601.MAXIMUM_VALUE, }) ); return result; } function processTimeSpan(featureNode) { const node = queryFirstNode(featureNode, "TimeSpan", namespaces.kmlgx); if (!defined(node)) { return undefined; } let result; const beginNode = queryFirstNode(node, "begin", namespaces.kmlgx); let beginDate = defined(beginNode) ? JulianDate.fromIso8601(beginNode.textContent) : undefined; const endNode = queryFirstNode(node, "end", namespaces.kmlgx); let endDate = defined(endNode) ? JulianDate.fromIso8601(endNode.textContent) : undefined; if (defined(beginDate) && defined(endDate)) { if (JulianDate.lessThan(endDate, beginDate)) { const tmp = beginDate; beginDate = endDate; endDate = tmp; } result = new TimeIntervalCollection(); result.addInterval( new TimeInterval({ start: beginDate, stop: endDate, }) ); } else if (defined(beginDate)) { result = new TimeIntervalCollection(); result.addInterval( new TimeInterval({ start: beginDate, stop: Iso8601.MAXIMUM_VALUE, }) ); } else if (defined(endDate)) { result = new TimeIntervalCollection(); result.addInterval( new TimeInterval({ start: Iso8601.MINIMUM_VALUE, stop: endDate, }) ); } return result; } function createDefaultBillboard() { const billboard = new BillboardGraphics(); billboard.width = BILLBOARD_SIZE; billboard.height = BILLBOARD_SIZE; billboard.scaleByDistance = new NearFarScalar( BILLBOARD_NEAR_DISTANCE, BILLBOARD_NEAR_RATIO, BILLBOARD_FAR_DISTANCE, BILLBOARD_FAR_RATIO ); billboard.pixelOffsetScaleByDistance = new NearFarScalar( BILLBOARD_NEAR_DISTANCE, BILLBOARD_NEAR_RATIO, BILLBOARD_FAR_DISTANCE, BILLBOARD_FAR_RATIO ); return billboard; } function createDefaultPolygon() { const polygon = new PolygonGraphics(); polygon.outline = true; polygon.outlineColor = Color.WHITE; return polygon; } function createDefaultLabel() { const label = new LabelGraphics(); label.translucencyByDistance = new NearFarScalar(3000000, 1.0, 5000000, 0.0); label.pixelOffset = new Cartesian2(17, 0); label.horizontalOrigin = HorizontalOrigin.LEFT; label.font = "16px sans-serif"; label.style = LabelStyle.FILL_AND_OUTLINE; return label; } function getIconHref( iconNode, dataSource, sourceResource, uriResolver, canRefresh ) { let href = queryStringValue(iconNode, "href", namespaces.kml); if (!defined(href) || href.length === 0) { return undefined; } if (href.indexOf("root://icons/palette-") === 0) { const palette = href.charAt(21); // Get the icon number let x = defaultValue(queryNumericValue(iconNode, "x", namespaces.gx), 0); let y = defaultValue(queryNumericValue(iconNode, "y", namespaces.gx), 0); x = Math.min(x / 32, 7); y = 7 - Math.min(y / 32, 7); const iconNum = 8 * y + x; href = `https://maps.google.com/mapfiles/kml/pal${palette}/icon${iconNum}.png`; } const hrefResource = resolveHref(href, sourceResource, uriResolver); if (canRefresh) { const refreshMode = queryStringValue( iconNode, "refreshMode", namespaces.kml ); const viewRefreshMode = queryStringValue( iconNode, "viewRefreshMode", namespaces.kml ); if (refreshMode === "onInterval" || refreshMode === "onExpire") { oneTimeWarning( `kml-refreshMode-${refreshMode}`, `KML - Unsupported Icon refreshMode: ${refreshMode}` ); } else if (viewRefreshMode === "onStop" || viewRefreshMode === "onRegion") { oneTimeWarning( `kml-refreshMode-${viewRefreshMode}`, `KML - Unsupported Icon viewRefreshMode: ${viewRefreshMode}` ); } const viewBoundScale = defaultValue( queryStringValue(iconNode, "viewBoundScale", namespaces.kml), 1.0 ); const defaultViewFormat = viewRefreshMode === "onStop" ? "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" : ""; const viewFormat = defaultValue( queryStringValue(iconNode, "viewFormat", namespaces.kml), defaultViewFormat ); const httpQuery = queryStringValue(iconNode, "httpQuery", namespaces.kml); if (defined(viewFormat)) { hrefResource.setQueryParameters(queryToObject(cleanupString(viewFormat))); } if (defined(httpQuery)) { hrefResource.setQueryParameters(queryToObject(cleanupString(httpQuery))); } const ellipsoid = dataSource._ellipsoid; processNetworkLinkQueryString( hrefResource, dataSource.camera, dataSource.canvas, viewBoundScale, dataSource._lastCameraView.bbox, ellipsoid ); return hrefResource; } return hrefResource; } function processBillboardIcon( dataSource, node, targetEntity, sourceResource, uriResolver ) { let scale = queryNumericValue(node, "scale", namespaces.kml); const heading = queryNumericValue(node, "heading", namespaces.kml); const color = queryColorValue(node, "color", namespaces.kml); const iconNode = queryFirstNode(node, "Icon", namespaces.kml); let icon = getIconHref( iconNode, dataSource, sourceResource, uriResolver, false ); // If icon tags are present but blank, we do not want to show an icon if (defined(iconNode) && !defined(icon)) { icon = false; } const x = queryNumericValue(iconNode, "x", namespaces.gx); const y = queryNumericValue(iconNode, "y", namespaces.gx); const w = queryNumericValue(iconNode, "w", namespaces.gx); const h = queryNumericValue(iconNode, "h", namespaces.gx); const hotSpotNode = queryFirstNode(node, "hotSpot", namespaces.kml); const hotSpotX = queryNumericAttribute(hotSpotNode, "x"); const hotSpotY = queryNumericAttribute(hotSpotNode, "y"); const hotSpotXUnit = queryStringAttribute(hotSpotNode, "xunits"); const hotSpotYUnit = queryStringAttribute(hotSpotNode, "yunits"); let billboard = targetEntity.billboard; if (!defined(billboard)) { billboard = createDefaultBillboard(); targetEntity.billboard = billboard; } billboard.image = icon; billboard.scale = scale; billboard.color = color; if (defined(x) || defined(y) || defined(w) || defined(h)) { billboard.imageSubRegion = new BoundingRectangle(x, y, w, h); } //GE treats a heading of zero as no heading //You can still point north using a 360 degree angle (or any multiple of 360) if (defined(heading) && heading !== 0) { billboard.rotation = CesiumMath.toRadians(-heading); billboard.alignedAxis = Cartesian3.UNIT_Z; } //Hotpot is the KML equivalent of pixel offset //The hotspot origin is the lower left, but we leave //our billboard origin at the center and simply //modify the pixel offset to take this into account scale = defaultValue(scale, 1.0); let xOffset; let yOffset; if (defined(hotSpotX)) { if (hotSpotXUnit === "pixels") { xOffset = -hotSpotX * scale; } else if (hotSpotXUnit === "insetPixels") { xOffset = (hotSpotX - BILLBOARD_SIZE) * scale; } else if (hotSpotXUnit === "fraction") { xOffset = -hotSpotX * BILLBOARD_SIZE * scale; } xOffset += BILLBOARD_SIZE * 0.5 * scale; } if (defined(hotSpotY)) { if (hotSpotYUnit === "pixels") { yOffset = hotSpotY * scale; } else if (hotSpotYUnit === "insetPixels") { yOffset = (-hotSpotY + BILLBOARD_SIZE) * scale; } else if (hotSpotYUnit === "fraction") { yOffset = hotSpotY * BILLBOARD_SIZE * scale; } yOffset -= BILLBOARD_SIZE * 0.5 * scale; } if (defined(xOffset) || defined(yOffset)) { billboard.pixelOffset = new Cartesian2(xOffset, yOffset); } } function applyStyle( dataSource, styleNode, targetEntity, sourceResource, uriResolver ) { for (let i = 0, len = styleNode.childNodes.length; i < len; i++) { const node = styleNode.childNodes.item(i); if (node.localName === "IconStyle") { processBillboardIcon( dataSource, node, targetEntity, sourceResource, uriResolver ); } else if (node.localName === "LabelStyle") { let label = targetEntity.label; if (!defined(label)) { label = createDefaultLabel(); targetEntity.label = label; } label.scale = defaultValue( queryNumericValue(node, "scale", namespaces.kml), label.scale ); label.fillColor = defaultValue( queryColorValue(node, "color", namespaces.kml), label.fillColor ); label.text = targetEntity.name; } else if (node.localName === "LineStyle") { let polyline = targetEntity.polyline; if (!defined(polyline)) { polyline = new PolylineGraphics(); targetEntity.polyline = polyline; } polyline.width = queryNumericValue(node, "width", namespaces.kml); polyline.material = queryColorValue(node, "color", namespaces.kml); if (defined(queryColorValue(node, "outerColor", namespaces.gx))) { oneTimeWarning( "kml-gx:outerColor", "KML - gx:outerColor is not supported in a LineStyle" ); } if (defined(queryNumericValue(node, "outerWidth", namespaces.gx))) { oneTimeWarning( "kml-gx:outerWidth", "KML - gx:outerWidth is not supported in a LineStyle" ); } if (defined(queryNumericValue(node, "physicalWidth", namespaces.gx))) { oneTimeWarning( "kml-gx:physicalWidth", "KML - gx:physicalWidth is not supported in a LineStyle" ); } if (defined(queryBooleanValue(node, "labelVisibility", namespaces.gx))) { oneTimeWarning( "kml-gx:labelVisibility", "KML - gx:labelVisibility is not supported in a LineStyle" ); } } else if (node.localName === "PolyStyle") { let polygon = targetEntity.polygon; if (!defined(polygon)) { polygon = createDefaultPolygon(); targetEntity.polygon = polygon; } polygon.material = defaultValue( queryColorValue(node, "color", namespaces.kml), polygon.material ); polygon.fill = defaultValue( queryBooleanValue(node, "fill", namespaces.kml), polygon.fill ); polygon.outline = defaultValue( queryBooleanValue(node, "outline", namespaces.kml), polygon.outline ); } else if (node.localName === "BalloonStyle") { const bgColor = defaultValue( parseColorString(queryStringValue(node, "bgColor", namespaces.kml)), Color.WHITE ); const textColor = defaultValue( parseColorString(queryStringValue(node, "textColor", namespaces.kml)), Color.BLACK ); const text = queryStringValue(node, "text", namespaces.kml); //This is purely an internal property used in style processing, //it never ends up on the final entity. targetEntity.addProperty("balloonStyle"); targetEntity.balloonStyle = { bgColor: bgColor, textColor: textColor, text: text, }; } else if (node.localName === "ListStyle") { const listItemType = queryStringValue( node, "listItemType", namespaces.kml ); if (listItemType === "radioFolder" || listItemType === "checkOffOnly") { oneTimeWarning( `kml-listStyle-${listItemType}`, `KML - Unsupported ListStyle with listItemType: ${listItemType}` ); } } } } //Processes and merges any inline styles for the provided node into the provided entity. function computeFinalStyle( dataSource, placeMark, styleCollection, sourceResource, uriResolver ) { const result = new Entity(); let styleEntity; //Google earth seems to always use the last inline Style/StyleMap only let styleIndex = -1; const childNodes = placeMark.childNodes; const length = childNodes.length; for (let q = 0; q < length; q++) { const child = childNodes[q]; if (child.localName === "Style" || child.localName === "StyleMap") { styleIndex = q; } } if (styleIndex !== -1) { const inlineStyleNode = childNodes[styleIndex]; if (inlineStyleNode.localName === "Style") { applyStyle( dataSource, inlineStyleNode, result, sourceResource, uriResolver ); } else { // StyleMap const pairs = queryChildNodes(inlineStyleNode, "Pair", namespaces.kml); for (let p = 0; p < pairs.length; p++) { const pair = pairs[p]; const key = queryStringValue(pair, "key", namespaces.kml); if (key === "normal") { const styleUrl = queryStringValue(pair, "styleUrl", namespaces.kml); if (defined(styleUrl)) { styleEntity = styleCollection.getById(styleUrl); if (!defined(styleEntity)) { styleEntity = styleCollection.getById(`#${styleUrl}`); } if (defined(styleEntity)) { result.merge(styleEntity); } } else { const node = queryFirstNode(pair, "Style", namespaces.kml); applyStyle(dataSource, node, result, sourceResource, uriResolver); } } else { oneTimeWarning( `kml-styleMap-${key}`, `KML - Unsupported StyleMap key: ${key}` ); } } } } //Google earth seems to always use the first external style only. const externalStyle = queryStringValue(placeMark, "styleUrl", namespaces.kml); if (defined(externalStyle)) { let id = externalStyle; if (externalStyle[0] !== "#" && externalStyle.indexOf("#") !== -1) { const tokens = externalStyle.split("#"); const uri = tokens[0]; const resource = sourceResource.getDerivedResource({ url: uri, }); id = `${resource.getUrlComponent()}#${tokens[1]}`; } styleEntity = styleCollection.getById(id); if (!defined(styleEntity)) { styleEntity = styleCollection.getById(`#${id}`); } if (defined(styleEntity)) { result.merge(styleEntity); } } return result; } //Asynchronously processes an external style file. function processExternalStyles(dataSource, resource, styleCollection) { return resource.fetchXML().then(function (styleKml) { return processStyles(dataSource, styleKml, styleCollection, resource, true); }); } //Processes all shared and external styles and stores //their id into the provided styleCollection. //Returns an array of promises that will resolve when //each style is loaded. function processStyles( dataSource, kml, styleCollection, sourceResource, isExternal, uriResolver ) { let i; let id; let styleEntity; let node; const styleNodes = queryNodes(kml, "Style", namespaces.kml); if (defined(styleNodes)) { const styleNodesLength = styleNodes.length; for (i = 0; i < styleNodesLength; i++) { node = styleNodes[i]; id = queryStringAttribute(node, "id"); if (defined(id)) { id = `#${id}`; if (isExternal && defined(sourceResource)) { id = sourceResource.getUrlComponent() + id; } if (!defined(styleCollection.getById(id))) { styleEntity = new Entity({ id: id, }); styleCollection.add(styleEntity); applyStyle( dataSource, node, styleEntity, sourceResource, uriResolver ); } } } } const styleMaps = queryNodes(kml, "StyleMap", namespaces.kml); if (defined(styleMaps)) { const styleMapsLength = styleMaps.length; for (i = 0; i < styleMapsLength; i++) { const styleMap = styleMaps[i]; id = queryStringAttribute(styleMap, "id"); if (defined(id)) { const pairs = queryChildNodes(styleMap, "Pair", namespaces.kml); for (let p = 0; p < pairs.length; p++) { const pair = pairs[p]; const key = queryStringValue(pair, "key", namespaces.kml); if (key === "normal") { id = `#${id}`; if (isExternal && defined(sourceResource)) { id = sourceResource.getUrlComponent() + id; } if (!defined(styleCollection.getById(id))) { styleEntity = styleCollection.getOrCreateEntity(id); let styleUrl = queryStringValue(pair, "styleUrl", namespaces.kml); if (defined(styleUrl)) { if (styleUrl[0] !== "#") { styleUrl = `#${styleUrl}`; } if (isExternal && defined(sourceResource)) { styleUrl = sourceResource.getUrlComponent() + styleUrl; } const base = styleCollection.getById(styleUrl); if (defined(base)) { styleEntity.merge(base); } } else { node = queryFirstNode(pair, "Style", namespaces.kml); applyStyle( dataSource, node, styleEntity, sourceResource, uriResolver ); } } } else { oneTimeWarning( `kml-styleMap-${key}`, `KML - Unsupported StyleMap key: ${key}` ); } } } } } const promises = []; const styleUrlNodes = kml.getElementsByTagName("styleUrl"); const styleUrlNodesLength = styleUrlNodes.length; for (i = 0; i < styleUrlNodesLength; i++) { const styleReference = styleUrlNodes[i].textContent; if (styleReference[0] !== "#") { //According to the spec, all local styles should start with a # //and everything else is an external style that has a # seperating //the URL of the document and the style. However, Google Earth //also accepts styleUrls without a # as meaning a local style. const tokens = styleReference.split("#"); if (tokens.length === 2) { const uri = tokens[0]; const resource = sourceResource.getDerivedResource({ url: uri, }); promises.push( processExternalStyles(dataSource, resource, styleCollection) ); } } } return promises; } function createDropLine(entityCollection, entity, styleEntity) { const entityPosition = new ReferenceProperty(entityCollection, entity.id, [ "position", ]); const surfacePosition = new ScaledPositionProperty(entity.position); entity.polyline = defined(styleEntity.polyline) ? styleEntity.polyline.clone() : new PolylineGraphics(); entity.polyline.positions = new PositionPropertyArray([ entityPosition, surfacePosition, ]); } function heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode) { if ( (!defined(altitudeMode) && !defined(gxAltitudeMode)) || altitudeMode === "clampToGround" ) { return HeightReference.CLAMP_TO_GROUND; } if (altitudeMode === "relativeToGround") { return HeightReference.RELATIVE_TO_GROUND; } if (altitudeMode === "absolute") { return HeightReference.NONE; } if (gxAltitudeMode === "clampToSeaFloor") { oneTimeWarning( "kml-gx:altitudeMode-clampToSeaFloor", "KML - :clampToSeaFloor is currently not supported, using :clampToGround." ); return HeightReference.CLAMP_TO_GROUND; } if (gxAltitudeMode === "relativeToSeaFloor") { oneTimeWarning( "kml-gx:altitudeMode-relativeToSeaFloor", "KML - :relativeToSeaFloor is currently not supported, using :relativeToGround." ); return HeightReference.RELATIVE_TO_GROUND; } if (defined(altitudeMode)) { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown :${altitudeMode}, using :CLAMP_TO_GROUND.` ); } else { oneTimeWarning( "kml-gx:altitudeMode-unknown", `KML - Unknown :${gxAltitudeMode}, using :CLAMP_TO_GROUND.` ); } // Clamp to ground is the default return HeightReference.CLAMP_TO_GROUND; } function createPositionPropertyFromAltitudeMode( property, altitudeMode, gxAltitudeMode ) { if ( gxAltitudeMode === "relativeToSeaFloor" || altitudeMode === "absolute" || altitudeMode === "relativeToGround" ) { //Just return the ellipsoid referenced property until we support MSL return property; } if ( (defined(altitudeMode) && altitudeMode !== "clampToGround") || // (defined(gxAltitudeMode) && gxAltitudeMode !== "clampToSeaFloor") ) { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown altitudeMode: ${defaultValue( altitudeMode, gxAltitudeMode )}` ); } // Clamp to ground is the default return new ScaledPositionProperty(property); } function createPositionPropertyArrayFromAltitudeMode( properties, altitudeMode, gxAltitudeMode, ellipsoid ) { if (!defined(properties)) { return undefined; } if ( gxAltitudeMode === "relativeToSeaFloor" || altitudeMode === "absolute" || altitudeMode === "relativeToGround" ) { //Just return the ellipsoid referenced property until we support MSL return properties; } if ( (defined(altitudeMode) && altitudeMode !== "clampToGround") || // (defined(gxAltitudeMode) && gxAltitudeMode !== "clampToSeaFloor") ) { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown altitudeMode: ${defaultValue( altitudeMode, gxAltitudeMode )}` ); } // Clamp to ground is the default const propertiesLength = properties.length; for (let i = 0; i < propertiesLength; i++) { const property = properties[i]; ellipsoid.scaleToGeodeticSurface(property, property); } return properties; } function processPositionGraphics( dataSource, entity, styleEntity, heightReference ) { let label = entity.label; if (!defined(label)) { label = defined(styleEntity.label) ? styleEntity.label.clone() : createDefaultLabel(); entity.label = label; } label.text = entity.name; let billboard = entity.billboard; if (!defined(billboard)) { billboard = defined(styleEntity.billboard) ? styleEntity.billboard.clone() : createDefaultBillboard(); entity.billboard = billboard; } if (!defined(billboard.image)) { billboard.image = dataSource._pinBuilder.fromColor(Color.YELLOW, 64); // If there were empty tags in the KML, then billboard.image was set to false above // However, in this case, the false value would have been converted to a property afterwards // Thus, we check if billboard.image is defined with value of false } else if (!billboard.image.getValue()) { billboard.image = undefined; } let scale = 1.0; if (defined(billboard.scale)) { scale = billboard.scale.getValue(); if (scale !== 0) { label.pixelOffset = new Cartesian2(scale * 16 + 1, 0); } else { //Minor tweaks to better match Google Earth. label.pixelOffset = undefined; label.horizontalOrigin = undefined; } } if (defined(heightReference) && dataSource._clampToGround) { billboard.heightReference = heightReference; label.heightReference = heightReference; } } function processPathGraphics(entity, styleEntity) { let path = entity.path; if (!defined(path)) { path = new PathGraphics(); path.leadTime = 0; entity.path = path; } const polyline = styleEntity.polyline; if (defined(polyline)) { path.material = polyline.material; path.width = polyline.width; } } function processPoint( dataSource, entityCollection, geometryNode, entity, styleEntity ) { const coordinatesString = queryStringValue( geometryNode, "coordinates", namespaces.kml ); const altitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.kml ); const gxAltitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.gx ); const extrude = queryBooleanValue(geometryNode, "extrude", namespaces.kml); const ellipsoid = dataSource._ellipsoid; const position = readCoordinate(coordinatesString, ellipsoid); entity.position = position; processPositionGraphics( dataSource, entity, styleEntity, heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode) ); if (extrude && isExtrudable(altitudeMode, gxAltitudeMode)) { createDropLine(entityCollection, entity, styleEntity); } return true; } function processLineStringOrLinearRing( dataSource, entityCollection, geometryNode, entity, styleEntity ) { const coordinatesNode = queryFirstNode( geometryNode, "coordinates", namespaces.kml ); const altitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.kml ); const gxAltitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.gx ); const extrude = queryBooleanValue(geometryNode, "extrude", namespaces.kml); const tessellate = queryBooleanValue( geometryNode, "tessellate", namespaces.kml ); const canExtrude = isExtrudable(altitudeMode, gxAltitudeMode); const zIndex = queryNumericValue(geometryNode, "drawOrder", namespaces.gx); const ellipsoid = dataSource._ellipsoid; const coordinates = readCoordinates(coordinatesNode, ellipsoid); let polyline = styleEntity.polyline; if (canExtrude && extrude) { const wall = new WallGraphics(); entity.wall = wall; wall.positions = coordinates; const polygon = styleEntity.polygon; if (defined(polygon)) { wall.fill = polygon.fill; wall.material = polygon.material; } //Always outline walls so they show up in 2D. wall.outline = true; if (defined(polyline)) { wall.outlineColor = defined(polyline.material) ? polyline.material.color : Color.WHITE; wall.outlineWidth = polyline.width; } else if (defined(polygon)) { wall.outlineColor = defined(polygon.material) ? polygon.material.color : Color.WHITE; } } else if (dataSource._clampToGround && !canExtrude && tessellate) { const polylineGraphics = new PolylineGraphics(); polylineGraphics.clampToGround = true; entity.polyline = polylineGraphics; polylineGraphics.positions = coordinates; if (defined(polyline)) { polylineGraphics.material = defined(polyline.material) ? polyline.material.color.getValue(Iso8601.MINIMUM_VALUE) : Color.WHITE; polylineGraphics.width = defaultValue(polyline.width, 1.0); } else { polylineGraphics.material = Color.WHITE; polylineGraphics.width = 1.0; } polylineGraphics.zIndex = zIndex; } else { if (defined(zIndex)) { oneTimeWarning( "kml-gx:drawOrder", "KML - gx:drawOrder is not supported in LineStrings when clampToGround is false" ); } if (dataSource._clampToGround && !tessellate) { oneTimeWarning( "kml-line-tesselate", "Ignoring clampToGround for KML lines without the tessellate flag." ); } polyline = defined(polyline) ? polyline.clone() : new PolylineGraphics(); entity.polyline = polyline; polyline.positions = createPositionPropertyArrayFromAltitudeMode( coordinates, altitudeMode, gxAltitudeMode, ellipsoid ); if (!tessellate || canExtrude) { polyline.arcType = ArcType.NONE; } } return true; } function processPolygon( dataSource, entityCollection, geometryNode, entity, styleEntity ) { const outerBoundaryIsNode = queryFirstNode( geometryNode, "outerBoundaryIs", namespaces.kml ); let linearRingNode = queryFirstNode( outerBoundaryIsNode, "LinearRing", namespaces.kml ); let coordinatesNode = queryFirstNode( linearRingNode, "coordinates", namespaces.kml ); const ellipsoid = dataSource._ellipsoid; let coordinates = readCoordinates(coordinatesNode, ellipsoid); const extrude = queryBooleanValue(geometryNode, "extrude", namespaces.kml); const altitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.kml ); const gxAltitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.gx ); const canExtrude = isExtrudable(altitudeMode, gxAltitudeMode); const polygon = defined(styleEntity.polygon) ? styleEntity.polygon.clone() : createDefaultPolygon(); const polyline = styleEntity.polyline; if (defined(polyline)) { polygon.outlineColor = defined(polyline.material) ? polyline.material.color : Color.WHITE; polygon.outlineWidth = polyline.width; } entity.polygon = polygon; if (canExtrude) { polygon.perPositionHeight = true; polygon.extrudedHeight = extrude ? 0 : undefined; } else if (!dataSource._clampToGround) { polygon.height = 0; } if (defined(coordinates)) { const hierarchy = new PolygonHierarchy(coordinates); const innerBoundaryIsNodes = queryChildNodes( geometryNode, "innerBoundaryIs", namespaces.kml ); for (let j = 0; j < innerBoundaryIsNodes.length; j++) { linearRingNode = queryChildNodes( innerBoundaryIsNodes[j], "LinearRing", namespaces.kml ); for (let k = 0; k < linearRingNode.length; k++) { coordinatesNode = queryFirstNode( linearRingNode[k], "coordinates", namespaces.kml ); coordinates = readCoordinates(coordinatesNode, ellipsoid); if (defined(coordinates)) { hierarchy.holes.push(new PolygonHierarchy(coordinates)); } } } polygon.hierarchy = hierarchy; } return true; } function processTrack( dataSource, entityCollection, geometryNode, entity, styleEntity ) { const altitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.kml ); const gxAltitudeMode = queryStringValue( geometryNode, "altitudeMode", namespaces.gx ); const coordNodes = queryChildNodes(geometryNode, "coord", namespaces.gx); const angleNodes = queryChildNodes(geometryNode, "angles", namespaces.gx); const timeNodes = queryChildNodes(geometryNode, "when", namespaces.kml); const extrude = queryBooleanValue(geometryNode, "extrude", namespaces.kml); const canExtrude = isExtrudable(altitudeMode, gxAltitudeMode); const ellipsoid = dataSource._ellipsoid; if (angleNodes.length > 0) { oneTimeWarning( "kml-gx:angles", "KML - gx:angles are not supported in gx:Tracks" ); } const length = Math.min(coordNodes.length, timeNodes.length); const coordinates = []; const times = []; for (let i = 0; i < length; i++) { const position = readCoordinate(coordNodes[i].textContent, ellipsoid); coordinates.push(position); times.push(JulianDate.fromIso8601(timeNodes[i].textContent)); } const property = new SampledPositionProperty(); property.addSamples(times, coordinates); entity.position = property; processPositionGraphics( dataSource, entity, styleEntity, heightReferenceFromAltitudeMode(altitudeMode, gxAltitudeMode) ); processPathGraphics(entity, styleEntity); entity.availability = new TimeIntervalCollection(); if (timeNodes.length > 0) { entity.availability.addInterval( new TimeInterval({ start: times[0], stop: times[times.length - 1], }) ); } if (canExtrude && extrude) { createDropLine(entityCollection, entity, styleEntity); } return true; } function addToMultiTrack( times, positions, composite, availability, dropShowProperty, extrude, altitudeMode, gxAltitudeMode, includeEndPoints ) { const start = times[0]; const stop = times[times.length - 1]; const data = new SampledPositionProperty(); data.addSamples(times, positions); composite.intervals.addInterval( new TimeInterval({ start: start, stop: stop, isStartIncluded: includeEndPoints, isStopIncluded: includeEndPoints, data: createPositionPropertyFromAltitudeMode( data, altitudeMode, gxAltitudeMode ), }) ); availability.addInterval( new TimeInterval({ start: start, stop: stop, isStartIncluded: includeEndPoints, isStopIncluded: includeEndPoints, }) ); dropShowProperty.intervals.addInterval( new TimeInterval({ start: start, stop: stop, isStartIncluded: includeEndPoints, isStopIncluded: includeEndPoints, data: extrude, }) ); } function processMultiTrack( dataSource, entityCollection, geometryNode, entity, styleEntity ) { // Multitrack options do not work in GE as detailed in the spec, // rather than altitudeMode being at the MultiTrack level, // GE just defers all settings to the underlying track. const interpolate = queryBooleanValue( geometryNode, "interpolate", namespaces.gx ); const trackNodes = queryChildNodes(geometryNode, "Track", namespaces.gx); let times; let lastStop; let lastStopPosition; let needDropLine = false; const dropShowProperty = new TimeIntervalCollectionProperty(); const availability = new TimeIntervalCollection(); const composite = new CompositePositionProperty(); const ellipsoid = dataSource._ellipsoid; for (let i = 0, len = trackNodes.length; i < len; i++) { const trackNode = trackNodes[i]; const timeNodes = queryChildNodes(trackNode, "when", namespaces.kml); const coordNodes = queryChildNodes(trackNode, "coord", namespaces.gx); const altitudeMode = queryStringValue( trackNode, "altitudeMode", namespaces.kml ); const gxAltitudeMode = queryStringValue( trackNode, "altitudeMode", namespaces.gx ); const canExtrude = isExtrudable(altitudeMode, gxAltitudeMode); const extrude = queryBooleanValue(trackNode, "extrude", namespaces.kml); const length = Math.min(coordNodes.length, timeNodes.length); const positions = []; times = []; for (let x = 0; x < length; x++) { const position = readCoordinate(coordNodes[x].textContent, ellipsoid); positions.push(position); times.push(JulianDate.fromIso8601(timeNodes[x].textContent)); } if (interpolate) { //If we are interpolating, then we need to fill in the end of //the last track and the beginning of this one with a sampled //property. From testing in Google Earth, this property //is never extruded and always absolute. if (defined(lastStop)) { addToMultiTrack( [lastStop, times[0]], [lastStopPosition, positions[0]], composite, availability, dropShowProperty, false, "absolute", undefined, false ); } lastStop = times[length - 1]; lastStopPosition = positions[positions.length - 1]; } addToMultiTrack( times, positions, composite, availability, dropShowProperty, canExtrude && extrude, altitudeMode, gxAltitudeMode, true ); needDropLine = needDropLine || (canExtrude && extrude); } entity.availability = availability; entity.position = composite; processPositionGraphics(dataSource, entity, styleEntity); processPathGraphics(entity, styleEntity); if (needDropLine) { createDropLine(entityCollection, entity, styleEntity); entity.polyline.show = dropShowProperty; } return true; } const geometryTypes = { Point: processPoint, LineString: processLineStringOrLinearRing, LinearRing: processLineStringOrLinearRing, Polygon: processPolygon, Track: processTrack, MultiTrack: processMultiTrack, MultiGeometry: processMultiGeometry, Model: processUnsupportedGeometry, }; function processMultiGeometry( dataSource, entityCollection, geometryNode, entity, styleEntity, context ) { const childNodes = geometryNode.childNodes; let hasGeometry = false; for (let i = 0, len = childNodes.length; i < len; i++) { const childNode = childNodes.item(i); const geometryProcessor = geometryTypes[childNode.localName]; if (defined(geometryProcessor)) { const childEntity = createEntity(childNode, entityCollection, context); childEntity.parent = entity; childEntity.name = entity.name; childEntity.availability = entity.availability; childEntity.description = entity.description; childEntity.kml = entity.kml; if ( geometryProcessor( dataSource, entityCollection, childNode, childEntity, styleEntity ) ) { hasGeometry = true; } } } return hasGeometry; } function processUnsupportedGeometry( dataSource, entityCollection, geometryNode, entity, styleEntity ) { oneTimeWarning( "kml-unsupportedGeometry", `KML - Unsupported geometry: ${geometryNode.localName}` ); return false; } function processExtendedData(node, entity) { const extendedDataNode = queryFirstNode(node, "ExtendedData", namespaces.kml); if (!defined(extendedDataNode)) { return undefined; } if (defined(queryFirstNode(extendedDataNode, "SchemaData", namespaces.kml))) { oneTimeWarning("kml-schemaData", "KML - SchemaData is unsupported"); } if (defined(queryStringAttribute(extendedDataNode, "xmlns:prefix"))) { oneTimeWarning( "kml-extendedData", "KML - ExtendedData with xmlns:prefix is unsupported" ); } const result = {}; const dataNodes = queryChildNodes(extendedDataNode, "Data", namespaces.kml); if (defined(dataNodes)) { const length = dataNodes.length; for (let i = 0; i < length; i++) { const dataNode = dataNodes[i]; const name = queryStringAttribute(dataNode, "name"); if (defined(name)) { result[name] = { displayName: queryStringValue( dataNode, "displayName", namespaces.kml ), value: queryStringValue(dataNode, "value", namespaces.kml), }; } } } entity.kml.extendedData = result; } let scratchDiv; if (typeof document !== "undefined") { scratchDiv = document.createElement("div"); } function processDescription( node, entity, styleEntity, uriResolver, sourceResource ) { let i; let key; let keys; const kmlData = entity.kml; const extendedData = kmlData.extendedData; const description = queryStringValue(node, "description", namespaces.kml); const balloonStyle = defaultValue( entity.balloonStyle, styleEntity.balloonStyle ); let background = Color.WHITE; let foreground = Color.BLACK; let text = description; if (defined(balloonStyle)) { background = defaultValue(balloonStyle.bgColor, Color.WHITE); foreground = defaultValue(balloonStyle.textColor, Color.BLACK); text = defaultValue(balloonStyle.text, description); } let value; if (defined(text)) { text = text.replace("$[name]", defaultValue(entity.name, "")); text = text.replace("$[description]", defaultValue(description, "")); text = text.replace("$[address]", defaultValue(kmlData.address, "")); text = text.replace("$[Snippet]", defaultValue(kmlData.snippet, "")); text = text.replace("$[id]", entity.id); //While not explicitly defined by the OGC spec, in Google Earth //The appearance of geDirections adds the directions to/from links //We simply replace this string with nothing. text = text.replace("$[geDirections]", ""); if (defined(extendedData)) { const matches = text.match(/\$\[.+?\]/g); if (matches !== null) { for (i = 0; i < matches.length; i++) { const token = matches[i]; let propertyName = token.substr(2, token.length - 3); const isDisplayName = /\/displayName$/.test(propertyName); propertyName = propertyName.replace(/\/displayName$/, ""); value = extendedData[propertyName]; if (defined(value)) { value = isDisplayName ? value.displayName : value.value; } if (defined(value)) { text = text.replace(token, defaultValue(value, "")); } } } } } else if (defined(extendedData)) { //If no description exists, build a table out of the extended data keys = Object.keys(extendedData); if (keys.length > 0) { text = ''; for (i = 0; i < keys.length; i++) { key = keys[i]; value = extendedData[key]; text += ``; } text += "
${defaultValue( value.displayName, key )}${defaultValue(value.value, "")}
"; } } if (!defined(text)) { //No description return; } //Turns non-explicit links into clickable links. text = autolinker.link(text); //Use a temporary div to manipulate the links //so that they open in a new window. scratchDiv.innerHTML = text; const links = scratchDiv.querySelectorAll("a"); for (i = 0; i < links.length; i++) { links[i].setAttribute("target", "_blank"); } //Rewrite any KMZ embedded urls if (defined(uriResolver) && uriResolver.keys.length > 1) { embedDataUris(scratchDiv, "a", "href", uriResolver); embedDataUris(scratchDiv, "link", "href", uriResolver); embedDataUris(scratchDiv, "area", "href", uriResolver); embedDataUris(scratchDiv, "img", "src", uriResolver); embedDataUris(scratchDiv, "iframe", "src", uriResolver); embedDataUris(scratchDiv, "video", "src", uriResolver); embedDataUris(scratchDiv, "audio", "src", uriResolver); embedDataUris(scratchDiv, "source", "src", uriResolver); embedDataUris(scratchDiv, "track", "src", uriResolver); embedDataUris(scratchDiv, "input", "src", uriResolver); embedDataUris(scratchDiv, "embed", "src", uriResolver); embedDataUris(scratchDiv, "script", "src", uriResolver); embedDataUris(scratchDiv, "video", "poster", uriResolver); } //Make relative urls absolute using the sourceResource applyBasePath(scratchDiv, "a", "href", sourceResource); applyBasePath(scratchDiv, "link", "href", sourceResource); applyBasePath(scratchDiv, "area", "href", sourceResource); applyBasePath(scratchDiv, "img", "src", sourceResource); applyBasePath(scratchDiv, "iframe", "src", sourceResource); applyBasePath(scratchDiv, "video", "src", sourceResource); applyBasePath(scratchDiv, "audio", "src", sourceResource); applyBasePath(scratchDiv, "source", "src", sourceResource); applyBasePath(scratchDiv, "track", "src", sourceResource); applyBasePath(scratchDiv, "input", "src", sourceResource); applyBasePath(scratchDiv, "embed", "src", sourceResource); applyBasePath(scratchDiv, "script", "src", sourceResource); applyBasePath(scratchDiv, "video", "poster", sourceResource); let tmp = '
`; scratchDiv.innerHTML = ""; //Set the final HTML as the description. entity.description = tmp; } function processFeature(dataSource, featureNode, processingData) { const entityCollection = processingData.entityCollection; const parent = processingData.parentEntity; const sourceResource = processingData.sourceResource; const uriResolver = processingData.uriResolver; const entity = createEntity( featureNode, entityCollection, processingData.context ); const kmlData = entity.kml; const styleEntity = computeFinalStyle( dataSource, featureNode, processingData.styleCollection, sourceResource, uriResolver ); const name = queryStringValue(featureNode, "name", namespaces.kml); entity.name = name; entity.parent = parent; let availability = processTimeSpan(featureNode); if (!defined(availability)) { availability = processTimeStamp(featureNode); } entity.availability = availability; mergeAvailabilityWithParent(entity); // Per KML spec "A Feature is visible only if it and all its ancestors are visible." function ancestryIsVisible(parentEntity) { if (!parentEntity) { return true; } return parentEntity.show && ancestryIsVisible(parentEntity.parent); } const visibility = queryBooleanValue( featureNode, "visibility", namespaces.kml ); entity.show = ancestryIsVisible(parent) && defaultValue(visibility, true); //const open = queryBooleanValue(featureNode, 'open', namespaces.kml); const authorNode = queryFirstNode(featureNode, "author", namespaces.atom); const author = kmlData.author; author.name = queryStringValue(authorNode, "name", namespaces.atom); author.uri = queryStringValue(authorNode, "uri", namespaces.atom); author.email = queryStringValue(authorNode, "email", namespaces.atom); const linkNode = queryFirstNode(featureNode, "link", namespaces.atom); const link = kmlData.link; link.href = queryStringAttribute(linkNode, "href"); link.hreflang = queryStringAttribute(linkNode, "hreflang"); link.rel = queryStringAttribute(linkNode, "rel"); link.type = queryStringAttribute(linkNode, "type"); link.title = queryStringAttribute(linkNode, "title"); link.length = queryStringAttribute(linkNode, "length"); kmlData.address = queryStringValue(featureNode, "address", namespaces.kml); kmlData.phoneNumber = queryStringValue( featureNode, "phoneNumber", namespaces.kml ); kmlData.snippet = queryStringValue(featureNode, "Snippet", namespaces.kml); processExtendedData(featureNode, entity); processDescription( featureNode, entity, styleEntity, uriResolver, sourceResource ); const ellipsoid = dataSource._ellipsoid; processLookAt(featureNode, entity, ellipsoid); processCamera(featureNode, entity, ellipsoid); if (defined(queryFirstNode(featureNode, "Region", namespaces.kml))) { oneTimeWarning("kml-region", "KML - Placemark Regions are unsupported"); } return { entity: entity, styleEntity: styleEntity, }; } function processDocument(dataSource, node, processingData, deferredLoading) { deferredLoading.addNodes(node.childNodes, processingData); deferredLoading.process(); } function processFolder(dataSource, node, processingData, deferredLoading) { const r = processFeature(dataSource, node, processingData); const newProcessingData = clone(processingData); newProcessingData.parentEntity = r.entity; processDocument(dataSource, node, newProcessingData, deferredLoading); } function processPlacemark( dataSource, placemark, processingData, deferredLoading ) { const r = processFeature(dataSource, placemark, processingData); const entity = r.entity; const styleEntity = r.styleEntity; let hasGeometry = false; const childNodes = placemark.childNodes; for (let i = 0, len = childNodes.length; i < len && !hasGeometry; i++) { const childNode = childNodes.item(i); const geometryProcessor = geometryTypes[childNode.localName]; if (defined(geometryProcessor)) { // pass the placemark entity id as a context for case of defining multiple child entities together to handle case // where some malformed kmls reuse the same id across placemarks, which works in GE, but is not technically to spec. geometryProcessor( dataSource, processingData.entityCollection, childNode, entity, styleEntity, entity.id ); hasGeometry = true; } } if (!hasGeometry) { entity.merge(styleEntity); processPositionGraphics(dataSource, entity, styleEntity); } } const playlistNodeProcessors = { FlyTo: processTourFlyTo, Wait: processTourWait, SoundCue: processTourUnsupportedNode, AnimatedUpdate: processTourUnsupportedNode, TourControl: processTourUnsupportedNode, }; function processTour(dataSource, node, processingData, deferredLoading) { const name = queryStringValue(node, "name", namespaces.kml); const id = queryStringAttribute(node, "id"); const tour = new KmlTour(name, id); const playlistNode = queryFirstNode(node, "Playlist", namespaces.gx); if (playlistNode) { const ellipsoid = dataSource._ellipsoid; const childNodes = playlistNode.childNodes; for (let i = 0; i < childNodes.length; i++) { const entryNode = childNodes[i]; if (entryNode.localName) { const playlistNodeProcessor = playlistNodeProcessors[entryNode.localName]; if (playlistNodeProcessor) { playlistNodeProcessor(tour, entryNode, ellipsoid); } else { console.log( `Unknown KML Tour playlist entry type ${entryNode.localName}` ); } } } } dataSource._kmlTours.push(tour); } function processTourUnsupportedNode(tour, entryNode) { oneTimeWarning(`KML Tour unsupported node ${entryNode.localName}`); } function processTourWait(tour, entryNode) { const duration = queryNumericValue(entryNode, "duration", namespaces.gx); tour.addPlaylistEntry(new KmlTourWait(duration)); } function processTourFlyTo(tour, entryNode, ellipsoid) { const duration = queryNumericValue(entryNode, "duration", namespaces.gx); const flyToMode = queryStringValue(entryNode, "flyToMode", namespaces.gx); const t = { kml: {} }; processLookAt(entryNode, t, ellipsoid); processCamera(entryNode, t, ellipsoid); const view = t.kml.lookAt || t.kml.camera; const flyto = new KmlTourFlyTo(duration, flyToMode, view); tour.addPlaylistEntry(flyto); } function processCamera(featureNode, entity, ellipsoid) { const camera = queryFirstNode(featureNode, "Camera", namespaces.kml); if (defined(camera)) { const lon = defaultValue( queryNumericValue(camera, "longitude", namespaces.kml), 0.0 ); const lat = defaultValue( queryNumericValue(camera, "latitude", namespaces.kml), 0.0 ); const altitude = defaultValue( queryNumericValue(camera, "altitude", namespaces.kml), 0.0 ); const heading = defaultValue( queryNumericValue(camera, "heading", namespaces.kml), 0.0 ); const tilt = defaultValue( queryNumericValue(camera, "tilt", namespaces.kml), 0.0 ); const roll = defaultValue( queryNumericValue(camera, "roll", namespaces.kml), 0.0 ); const position = Cartesian3.fromDegrees(lon, lat, altitude, ellipsoid); const hpr = HeadingPitchRoll.fromDegrees(heading, tilt - 90.0, roll); entity.kml.camera = new KmlCamera(position, hpr); } } function processLookAt(featureNode, entity, ellipsoid) { const lookAt = queryFirstNode(featureNode, "LookAt", namespaces.kml); if (defined(lookAt)) { const lon = defaultValue( queryNumericValue(lookAt, "longitude", namespaces.kml), 0.0 ); const lat = defaultValue( queryNumericValue(lookAt, "latitude", namespaces.kml), 0.0 ); const altitude = defaultValue( queryNumericValue(lookAt, "altitude", namespaces.kml), 0.0 ); let heading = queryNumericValue(lookAt, "heading", namespaces.kml); let tilt = queryNumericValue(lookAt, "tilt", namespaces.kml); const range = defaultValue( queryNumericValue(lookAt, "range", namespaces.kml), 0.0 ); tilt = CesiumMath.toRadians(defaultValue(tilt, 0.0)); heading = CesiumMath.toRadians(defaultValue(heading, 0.0)); const hpr = new HeadingPitchRange( heading, tilt - CesiumMath.PI_OVER_TWO, range ); const viewPoint = Cartesian3.fromDegrees(lon, lat, altitude, ellipsoid); entity.kml.lookAt = new KmlLookAt(viewPoint, hpr); } } function processScreenOverlay( dataSource, screenOverlayNode, processingData, deferredLoading ) { const screenOverlay = processingData.screenOverlayContainer; if (!defined(screenOverlay)) { return undefined; } const sourceResource = processingData.sourceResource; const uriResolver = processingData.uriResolver; const iconNode = queryFirstNode(screenOverlayNode, "Icon", namespaces.kml); const icon = getIconHref( iconNode, dataSource, sourceResource, uriResolver, false ); if (!defined(icon)) { return undefined; } const img = document.createElement("img"); dataSource._screenOverlays.push(img); img.src = icon.url; img.onload = function () { const styles = ["position: absolute"]; const screenXY = queryFirstNode( screenOverlayNode, "screenXY", namespaces.kml ); const overlayXY = queryFirstNode( screenOverlayNode, "overlayXY", namespaces.kml ); const size = queryFirstNode(screenOverlayNode, "size", namespaces.kml); let x, y; let xUnit, yUnit; let xStyle, yStyle; if (defined(size)) { x = queryNumericAttribute(size, "x"); y = queryNumericAttribute(size, "y"); xUnit = queryStringAttribute(size, "xunits"); yUnit = queryStringAttribute(size, "yunits"); if (defined(x) && x !== -1 && x !== 0) { if (xUnit === "fraction") { xStyle = `width: ${Math.floor(x * 100)}%`; } else if (xUnit === "pixels") { xStyle = `width: ${x}px`; } styles.push(xStyle); } if (defined(y) && y !== -1 && y !== 0) { if (yUnit === "fraction") { yStyle = `height: ${Math.floor(y * 100)}%`; } else if (yUnit === "pixels") { yStyle = `height: ${y}px`; } styles.push(yStyle); } } // set the interim style so the width/height properties get calculated img.style = styles.join(";"); let xOrigin = 0; let yOrigin = img.height; if (defined(overlayXY)) { x = queryNumericAttribute(overlayXY, "x"); y = queryNumericAttribute(overlayXY, "y"); xUnit = queryStringAttribute(overlayXY, "xunits"); yUnit = queryStringAttribute(overlayXY, "yunits"); if (defined(x)) { if (xUnit === "fraction") { xOrigin = x * img.width; } else if (xUnit === "pixels") { xOrigin = x; } else if (xUnit === "insetPixels") { xOrigin = x; } } if (defined(y)) { if (yUnit === "fraction") { yOrigin = y * img.height; } else if (yUnit === "pixels") { yOrigin = y; } else if (yUnit === "insetPixels") { yOrigin = y; } } } if (defined(screenXY)) { x = queryNumericAttribute(screenXY, "x"); y = queryNumericAttribute(screenXY, "y"); xUnit = queryStringAttribute(screenXY, "xunits"); yUnit = queryStringAttribute(screenXY, "yunits"); if (defined(x)) { if (xUnit === "fraction") { xStyle = `${"left: " + "calc("}${Math.floor( x * 100 )}% - ${xOrigin}px)`; } else if (xUnit === "pixels") { xStyle = `left: ${x - xOrigin}px`; } else if (xUnit === "insetPixels") { xStyle = `right: ${x - xOrigin}px`; } styles.push(xStyle); } if (defined(y)) { if (yUnit === "fraction") { yStyle = `${"bottom: " + "calc("}${Math.floor( y * 100 )}% - ${yOrigin}px)`; } else if (yUnit === "pixels") { yStyle = `bottom: ${y - yOrigin}px`; } else if (yUnit === "insetPixels") { yStyle = `top: ${y - yOrigin}px`; } styles.push(yStyle); } } img.style = styles.join(";"); }; screenOverlay.appendChild(img); } function processGroundOverlay( dataSource, groundOverlay, processingData, deferredLoading ) { const r = processFeature(dataSource, groundOverlay, processingData); const entity = r.entity; let geometry; let isLatLonQuad = false; const ellipsoid = dataSource._ellipsoid; const positions = readCoordinates( queryFirstNode(groundOverlay, "LatLonQuad", namespaces.gx), ellipsoid ); const zIndex = queryNumericValue(groundOverlay, "drawOrder", namespaces.kml); if (defined(positions)) { geometry = createDefaultPolygon(); geometry.hierarchy = new PolygonHierarchy(positions); geometry.zIndex = zIndex; entity.polygon = geometry; isLatLonQuad = true; } else { geometry = new RectangleGraphics(); geometry.zIndex = zIndex; entity.rectangle = geometry; const latLonBox = queryFirstNode( groundOverlay, "LatLonBox", namespaces.kml ); if (defined(latLonBox)) { let west = queryNumericValue(latLonBox, "west", namespaces.kml); let south = queryNumericValue(latLonBox, "south", namespaces.kml); let east = queryNumericValue(latLonBox, "east", namespaces.kml); let north = queryNumericValue(latLonBox, "north", namespaces.kml); if (defined(west)) { west = CesiumMath.negativePiToPi(CesiumMath.toRadians(west)); } if (defined(south)) { south = CesiumMath.clampToLatitudeRange(CesiumMath.toRadians(south)); } if (defined(east)) { east = CesiumMath.negativePiToPi(CesiumMath.toRadians(east)); } if (defined(north)) { north = CesiumMath.clampToLatitudeRange(CesiumMath.toRadians(north)); } geometry.coordinates = new Rectangle(west, south, east, north); const rotation = queryNumericValue(latLonBox, "rotation", namespaces.kml); if (defined(rotation)) { const rotationRadians = CesiumMath.toRadians(rotation); geometry.rotation = rotationRadians; geometry.stRotation = rotationRadians; } } } const iconNode = queryFirstNode(groundOverlay, "Icon", namespaces.kml); const href = getIconHref( iconNode, dataSource, processingData.sourceResource, processingData.uriResolver, true ); if (defined(href)) { if (isLatLonQuad) { oneTimeWarning( "kml-gx:LatLonQuad", "KML - gx:LatLonQuad Icon does not support texture projection." ); } const x = queryNumericValue(iconNode, "x", namespaces.gx); const y = queryNumericValue(iconNode, "y", namespaces.gx); const w = queryNumericValue(iconNode, "w", namespaces.gx); const h = queryNumericValue(iconNode, "h", namespaces.gx); if (defined(x) || defined(y) || defined(w) || defined(h)) { oneTimeWarning( "kml-groundOverlay-xywh", "KML - gx:x, gx:y, gx:w, gx:h aren't supported for GroundOverlays" ); } geometry.material = href; geometry.material.color = queryColorValue( groundOverlay, "color", namespaces.kml ); geometry.material.transparent = true; } else { geometry.material = queryColorValue(groundOverlay, "color", namespaces.kml); } let altitudeMode = queryStringValue( groundOverlay, "altitudeMode", namespaces.kml ); if (defined(altitudeMode)) { if (altitudeMode === "absolute") { //Use height above ellipsoid until we support MSL. geometry.height = queryNumericValue( groundOverlay, "altitude", namespaces.kml ); geometry.zIndex = undefined; } else if (altitudeMode !== "clampToGround") { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown altitudeMode: ${altitudeMode}` ); } // else just use the default of 0 until we support 'clampToGround' } else { altitudeMode = queryStringValue( groundOverlay, "altitudeMode", namespaces.gx ); if (altitudeMode === "relativeToSeaFloor") { oneTimeWarning( "kml-altitudeMode-relativeToSeaFloor", "KML - altitudeMode relativeToSeaFloor is currently not supported, treating as absolute." ); geometry.height = queryNumericValue( groundOverlay, "altitude", namespaces.kml ); geometry.zIndex = undefined; } else if (altitudeMode === "clampToSeaFloor") { oneTimeWarning( "kml-altitudeMode-clampToSeaFloor", "KML - altitudeMode clampToSeaFloor is currently not supported, treating as clampToGround." ); } else if (defined(altitudeMode)) { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown altitudeMode: ${altitudeMode}` ); } } } function processUnsupportedFeature( dataSource, node, processingData, deferredLoading ) { dataSource._unsupportedNode.raiseEvent( dataSource, processingData.parentEntity, node, processingData.entityCollection, processingData.styleCollection, processingData.sourceResource, processingData.uriResolver ); oneTimeWarning( `kml-unsupportedFeature-${node.nodeName}`, `KML - Unsupported feature: ${node.nodeName}` ); } const RefreshMode = { INTERVAL: 0, EXPIRE: 1, STOP: 2, }; function cleanupString(s) { if (!defined(s) || s.length === 0) { return ""; } const sFirst = s[0]; if (sFirst === "&" || sFirst === "?") { s = s.substring(1); } return s; } const zeroRectangle = new Rectangle(); const scratchCartographic = new Cartographic(); const scratchCartesian2 = new Cartesian2(); const scratchCartesian3 = new Cartesian3(); function processNetworkLinkQueryString( resource, camera, canvas, viewBoundScale, bbox, ellipsoid ) { function fixLatitude(value) { if (value < -CesiumMath.PI_OVER_TWO) { return -CesiumMath.PI_OVER_TWO; } else if (value > CesiumMath.PI_OVER_TWO) { return CesiumMath.PI_OVER_TWO; } return value; } function fixLongitude(value) { if (value > CesiumMath.PI) { return value - CesiumMath.TWO_PI; } else if (value < -CesiumMath.PI) { return value + CesiumMath.TWO_PI; } return value; } let queryString = objectToQuery(resource.queryParameters); // objectToQuery escapes [ and ], so fix that queryString = queryString.replace(/%5B/g, "[").replace(/%5D/g, "]"); if (defined(camera) && camera._mode !== SceneMode.MORPHING) { let centerCartesian; let centerCartographic; bbox = defaultValue(bbox, zeroRectangle); if (defined(canvas)) { scratchCartesian2.x = canvas.clientWidth * 0.5; scratchCartesian2.y = canvas.clientHeight * 0.5; centerCartesian = camera.pickEllipsoid( scratchCartesian2, ellipsoid, scratchCartesian3 ); } if (defined(centerCartesian)) { centerCartographic = ellipsoid.cartesianToCartographic( centerCartesian, scratchCartographic ); } else { centerCartographic = Rectangle.center(bbox, scratchCartographic); centerCartesian = ellipsoid.cartographicToCartesian(centerCartographic); } if ( defined(viewBoundScale) && !CesiumMath.equalsEpsilon(viewBoundScale, 1.0, CesiumMath.EPSILON9) ) { const newHalfWidth = bbox.width * viewBoundScale * 0.5; const newHalfHeight = bbox.height * viewBoundScale * 0.5; bbox = new Rectangle( fixLongitude(centerCartographic.longitude - newHalfWidth), fixLatitude(centerCartographic.latitude - newHalfHeight), fixLongitude(centerCartographic.longitude + newHalfWidth), fixLatitude(centerCartographic.latitude + newHalfHeight) ); } queryString = queryString.replace( "[bboxWest]", CesiumMath.toDegrees(bbox.west).toString() ); queryString = queryString.replace( "[bboxSouth]", CesiumMath.toDegrees(bbox.south).toString() ); queryString = queryString.replace( "[bboxEast]", CesiumMath.toDegrees(bbox.east).toString() ); queryString = queryString.replace( "[bboxNorth]", CesiumMath.toDegrees(bbox.north).toString() ); const lon = CesiumMath.toDegrees(centerCartographic.longitude).toString(); const lat = CesiumMath.toDegrees(centerCartographic.latitude).toString(); queryString = queryString.replace("[lookatLon]", lon); queryString = queryString.replace("[lookatLat]", lat); queryString = queryString.replace( "[lookatTilt]", CesiumMath.toDegrees(camera.pitch).toString() ); queryString = queryString.replace( "[lookatHeading]", CesiumMath.toDegrees(camera.heading).toString() ); queryString = queryString.replace( "[lookatRange]", Cartesian3.distance(camera.positionWC, centerCartesian) ); queryString = queryString.replace("[lookatTerrainLon]", lon); queryString = queryString.replace("[lookatTerrainLat]", lat); queryString = queryString.replace( "[lookatTerrainAlt]", centerCartographic.height.toString() ); ellipsoid.cartesianToCartographic(camera.positionWC, scratchCartographic); queryString = queryString.replace( "[cameraLon]", CesiumMath.toDegrees(scratchCartographic.longitude).toString() ); queryString = queryString.replace( "[cameraLat]", CesiumMath.toDegrees(scratchCartographic.latitude).toString() ); queryString = queryString.replace( "[cameraAlt]", CesiumMath.toDegrees(scratchCartographic.height).toString() ); const frustum = camera.frustum; const aspectRatio = frustum.aspectRatio; let horizFov = ""; let vertFov = ""; if (defined(aspectRatio)) { const fov = CesiumMath.toDegrees(frustum.fov); if (aspectRatio > 1.0) { horizFov = fov; vertFov = fov / aspectRatio; } else { vertFov = fov; horizFov = fov * aspectRatio; } } queryString = queryString.replace("[horizFov]", horizFov.toString()); queryString = queryString.replace("[vertFov]", vertFov.toString()); } else { queryString = queryString.replace("[bboxWest]", "-180"); queryString = queryString.replace("[bboxSouth]", "-90"); queryString = queryString.replace("[bboxEast]", "180"); queryString = queryString.replace("[bboxNorth]", "90"); queryString = queryString.replace("[lookatLon]", ""); queryString = queryString.replace("[lookatLat]", ""); queryString = queryString.replace("[lookatRange]", ""); queryString = queryString.replace("[lookatTilt]", ""); queryString = queryString.replace("[lookatHeading]", ""); queryString = queryString.replace("[lookatTerrainLon]", ""); queryString = queryString.replace("[lookatTerrainLat]", ""); queryString = queryString.replace("[lookatTerrainAlt]", ""); queryString = queryString.replace("[cameraLon]", ""); queryString = queryString.replace("[cameraLat]", ""); queryString = queryString.replace("[cameraAlt]", ""); queryString = queryString.replace("[horizFov]", ""); queryString = queryString.replace("[vertFov]", ""); } if (defined(canvas)) { queryString = queryString.replace("[horizPixels]", canvas.clientWidth); queryString = queryString.replace("[vertPixels]", canvas.clientHeight); } else { queryString = queryString.replace("[horizPixels]", ""); queryString = queryString.replace("[vertPixels]", ""); } queryString = queryString.replace("[terrainEnabled]", "1"); queryString = queryString.replace("[clientVersion]", "1"); queryString = queryString.replace("[kmlVersion]", "2.2"); queryString = queryString.replace("[clientName]", "Cesium"); queryString = queryString.replace("[language]", "English"); resource.setQueryParameters(queryToObject(queryString)); } function processNetworkLink(dataSource, node, processingData, deferredLoading) { const r = processFeature(dataSource, node, processingData); const networkEntity = r.entity; const sourceResource = processingData.sourceResource; const uriResolver = processingData.uriResolver; let link = queryFirstNode(node, "Link", namespaces.kml); if (!defined(link)) { link = queryFirstNode(node, "Url", namespaces.kml); } if (defined(link)) { let href = queryStringValue(link, "href", namespaces.kml); let viewRefreshMode; let viewBoundScale; if (defined(href)) { let newSourceUri = href; href = resolveHref(href, sourceResource, processingData.uriResolver); // We need to pass in the original path if resolveHref returns a data uri because the network link // references a document in a KMZ archive if (/^data:/.test(href.getUrlComponent())) { // So if sourceUri isn't the kmz file, then its another kml in the archive, so resolve it if (!/\.kmz/i.test(sourceResource.getUrlComponent())) { newSourceUri = sourceResource.getDerivedResource({ url: newSourceUri, }); } } else { newSourceUri = href.clone(); // Not a data uri so use the fully qualified uri viewRefreshMode = queryStringValue( link, "viewRefreshMode", namespaces.kml ); if (viewRefreshMode === "onRegion") { oneTimeWarning( "kml-refrehMode-onRegion", "KML - Unsupported viewRefreshMode: onRegion" ); return; } viewBoundScale = defaultValue( queryStringValue(link, "viewBoundScale", namespaces.kml), 1.0 ); const defaultViewFormat = viewRefreshMode === "onStop" ? "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" : ""; const viewFormat = defaultValue( queryStringValue(link, "viewFormat", namespaces.kml), defaultViewFormat ); const httpQuery = queryStringValue(link, "httpQuery", namespaces.kml); if (defined(viewFormat)) { href.setQueryParameters(queryToObject(cleanupString(viewFormat))); } if (defined(httpQuery)) { href.setQueryParameters(queryToObject(cleanupString(httpQuery))); } const ellipsoid = dataSource._ellipsoid; processNetworkLinkQueryString( href, dataSource.camera, dataSource.canvas, viewBoundScale, dataSource._lastCameraView.bbox, ellipsoid ); } const options = { sourceUri: newSourceUri, uriResolver: uriResolver, context: networkEntity.id, screenOverlayContainer: processingData.screenOverlayContainer, }; const networkLinkCollection = new EntityCollection(); const promise = load(dataSource, networkLinkCollection, href, options) .then(function (rootElement) { const entities = dataSource._entityCollection; const newEntities = networkLinkCollection.values; entities.suspendEvents(); for (let i = 0; i < newEntities.length; i++) { const newEntity = newEntities[i]; if (!defined(newEntity.parent)) { newEntity.parent = networkEntity; mergeAvailabilityWithParent(newEntity); } entities.add(newEntity); } entities.resumeEvents(); // Add network links to a list if we need they will need to be updated const refreshMode = queryStringValue( link, "refreshMode", namespaces.kml ); let refreshInterval = defaultValue( queryNumericValue(link, "refreshInterval", namespaces.kml), 0 ); if ( (refreshMode === "onInterval" && refreshInterval > 0) || refreshMode === "onExpire" || viewRefreshMode === "onStop" ) { const networkLinkControl = queryFirstNode( rootElement, "NetworkLinkControl", namespaces.kml ); const hasNetworkLinkControl = defined(networkLinkControl); const now = JulianDate.now(); const networkLinkInfo = { id: createGuid(), href: href, cookie: {}, lastUpdated: now, updating: false, entity: networkEntity, viewBoundScale: viewBoundScale, needsUpdate: false, cameraUpdateTime: now, }; let minRefreshPeriod = 0; if (hasNetworkLinkControl) { networkLinkInfo.cookie = queryToObject( defaultValue( queryStringValue( networkLinkControl, "cookie", namespaces.kml ), "" ) ); minRefreshPeriod = defaultValue( queryNumericValue( networkLinkControl, "minRefreshPeriod", namespaces.kml ), 0 ); } if (refreshMode === "onInterval") { if (hasNetworkLinkControl) { refreshInterval = Math.max(minRefreshPeriod, refreshInterval); } networkLinkInfo.refreshMode = RefreshMode.INTERVAL; networkLinkInfo.time = refreshInterval; } else if (refreshMode === "onExpire") { let expires; if (hasNetworkLinkControl) { expires = queryStringValue( networkLinkControl, "expires", namespaces.kml ); } if (defined(expires)) { try { const date = JulianDate.fromIso8601(expires); const diff = JulianDate.secondsDifference(date, now); if (diff > 0 && diff < minRefreshPeriod) { JulianDate.addSeconds(now, minRefreshPeriod, date); } networkLinkInfo.refreshMode = RefreshMode.EXPIRE; networkLinkInfo.time = date; } catch (e) { oneTimeWarning( "kml-refreshMode-onInterval-onExpire", "KML - NetworkLinkControl expires is not a valid date" ); } } else { oneTimeWarning( "kml-refreshMode-onExpire", "KML - refreshMode of onExpire requires the NetworkLinkControl to have an expires element" ); } } else if (defined(dataSource.camera)) { // Only allow onStop refreshes if we have a camera networkLinkInfo.refreshMode = RefreshMode.STOP; networkLinkInfo.time = defaultValue( queryNumericValue(link, "viewRefreshTime", namespaces.kml), 0 ); } else { oneTimeWarning( "kml-refrehMode-onStop-noCamera", "A NetworkLink with viewRefreshMode=onStop requires the `camera` property to be defined." ); } if (defined(networkLinkInfo.refreshMode)) { dataSource._networkLinks.set(networkLinkInfo.id, networkLinkInfo); } } }) .catch(function (error) { oneTimeWarning(`An error occured during loading ${href.url}`); dataSource._error.raiseEvent(dataSource, error); }); deferredLoading.addPromise(promise); } } } function processFeatureNode(dataSource, node, processingData, deferredLoading) { const featureProcessor = featureTypes[node.localName]; if (defined(featureProcessor)) { return featureProcessor(dataSource, node, processingData, deferredLoading); } return processUnsupportedFeature( dataSource, node, processingData, deferredLoading ); } function loadKml( dataSource, entityCollection, kml, sourceResource, uriResolver, screenOverlayContainer, context ) { entityCollection.removeAll(); const documentElement = kml.documentElement; const document = documentElement.localName === "Document" ? documentElement : queryFirstNode(documentElement, "Document", namespaces.kml); let name = queryStringValue(document, "name", namespaces.kml); if (!defined(name)) { name = getFilenameFromUri(sourceResource.getUrlComponent()); } // Only set the name from the root document if (!defined(dataSource._name)) { dataSource._name = name; } const deferredLoading = new KmlDataSource._DeferredLoading(dataSource); const styleCollection = new EntityCollection(dataSource); return Promise.all( processStyles( dataSource, kml, styleCollection, sourceResource, false, uriResolver ) ).then(function () { let element = kml.documentElement; if (element.localName === "kml") { const childNodes = element.childNodes; for (let i = 0; i < childNodes.length; i++) { const tmp = childNodes[i]; if (defined(featureTypes[tmp.localName])) { element = tmp; break; } } } const processingData = { parentEntity: undefined, entityCollection: entityCollection, styleCollection: styleCollection, sourceResource: sourceResource, uriResolver: uriResolver, context: context, screenOverlayContainer: screenOverlayContainer, }; entityCollection.suspendEvents(); processFeatureNode(dataSource, element, processingData, deferredLoading); entityCollection.resumeEvents(); return deferredLoading.wait().then(function () { return kml.documentElement; }); }); } function loadKmz( dataSource, entityCollection, blob, sourceResource, screenOverlayContainer ) { 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 reader = new zip.ZipReader(new zip.BlobReader(blob)); return Promise.resolve(reader.getEntries()).then(function (entries) { const promises = []; const uriResolver = {}; let docEntry; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (!entry.directory) { if (/\.kml$/i.test(entry.filename)) { // We use the first KML document we come across // https://developers.google.com/kml/documentation/kmzarchives // Unless we come across a .kml file at the root of the archive because GE does this if (!defined(docEntry) || !/\//i.test(entry.filename)) { if (defined(docEntry)) { // We found one at the root so load the initial kml as a data uri promises.push(loadDataUriFromZip(docEntry, uriResolver)); } docEntry = entry; } else { // Wasn't the first kml and wasn't at the root promises.push(loadDataUriFromZip(entry, uriResolver)); } } else { promises.push(loadDataUriFromZip(entry, uriResolver)); } } } // Now load the root KML document if (defined(docEntry)) { promises.push(loadXmlFromZip(docEntry, uriResolver)); } return Promise.all(promises).then(function () { reader.close(); if (!defined(uriResolver.kml)) { throw new RuntimeError("KMZ file does not contain a KML document."); } uriResolver.keys = Object.keys(uriResolver); return loadKml( dataSource, entityCollection, uriResolver.kml, sourceResource, uriResolver, screenOverlayContainer ); }); }); } function load(dataSource, entityCollection, data, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); let sourceUri = options.sourceUri; const uriResolver = options.uriResolver; const context = options.context; let screenOverlayContainer = options.screenOverlayContainer; let promise = data; if (typeof data === "string" || data instanceof Resource) { data = Resource.createIfNeeded(data); promise = data.fetchBlob(); sourceUri = defaultValue(sourceUri, data.clone()); // Add resource credits to our list of credits to display const resourceCredits = dataSource._resourceCredits; const credits = data.credits; if (defined(credits)) { const length = credits.length; for (let i = 0; i < length; i++) { resourceCredits.push(credits[i]); } } } else { sourceUri = defaultValue(sourceUri, Resource.DEFAULT.clone()); } sourceUri = Resource.createIfNeeded(sourceUri); if (defined(screenOverlayContainer)) { screenOverlayContainer = getElement(screenOverlayContainer); } return Promise.resolve(promise) .then(function (dataToLoad) { if (dataToLoad instanceof Blob) { return isZipFile(dataToLoad).then(function (isZip) { if (isZip) { return loadKmz( dataSource, entityCollection, dataToLoad, sourceUri, screenOverlayContainer ); } return readBlobAsText(dataToLoad).then(function (text) { //There's no official way to validate if a parse was successful. //The following check detects the error on various browsers. //Insert missing namespaces text = insertNamespaces(text); //Remove Duplicate Namespaces text = removeDuplicateNamespaces(text); //IE raises an exception let kml; let error; try { kml = parser.parseFromString(text, "application/xml"); } catch (e) { error = e.toString(); } //The parse succeeds on Chrome and Firefox, but the error //handling is different in each. if ( defined(error) || kml.body || kml.documentElement.tagName === "parsererror" ) { //Firefox has error information as the firstChild nodeValue. let msg = defined(error) ? error : kml.documentElement.firstChild.nodeValue; //Chrome has it in the body text. if (!msg) { msg = kml.body.innerText; } //Return the error throw new RuntimeError(msg); } return loadKml( dataSource, entityCollection, kml, sourceUri, uriResolver, screenOverlayContainer, context ); }); }); } return loadKml( dataSource, entityCollection, dataToLoad, sourceUri, uriResolver, screenOverlayContainer, context ); }) .catch(function (error) { dataSource._error.raiseEvent(dataSource, error); console.log(error); return Promise.reject(error); }); } // NOTE: LoadOptions properties are repeated in ConstructorOptions because some // tooling does not support "base types" for @typedef. Remove if/when // https://github.com/microsoft/TypeScript/issues/20077 and/or // https://github.com/jsdoc/jsdoc/issues/1199 actually get resolved /** * @typedef {object} KmlDataSource.LoadOptions * * Initialization options for the `load` method. * * @property {string} [sourceUri] Overrides the url to use for resolving relative links and other KML network features. * @property {boolean} [clampToGround=false] true if we want the geometry features (Polygons, LineStrings and LinearRings) clamped to the ground. * @property {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The global ellipsoid used for geographical calculations. * @property {Element|string} [screenOverlayContainer] A container for ScreenOverlay images. */ /** * @typedef {object} KmlDataSource.ConstructorOptions * * Options for constructing a new KmlDataSource, or calling the static `load` method. * * @property {Camera} [camera] The camera that is used for viewRefreshModes and sending camera properties to network links. * @property {HTMLCanvasElement} [canvas] The canvas that is used for sending viewer properties to network links. * @property {Credit|string} [credit] A credit for the data source, which is displayed on the canvas. * * @property {string} [sourceUri] Overrides the url to use for resolving relative links and other KML network features. * @property {boolean} [clampToGround=false] true if we want the geometry features (Polygons, LineStrings and LinearRings) clamped to the ground. * @property {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The global ellipsoid used for geographical calculations. * @property {Element|string} [screenOverlayContainer] A container for ScreenOverlay images. */ /** * A {@link DataSource} which processes Keyhole Markup Language 2.2 (KML). *

* KML support in Cesium is incomplete, but a large amount of the standard, * as well as Google's gx extension namespace, is supported. See Github issue * {@link https://github.com/CesiumGS/cesium/issues/873|#873} for a * detailed list of what is and isn't supported. Cesium will also write information to the * console when it encounters most unsupported features. *

*

* Non visual feature data, such as atom:author and ExtendedData * is exposed via an instance of {@link KmlFeatureData}, which is added to each {@link Entity} * under the kml property. *

* * @alias KmlDataSource * @constructor * * @param {KmlDataSource.ConstructorOptions} [options] Object describing initialization options * * @see {@link http://www.opengeospatial.org/standards/kml/|Open Geospatial Consortium KML Standard} * @see {@link https://developers.google.com/kml/|Google KML Documentation} * * @demo {@link https://sandcastle.cesium.com/index.html?src=KML.html|Cesium Sandcastle KML Demo} * * @example * const viewer = new Cesium.Viewer('cesiumContainer'); * viewer.dataSources.add(Cesium.KmlDataSource.load('../../SampleData/facilities.kmz', * { * camera: viewer.scene.camera, * canvas: viewer.scene.canvas * }) * ); */ function KmlDataSource(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); const camera = options.camera; const canvas = options.canvas; this._changed = new Event(); this._error = new Event(); this._loading = new Event(); this._refresh = new Event(); this._unsupportedNode = new Event(); this._clock = undefined; this._entityCollection = new EntityCollection(this); this._name = undefined; this._isLoading = false; this._pinBuilder = new PinBuilder(); this._networkLinks = new AssociativeArray(); this._entityCluster = new EntityCluster(); /** * The current size of this Canvas will be used to populate the Link parameters * for client height and width. * * @type {HTMLCanvasElement | undefined} */ this.canvas = canvas; /** * The position and orientation of this {@link Camera} will be used to * populate various camera parameters when making network requests. * Camera movement will determine when to trigger NetworkLink refresh if * viewRefreshMode is onStop. * * @type {Camera | undefined} */ this.camera = camera; this._lastCameraView = { position: defined(camera) ? Cartesian3.clone(camera.positionWC) : undefined, direction: defined(camera) ? Cartesian3.clone(camera.directionWC) : undefined, up: defined(camera) ? Cartesian3.clone(camera.upWC) : undefined, bbox: defined(camera) ? camera.computeViewRectangle() : Rectangle.clone(Rectangle.MAX_VALUE), }; this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); // User specified credit let credit = options.credit; if (typeof credit === "string") { credit = new Credit(credit); } this._credit = credit; // Create a list of Credit's from the resource that the user can't remove this._resourceCredits = []; this._kmlTours = []; this._screenOverlays = []; } /** * Creates a Promise to a new instance loaded with the provided KML data. * * @param {Resource|string|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document. * @param {KmlDataSource.ConstructorOptions} [options] An object specifying configuration options * * @returns {Promise} A promise that will resolve to a new KmlDataSource instance once the KML is loaded. */ KmlDataSource.load = function (data, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); const dataSource = new KmlDataSource(options); return dataSource.load(data, options); }; Object.defineProperties(KmlDataSource.prototype, { /** * Gets or sets a human-readable name for this instance. * This will be automatically be set to the KML document name on load. * @memberof KmlDataSource.prototype * @type {string} */ name: { get: function () { return this._name; }, set: function (value) { if (this._name !== value) { this._name = value; this._changed.raiseEvent(this); } }, }, /** * Gets the clock settings defined by the loaded KML. This represents the total * availability interval for all time-dynamic data. If the KML does not contain * time-dynamic data, this value is undefined. * @memberof KmlDataSource.prototype * @type {DataSourceClock} */ clock: { get: function () { return this._clock; }, }, /** * Gets the collection of {@link Entity} instances. * @memberof KmlDataSource.prototype * @type {EntityCollection} */ entities: { get: function () { return this._entityCollection; }, }, /** * Gets a value indicating if the data source is currently loading data. * @memberof KmlDataSource.prototype * @type {boolean} */ isLoading: { get: function () { return this._isLoading; }, }, /** * Gets an event that will be raised when the underlying data changes. * @memberof KmlDataSource.prototype * @type {Event} */ changedEvent: { get: function () { return this._changed; }, }, /** * Gets an event that will be raised if an error is encountered during processing. * @memberof KmlDataSource.prototype * @type {Event} */ errorEvent: { get: function () { return this._error; }, }, /** * Gets an event that will be raised when the data source either starts or stops loading. * @memberof KmlDataSource.prototype * @type {Event} */ loadingEvent: { get: function () { return this._loading; }, }, /** * Gets an event that will be raised when the data source refreshes a network link. * @memberof KmlDataSource.prototype * @type {Event} */ refreshEvent: { get: function () { return this._refresh; }, }, /** * Gets an event that will be raised when the data source finds an unsupported node type. * @memberof KmlDataSource.prototype * @type {Event} */ unsupportedNodeEvent: { get: function () { return this._unsupportedNode; }, }, /** * Gets whether or not this data source should be displayed. * @memberof KmlDataSource.prototype * @type {boolean} */ show: { get: function () { return this._entityCollection.show; }, set: function (value) { this._entityCollection.show = value; }, }, /** * Gets or sets the clustering options for this data source. This object can be shared between multiple data sources. * * @memberof KmlDataSource.prototype * @type {EntityCluster} */ clustering: { get: function () { return this._entityCluster; }, set: function (value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError("value must be defined."); } //>>includeEnd('debug'); this._entityCluster = value; }, }, /** * Gets the credit that will be displayed for the data source * @memberof KmlDataSource.prototype * @type {Credit} */ credit: { get: function () { return this._credit; }, }, /** * Gets the KML Tours that are used to guide the camera to specified destinations on given time intervals. * @memberof KmlDataSource.prototype * @type {KmlTour[]} */ kmlTours: { get: function () { return this._kmlTours; }, }, }); /** * Asynchronously loads the provided KML data, replacing any existing data. * * @param {Resource|string|Document|Blob} data A url, parsed KML document, or Blob containing binary KMZ data or a parsed KML document. * @param {KmlDataSource.LoadOptions} [options] An object specifying configuration options * * @returns {Promise} A promise that will resolve to this instances once the KML is loaded. */ KmlDataSource.prototype.load = function (data, options) { //>>includeStart('debug', pragmas.debug); if (!defined(data)) { throw new DeveloperError("data is required."); } //>>includeEnd('debug'); options = defaultValue(options, defaultValue.EMPTY_OBJECT); DataSource.setLoading(this, true); const oldName = this._name; this._name = undefined; this._clampToGround = defaultValue(options.clampToGround, false); const that = this; return load(this, this._entityCollection, data, options) .then(function () { let clock; const availability = that._entityCollection.computeAvailability(); let start = availability.start; let stop = availability.stop; const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE); const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE); if (!isMinStart || !isMaxStop) { let date; //If start is min time just start at midnight this morning, local time if (isMinStart) { date = new Date(); date.setHours(0, 0, 0, 0); start = JulianDate.fromDate(date); } //If stop is max value just stop at midnight tonight, local time if (isMaxStop) { date = new Date(); date.setHours(24, 0, 0, 0); stop = JulianDate.fromDate(date); } clock = new DataSourceClock(); clock.startTime = start; clock.stopTime = stop; clock.currentTime = JulianDate.clone(start); clock.clockRange = ClockRange.LOOP_STOP; clock.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER; clock.multiplier = Math.round( Math.min( Math.max(JulianDate.secondsDifference(stop, start) / 60, 1), 3.15569e7 ) ); } let changed = false; if (clock !== that._clock) { that._clock = clock; changed = true; } if (oldName !== that._name) { changed = true; } if (changed) { that._changed.raiseEvent(that); } DataSource.setLoading(that, false); return that; }) .catch(function (error) { DataSource.setLoading(that, false); that._error.raiseEvent(that, error); console.log(error); return Promise.reject(error); }); }; /** * Cleans up any non-entity elements created by the data source. Currently this only affects ScreenOverlay elements. */ KmlDataSource.prototype.destroy = function () { while (this._screenOverlays.length > 0) { const elem = this._screenOverlays.pop(); elem.remove(); } }; function mergeAvailabilityWithParent(child) { const parent = child.parent; if (defined(parent)) { const parentAvailability = parent.availability; if (defined(parentAvailability)) { const childAvailability = child.availability; if (defined(childAvailability)) { childAvailability.intersect(parentAvailability); } else { child.availability = parentAvailability; } } } } function getNetworkLinkUpdateCallback( dataSource, networkLink, newEntityCollection, networkLinks, processedHref ) { return function (rootElement) { if (!networkLinks.contains(networkLink.id)) { // Got into the odd case where a parent network link was updated while a child // network link update was in flight, so just throw it away. return; } let remove = false; const networkLinkControl = queryFirstNode( rootElement, "NetworkLinkControl", namespaces.kml ); const hasNetworkLinkControl = defined(networkLinkControl); let minRefreshPeriod = 0; if (hasNetworkLinkControl) { if ( defined(queryFirstNode(networkLinkControl, "Update", namespaces.kml)) ) { oneTimeWarning( "kml-networkLinkControl-update", "KML - NetworkLinkControl updates aren't supported." ); networkLink.updating = false; networkLinks.remove(networkLink.id); return; } networkLink.cookie = queryToObject( defaultValue( queryStringValue(networkLinkControl, "cookie", namespaces.kml), "" ) ); minRefreshPeriod = defaultValue( queryNumericValue( networkLinkControl, "minRefreshPeriod", namespaces.kml ), 0 ); } const now = JulianDate.now(); const refreshMode = networkLink.refreshMode; if (refreshMode === RefreshMode.INTERVAL) { if (defined(networkLinkControl)) { networkLink.time = Math.max(minRefreshPeriod, networkLink.time); } } else if (refreshMode === RefreshMode.EXPIRE) { let expires; if (defined(networkLinkControl)) { expires = queryStringValue( networkLinkControl, "expires", namespaces.kml ); } if (defined(expires)) { try { const date = JulianDate.fromIso8601(expires); const diff = JulianDate.secondsDifference(date, now); if (diff > 0 && diff < minRefreshPeriod) { JulianDate.addSeconds(now, minRefreshPeriod, date); } networkLink.time = date; } catch (e) { oneTimeWarning( "kml-networkLinkControl-expires", "KML - NetworkLinkControl expires is not a valid date" ); remove = true; } } else { oneTimeWarning( "kml-refreshMode-onExpire", "KML - refreshMode of onExpire requires the NetworkLinkControl to have an expires element" ); remove = true; } } const networkLinkEntity = networkLink.entity; const entityCollection = dataSource._entityCollection; const newEntities = newEntityCollection.values; function removeChildren(entity) { entityCollection.remove(entity); const children = entity._children; const count = children.length; for (let i = 0; i < count; ++i) { removeChildren(children[i]); } } // Remove old entities entityCollection.suspendEvents(); const entitiesCopy = entityCollection.values.slice(); let i; for (i = 0; i < entitiesCopy.length; ++i) { const entityToRemove = entitiesCopy[i]; if (entityToRemove.parent === networkLinkEntity) { entityToRemove.parent = undefined; removeChildren(entityToRemove); } } entityCollection.resumeEvents(); // Add new entities entityCollection.suspendEvents(); for (i = 0; i < newEntities.length; i++) { const newEntity = newEntities[i]; if (!defined(newEntity.parent)) { newEntity.parent = networkLinkEntity; mergeAvailabilityWithParent(newEntity); } entityCollection.add(newEntity); } entityCollection.resumeEvents(); // No refresh information remove it, otherwise update lastUpdate time if (remove) { networkLinks.remove(networkLink.id); } else { networkLink.lastUpdated = now; } const availability = entityCollection.computeAvailability(); const start = availability.start; const stop = availability.stop; const isMinStart = JulianDate.equals(start, Iso8601.MINIMUM_VALUE); const isMaxStop = JulianDate.equals(stop, Iso8601.MAXIMUM_VALUE); if (!isMinStart || !isMaxStop) { const clock = dataSource._clock; if (clock.startTime !== start || clock.stopTime !== stop) { clock.startTime = start; clock.stopTime = stop; dataSource._changed.raiseEvent(dataSource); } } networkLink.updating = false; networkLink.needsUpdate = false; dataSource._refresh.raiseEvent( dataSource, processedHref.getUrlComponent(true) ); }; } const entitiesToIgnore = new AssociativeArray(); /** * Updates any NetworkLink that require updating. * * @param {JulianDate} time The simulation time. * @returns {boolean} True if this data source is ready to be displayed at the provided time, false otherwise. */ KmlDataSource.prototype.update = function (time) { const networkLinks = this._networkLinks; if (networkLinks.length === 0) { return true; } const now = JulianDate.now(); const that = this; entitiesToIgnore.removeAll(); function recurseIgnoreEntities(entity) { const children = entity._children; const count = children.length; for (let i = 0; i < count; ++i) { const child = children[i]; entitiesToIgnore.set(child.id, child); recurseIgnoreEntities(child); } } let cameraViewUpdate = false; const lastCameraView = this._lastCameraView; const camera = this.camera; if ( defined(camera) && !( camera.positionWC.equalsEpsilon( lastCameraView.position, CesiumMath.EPSILON7 ) && camera.directionWC.equalsEpsilon( lastCameraView.direction, CesiumMath.EPSILON7 ) && camera.upWC.equalsEpsilon(lastCameraView.up, CesiumMath.EPSILON7) ) ) { // Camera has changed so update the last view lastCameraView.position = Cartesian3.clone(camera.positionWC); lastCameraView.direction = Cartesian3.clone(camera.directionWC); lastCameraView.up = Cartesian3.clone(camera.upWC); lastCameraView.bbox = camera.computeViewRectangle(); cameraViewUpdate = true; } const newNetworkLinks = new AssociativeArray(); let changed = false; networkLinks.values.forEach(function (networkLink) { const entity = networkLink.entity; if (entitiesToIgnore.contains(entity.id)) { return; } if (!networkLink.updating) { let doUpdate = false; if (networkLink.refreshMode === RefreshMode.INTERVAL) { if ( JulianDate.secondsDifference(now, networkLink.lastUpdated) > networkLink.time ) { doUpdate = true; } } else if (networkLink.refreshMode === RefreshMode.EXPIRE) { if (JulianDate.greaterThan(now, networkLink.time)) { doUpdate = true; } } else if (networkLink.refreshMode === RefreshMode.STOP) { if (cameraViewUpdate) { networkLink.needsUpdate = true; networkLink.cameraUpdateTime = now; } if ( networkLink.needsUpdate && JulianDate.secondsDifference(now, networkLink.cameraUpdateTime) >= networkLink.time ) { doUpdate = true; } } if (doUpdate) { recurseIgnoreEntities(entity); networkLink.updating = true; const newEntityCollection = new EntityCollection(); const href = networkLink.href.clone(); href.setQueryParameters(networkLink.cookie); const ellipsoid = defaultValue(that._ellipsoid, Ellipsoid.WGS84); processNetworkLinkQueryString( href, that.camera, that.canvas, networkLink.viewBoundScale, lastCameraView.bbox, ellipsoid ); load(that, newEntityCollection, href, { context: entity.id, }) .then( getNetworkLinkUpdateCallback( that, networkLink, newEntityCollection, newNetworkLinks, href ) ) .catch(function (error) { const msg = `NetworkLink ${networkLink.href} refresh failed: ${error}`; console.log(msg); that._error.raiseEvent(that, msg); }); changed = true; } } newNetworkLinks.set(networkLink.id, networkLink); }); if (changed) { this._networkLinks = newNetworkLinks; this._changed.raiseEvent(this); } return true; }; /** * Contains KML Feature data loaded into the Entity.kml property by {@link KmlDataSource}. * @alias KmlFeatureData * @constructor */ function KmlFeatureData() { /** * @typedef KmlFeatureData.Author * @type {object} * @property {string} name Gets the name. * @property {string} uri Gets the URI. * @property {number} age Gets the email. */ /** * Gets the atom syndication format author field. * @type {KmlFeatureData.Author} */ this.author = { name: undefined, uri: undefined, email: undefined, }; /** * @typedef KmlFeatureData.Link * @type {object} * @property {string} href Gets the href. * @property {string} hreflang Gets the language of the linked resource. * @property {string} rel Gets the link relation. * @property {string} type Gets the link type. * @property {string} title Gets the link title. * @property {string} length Gets the link length. */ /** * Gets the link. * @type {KmlFeatureData.Link} */ this.link = { href: undefined, hreflang: undefined, rel: undefined, type: undefined, title: undefined, length: undefined, }; /** * Gets the unstructured address field. * @type {string} */ this.address = undefined; /** * Gets the phone number. * @type {string} */ this.phoneNumber = undefined; /** * Gets the snippet. * @type {string} */ this.snippet = undefined; /** * Gets the extended data, parsed into a JSON object. * Currently only the Data property is supported. * SchemaData and custom data are ignored. * @type {string} */ this.extendedData = undefined; } // For testing KmlDataSource._DeferredLoading = DeferredLoading; KmlDataSource._getTimestamp = getTimestamp; export default KmlDataSource;