UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

1,707 lines (1,545 loc) 128 kB
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 Frozen from "../Core/Frozen.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("<kml") + 4); lastPart = text.substr(firstPart.length); } firstPart += ` ${declaration}"${namespaceMap[key]}"`; } } } if (defined(firstPart)) { text = firstPart + lastPart; } return text; } function removeDuplicateNamespaces(text) { let index = text.indexOf("xmlns:"); const endDeclaration = 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 = 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 = queryNumericValue(iconNode, "x", namespaces.gx) ?? 0; let y = 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 = queryStringValue(iconNode, "viewBoundScale", namespaces.kml) ?? 1.0; const defaultViewFormat = viewRefreshMode === "onStop" ? "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" : ""; const viewFormat = 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 = 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 = queryNumericValue(node, "scale", namespaces.kml) ?? label.scale; label.fillColor = 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 = queryColorValue(node, "color", namespaces.kml) ?? polygon.material; polygon.fill = queryBooleanValue(node, "fill", namespaces.kml) ?? polygon.fill; polygon.outline = queryBooleanValue(node, "outline", namespaces.kml) ?? polygon.outline; } else if (node.localName === "BalloonStyle") { const bgColor = parseColorString(queryStringValue(node, "bgColor", namespaces.kml)) ?? Color.WHITE; const textColor = 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 - <gx:altitudeMode>:clampToSeaFloor is currently not supported, using <kml:altitudeMode>:clampToGround.", ); return HeightReference.CLAMP_TO_GROUND; } if (gxAltitudeMode === "relativeToSeaFloor") { oneTimeWarning( "kml-gx:altitudeMode-relativeToSeaFloor", "KML - <gx:altitudeMode>:relativeToSeaFloor is currently not supported, using <kml:altitudeMode>:relativeToGround.", ); return HeightReference.RELATIVE_TO_GROUND; } if (defined(altitudeMode)) { oneTimeWarning( "kml-altitudeMode-unknown", `KML - Unknown <kml:altitudeMode>:${altitudeMode}, using <kml:altitudeMode>:CLAMP_TO_GROUND.`, ); } else { oneTimeWarning( "kml-gx:altitudeMode-unknown", `KML - Unknown <gx:altitudeMode>:${gxAltitudeMode}, using <kml:altitudeMode>: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: ${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: ${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 <Icon> 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 = 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( geome