@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
JavaScript
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