Flatlogic Bot 055d24df95 WORKING
2025-10-14 02:37:44 +00:00

4325 lines
126 KiB
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 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("<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 = 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 - <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: ${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 <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 = 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 =
'<table class="cesium-infoBox-defaultTable cesium-infoBox-defaultTable-lighter"><tbody>';
for (i = 0; i < keys.length; i++) {
key = keys[i];
value = extendedData[key];
text += `<tr><th>${defaultValue(
value.displayName,
key
)}</th><td>${defaultValue(value.value, "")}</td></tr>`;
}
text += "</tbody></table>";
}
}
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 = '<div class="cesium-infoBox-description-lighter" style="';
tmp += "overflow:auto;";
tmp += "word-wrap:break-word;";
tmp += `background-color:${background.toCssColorString()};`;
tmp += `color:${foreground.toCssColorString()};`;
tmp += '">';
tmp += `${scratchDiv.innerHTML}</div>`;
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).
* <p>
* KML support in Cesium is incomplete, but a large amount of the standard,
* as well as Google's <code>gx</code> 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.
* </p>
* <p>
* Non visual feature data, such as <code>atom:author</code> and <code>ExtendedData</code>
* is exposed via an instance of {@link KmlFeatureData}, which is added to each {@link Entity}
* under the <code>kml</code> property.
* </p>
*
* @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
* <code>viewRefreshMode</code> is <code>onStop</code>.
*
* @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<KmlDataSource>} 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<KmlDataSource>} 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 <code>Entity.kml</code> 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 <code>Data</code> property is supported.
* <code>SchemaData</code> and custom data are ignored.
* @type {string}
*/
this.extendedData = undefined;
}
// For testing
KmlDataSource._DeferredLoading = DeferredLoading;
KmlDataSource._getTimestamp = getTimestamp;
export default KmlDataSource;