From eca48b4fab95a11c5121b45bc6df9be0754f2e1f Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Thu, 16 May 2019 17:59:03 -0400 Subject: [PATCH 01/27] Initial commit of KML exporter --- Source/DataSources/KmlExporter.js | 72 ++++++++++++++++++++++++++++ Specs/DataSources/KmlExporterSpec.js | 48 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 Source/DataSources/KmlExporter.js create mode 100644 Specs/DataSources/KmlExporterSpec.js diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js new file mode 100644 index 000000000000..98fe7f258137 --- /dev/null +++ b/Source/DataSources/KmlExporter.js @@ -0,0 +1,72 @@ +define([ + '../Core/defined' +], function( + defined) { + 'use strict'; + /** + * @alias KmlExporter + * @constructor + * + * @param {EntityCollection} entities The EntityCollection to export as KML + * @param {Function} externalFileCallback A callback that will be called with the URL of external files + */ + function KmlExporter(entities, externalFileCallback) { + var kmlDoc = this._kmlDoc = document.implementation.createDocument(null, 'kml'); + var kmlElement = kmlDoc.documentElement; + var kmlDocumentElement = kmlDoc.createElement('Document'); + kmlElement.appendChild(kmlDocumentElement); + + var rootEntities = entities.values.filter(function(entity) { + return !defined(entity.parent); + }); + recurseEntities(kmlDoc, kmlDocumentElement, rootEntities); + } + + KmlExporter.prototype.toString = function() { + var serializer = new XMLSerializer(); + return serializer.serializeToString(this._kmlDoc); + }; + + function recurseEntities(kmlDoc, parentNode, entities) { + var count = entities.length; + var placemark; + var geometry; + for (var i = 0; i < count; ++i) { + var entity = entities[i]; + placemark = undefined; + geometry = undefined; + + // TODO: Handle multiple geometries + if (defined(entity.point) || defined(entity.billboard)) { + placemark = kmlDoc.createElement('Placemark'); + geometry = kmlDoc.createElement('Point'); + } else if (defined(entity.polygon)) { + placemark = kmlDoc.createElement('Placemark'); + geometry = kmlDoc.createElement('Polygon'); + } else if (defined(entity.polyline)) { + placemark = kmlDoc.createElement('Placemark'); + geometry = kmlDoc.createElement('LineString'); + } else if (defined(entity.rectangle)) { + placemark = kmlDoc.createElement('Placemark'); + geometry = kmlDoc.createElement('Polygon'); + } + + if (defined(placemark)) { + placemark.appendChild(geometry); + parentNode.appendChild(placemark); + + placemark.setAttribute('id', entity.id); + placemark.setAttribute('name', entity.name); + } + + var children = entity._children; + if (children.length > 0) { + var folderNode = kmlDoc.createElement('Folder'); + parentNode.appendChild(folderNode); + recurseEntities(kmlDoc, folderNode, children); + } + } + } + + return KmlExporter; + }); diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js new file mode 100644 index 000000000000..a61983a31e71 --- /dev/null +++ b/Specs/DataSources/KmlExporterSpec.js @@ -0,0 +1,48 @@ +defineSuite([ + 'DataSources/KmlExporter', + 'Core/Cartesian3', + 'Core/PerspectiveFrustum', + 'Core/Rectangle', + 'DataSources/KmlDataSource', + 'Specs/pollToPromise' +], function( + KmlExporter, + Cartesian3, + PerspectiveFrustum, + Rectangle, + KmlDataSource, + pollToPromise) { +'use strict'; + +var options = { + camera : { + positionWC : new Cartesian3(0.0, 0.0, 0.0), + directionWC : new Cartesian3(0.0, 0.0, 1.0), + upWC : new Cartesian3(0.0, 1.0, 0.0), + pitch : 0.0, + heading : 0.0, + frustum : new PerspectiveFrustum(), + computeViewRectangle : function() { + return Rectangle.MAX_VALUE; + }, + pickEllipsoid : function() { + return undefined; + } + }, + canvas : { + clientWidth : 512, + clientHeight : 512 + } +}; + + it('test', function() { + return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) + .then(function(datasource) { + var exporter = new KmlExporter(datasource.entities); + + var kml = exporter.toString(); + console.log(kml); + }); + }); + +}); From 359f32b9ccbf095a0234e1059292ba84a5b5e9cf Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Fri, 17 May 2019 16:42:44 -0400 Subject: [PATCH 02/27] Got bilboards with styling pretty much there. Need to test (especially the hotspots) --- Source/DataSources/KmlExporter.js | 343 +++++++++++++++++++++++++++--- 1 file changed, 311 insertions(+), 32 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 98fe7f258137..bd4ae753e677 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -1,25 +1,75 @@ define([ - '../Core/defined' + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Cartographic', + '../Core/defaultValue', + '../Core/defined', + '../Core/Ellipsoid', + '../Core/Iso8601', + '../Core/Math', + '../Scene/HeightReference', + '../Scene/HorizontalOrigin', + '../Scene/VerticalOrigin' ], function( - defined) { + Cartesian2, + Cartesian3, + Cartographic, + defaultValue, + defined, + Ellipsoid, + Iso8601, + CesiumMath, + HeightReference, + HorizontalOrigin, + VerticalOrigin) { 'use strict'; + + var BILLBOARD_SIZE = 32; + var kmlNamespace = 'http://www.opengis.net/kml/2.2'; + var gxNamespace = 'http://www.google.com/kml/ext/2.2'; + var xmlnsNamespace = 'http://www.w3.org/2000/xmlns/'; + + function defaultTextureCallback(texture) { + if (typeof texture === 'string') { + return texture; + } + + if (texture instanceof HTMLCanvasElement) { + return texture.toDataURL(); + } + + if (texture instanceof HTMLImageElement) { + return ''; // TODO + } + + return ''; + } + /** * @alias KmlExporter * @constructor * * @param {EntityCollection} entities The EntityCollection to export as KML - * @param {Function} externalFileCallback A callback that will be called with the URL of external files + * @param {Object} options An object with the following properties: + * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file + * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas */ - function KmlExporter(entities, externalFileCallback) { - var kmlDoc = this._kmlDoc = document.implementation.createDocument(null, 'kml'); + function KmlExporter(entities, options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); + this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); + + var kmlDoc = this._kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml'); var kmlElement = kmlDoc.documentElement; + kmlElement.setAttributeNS(xmlnsNamespace, 'xmlns:gx', gxNamespace); + var kmlDocumentElement = kmlDoc.createElement('Document'); kmlElement.appendChild(kmlDocumentElement); var rootEntities = entities.values.filter(function(entity) { return !defined(entity.parent); }); - recurseEntities(kmlDoc, kmlDocumentElement, rootEntities); + recurseEntities(this, kmlDocumentElement, rootEntities); } KmlExporter.prototype.toString = function() { @@ -27,45 +77,274 @@ define([ return serializer.serializeToString(this._kmlDoc); }; - function recurseEntities(kmlDoc, parentNode, entities) { - var count = entities.length; - var placemark; - var geometry; + function recurseEntities(that, parentNode, entities) { + var kmlDoc = that._kmlDoc; + + var count = Math.min(10, entities.length); // TODO + var geometries; for (var i = 0; i < count; ++i) { var entity = entities[i]; - placemark = undefined; - geometry = undefined; - - // TODO: Handle multiple geometries - if (defined(entity.point) || defined(entity.billboard)) { - placemark = kmlDoc.createElement('Placemark'); - geometry = kmlDoc.createElement('Point'); - } else if (defined(entity.polygon)) { - placemark = kmlDoc.createElement('Placemark'); - geometry = kmlDoc.createElement('Polygon'); - } else if (defined(entity.polyline)) { - placemark = kmlDoc.createElement('Placemark'); - geometry = kmlDoc.createElement('LineString'); - } else if (defined(entity.rectangle)) { - placemark = kmlDoc.createElement('Placemark'); - geometry = kmlDoc.createElement('Polygon'); - } + geometries = []; - if (defined(placemark)) { - placemark.appendChild(geometry); - parentNode.appendChild(placemark); + createPoint(that, entity, entity.point, geometries); + createPoint(that, entity, entity.billboard, geometries); + createLineString(that, entity.polyline, geometries); + createPolygon(that, entity.polygon, geometries); + createPolygon(that, entity.rectangle, geometries); + + // TODO: Handle the rest of the geometries + var geometryCount = geometries.length; + if (geometryCount > 0) { + var placemark = kmlDoc.createElement('Placemark'); placemark.setAttribute('id', entity.id); - placemark.setAttribute('name', entity.name); + + placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); + + parentNode.appendChild(placemark); + + if (geometries.length === 1) { + placemark.appendChild(geometries[0]); + } else if (geometries.length > 1) { + var multigeometry = kmlDoc.createElement('MultiGeometry'); + for (var j = 0; j < geometryCount; ++j) { + multigeometry.appendChild(geometries[j]); + } + placemark.appendChild(multigeometry); + } } var children = entity._children; if (children.length > 0) { var folderNode = kmlDoc.createElement('Folder'); + folderNode.setAttribute('name', entity.name); parentNode.appendChild(folderNode); - recurseEntities(kmlDoc, folderNode, children); + + recurseEntities(that, folderNode, children); + } + } + } + + var scratchCartesian3 = new Cartesian3(); + var scratchCartographic = new Cartographic(); + + function createPoint(that, entity, geometry, geometries) { + var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; + + if (!defined(geometry)) { + return; + } + + var pointGeometry = kmlDoc.createElement('Point'); + + // Set altitude mode + var altitudeMode = kmlDoc.createElement('altitudeMode'); + altitudeMode.appendChild(getAltitudeMode(kmlDoc, geometry.heightReference)); + pointGeometry.appendChild(altitudeMode); + + // Set coordinates + var coordinates; + var positionProperty = entity.position; + if (positionProperty.isConstant) { + positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + coordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(scratchCartesian3, ellipsoid)); + } else { + // TODO: Time dynamic + } + + pointGeometry.appendChild(coordinates); + + geometries.push(pointGeometry); + } + + function createIconStyleFromPoint(pointGraphics) { + + } + + function createIconStyleFromBillboard(that, billboardGraphics) { + var kmlDoc = that._kmlDoc; + var iconStyle = kmlDoc.createElement('IconStyle'); + + var image = getValue(billboardGraphics.image); + if (defined(image)) { + image = that._textureCallback(image); + + var icon = kmlDoc.createElement('Icon'); + icon.appendChild(createBasicElementWithText(kmlDoc, 'href', image)); + + var imageSubRegion = getValue(billboardGraphics.imageSubRegion); + if (defined(imageSubRegion)) { + icon.appendChild(createBasicElementWithText(kmlDoc, 'x', imageSubRegion.x, gxNamespace)); + icon.appendChild(createBasicElementWithText(kmlDoc, 'y', imageSubRegion.y, gxNamespace)); + icon.appendChild(createBasicElementWithText(kmlDoc, 'w', imageSubRegion.width, gxNamespace)); + icon.appendChild(createBasicElementWithText(kmlDoc, 'h', imageSubRegion.height, gxNamespace)); } + + iconStyle.appendChild(icon); + } + + var color = getValue(billboardGraphics.color); + if (defined(color)) { + color = colorToString(color); + + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); + } + + var scale = getValue(billboardGraphics.scale, 1.0); + if (defined(scale)) { + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale)); } + + var pixelOffset = getValue(billboardGraphics.pixelOffset); + if (defined(pixelOffset)) { + Cartesian2.divideByScalar(pixelOffset, scale, pixelOffset); + + var width = getValue(billboardGraphics.width, BILLBOARD_SIZE); + var height = getValue(billboardGraphics.height, BILLBOARD_SIZE); + + // KML Hotspots are from the bottom left, but we work from the top left + + // Move to left + var horizontalOrigin = getValue(billboardGraphics.horizontalOrigin, HorizontalOrigin.CENTER); + if (horizontalOrigin === HorizontalOrigin.CENTER) { + pixelOffset.x -= width * 0.5; + } else if (horizontalOrigin === HorizontalOrigin.RIGHT) { + pixelOffset.x -= width; + } + + // Move to bottom + var verticalOrigin = getValue(billboardGraphics.verticalOrigin, VerticalOrigin.CENTER); + if (verticalOrigin === VerticalOrigin.TOP) { + pixelOffset.y += height; + } else if (verticalOrigin === VerticalOrigin.CENTER) { + pixelOffset.y += height * 0.5; + } + + var hotSpot = kmlDoc.createElement('hotSpot'); + hotSpot.setAttribute('x', -pixelOffset.x); + hotSpot.setAttribute('y', pixelOffset.y); + hotSpot.setAttribute('xunits', 'pixels'); + hotSpot.setAttribute('yunits', 'pixels'); + + iconStyle.appendChild(hotSpot); + } + + // We can only specify heading so if axis isn't Z, then we skip the rotation + // GE treats a heading of zero as no heading but can still point north using a 360 degree angle + var rotation = getValue(billboardGraphics.rotation); + var alignedAxis = getValue(billboardGraphics.alignedAxis); + if (defined(rotation) && alignedAxis === Cartesian3.UNIT_Z) { + if (rotation === 0) { + rotation = 360; + } + rotation = Math.toDegrees(-rotation); + } + } + + function createLineString(that, polyline, geometries) { + var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; + + if (!defined(polyline)) { + return; + } + + var lineStringGeometry = kmlDoc.createElement('LineString'); + geometries.push(lineStringGeometry); + } + + function createPolygon(that, polygonOrRectangle, geometries) { + var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; + + if (!defined(polygonOrRectangle)) { + return; + } + + var polygonGeometry = kmlDoc.createElement('Polygon'); + geometries.push(polygonGeometry); + } + + function getAltitudeMode(kmlDoc, heightReferenceProperty) { + // TODO: Time dynamic + var heightReference = defined(heightReferenceProperty) ? + heightReferenceProperty.getValue(Iso8601.MINIMUM_VALUE) : HeightReference.NONE; + var altitudeModeText; + switch (heightReference) { + case HeightReference.NONE: + altitudeModeText = kmlDoc.createTextNode('absolute'); + break; + case HeightReference.CLAMP_TO_GROUND: + altitudeModeText = kmlDoc.createTextNode('clampToGround'); + break; + case HeightReference.RELATIVE_TO_GROUND: + altitudeModeText = kmlDoc.createTextNode('relativeToGround'); + break; + } + + return altitudeModeText; + } + + function getCoordinates(kmlDoc, coordinates, ellipsoid) { + if (!Array.isArray(coordinates)) { + coordinates = [coordinates]; + } + + var count = coordinates.length; + var coordinateStrings = []; + for (var i = 0; i < count; ++i) { + Cartographic.fromCartesian(coordinates[i], ellipsoid, scratchCartographic); + coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' + + CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + + scratchCartographic.height); + } + + return coordinateStrings.join(' '); + } + + function createBasicElementWithText(kmlDoc, elementName, elementValue, namespace) { + if (typeof elementValue === 'boolean') { + elementValue = elementValue ? '1' : '0'; + } + + if (elementValue.indexOf('<') !== -1) { + elementValue = ''; + } + + var element = defined(namespace) ? kmlDoc.createElementNS(namespace, elementName) : kmlDoc.createElement(elementName); + var text = kmlDoc.createTextNode(elementValue); + + element.appendChild(text); + + return element; + } + + function colorToString(color) { + var result = ''; + var bytes = color.toBytes(); + for (var i=3;i>=0;--i) { + result += (bytes[i] < 16) ? ('0' + bytes[i].toString(16)) : bytes[i].toString(16); + } + + return result; + } + + function getValue(property, defaultVal) { + var value; + if (defined(property)) { + if (property.isConstant) { + value = property.getValue(Iso8601.MINIMUM_VALUE); + } else { + // TODO + } + } + + return defaultValue(value, defaultVal); } return KmlExporter; From 3a057140704e4647049a35a8b69eae2a050ea4a2 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Tue, 21 May 2019 13:37:12 -0400 Subject: [PATCH 03/27] Got points/billboards done. --- Source/DataSources/KmlExporter.js | 93 +++++++++++++++++++++------- Specs/DataSources/KmlExporterSpec.js | 52 ++++++++++------ 2 files changed, 102 insertions(+), 43 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index bd4ae753e677..0dea691cdd76 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -1,4 +1,5 @@ define([ + './BillboardGraphics', '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartographic', @@ -7,10 +8,12 @@ define([ '../Core/Ellipsoid', '../Core/Iso8601', '../Core/Math', + '../Core/Resource', '../Scene/HeightReference', '../Scene/HorizontalOrigin', '../Scene/VerticalOrigin' ], function( + BillboardGraphics, Cartesian2, Cartesian3, Cartographic, @@ -19,6 +22,7 @@ define([ Ellipsoid, Iso8601, CesiumMath, + Resource, HeightReference, HorizontalOrigin, VerticalOrigin) { @@ -34,6 +38,10 @@ define([ return texture; } + if (texture instanceof Resource) { + return texture.url; + } + if (texture instanceof HTMLCanvasElement) { return texture.toDataURL(); } @@ -80,17 +88,19 @@ define([ function recurseEntities(that, parentNode, entities) { var kmlDoc = that._kmlDoc; - var count = Math.min(10, entities.length); // TODO + var count = entities.length; var geometries; + var styles; for (var i = 0; i < count; ++i) { var entity = entities[i]; geometries = []; + styles = []; - createPoint(that, entity, entity.point, geometries); - createPoint(that, entity, entity.billboard, geometries); - createLineString(that, entity.polyline, geometries); - createPolygon(that, entity.polygon, geometries); - createPolygon(that, entity.rectangle, geometries); + createPoint(that, entity, entity.point, geometries, styles); + createPoint(that, entity, entity.billboard, geometries, styles); + createLineString(that, entity.polyline, geometries, styles); + createPolygon(that, entity.polygon, geometries, styles); + createPolygon(that, entity.rectangle, geometries, styles); // TODO: Handle the rest of the geometries @@ -99,18 +109,28 @@ define([ var placemark = kmlDoc.createElement('Placemark'); placemark.setAttribute('id', entity.id); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', getValue(entity.name))); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', getValue(entity.show, true))); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', getValue(entity.description))); parentNode.appendChild(placemark); + var styleCount = styles.length; + if (styleCount > 0) { + var style = kmlDoc.createElement('Style'); + for (var styleIndex = 0; styleIndex < geometryCount; ++styleIndex) { + style.appendChild(styles[styleIndex]); + } + + placemark.appendChild(style); + } + if (geometries.length === 1) { placemark.appendChild(geometries[0]); } else if (geometries.length > 1) { var multigeometry = kmlDoc.createElement('MultiGeometry'); - for (var j = 0; j < geometryCount; ++j) { - multigeometry.appendChild(geometries[j]); + for (var geometryIndex = 0; geometryIndex < geometryCount; ++geometryIndex) { + multigeometry.appendChild(geometries[geometryIndex]); } placemark.appendChild(multigeometry); } @@ -130,7 +150,7 @@ define([ var scratchCartesian3 = new Cartesian3(); var scratchCartographic = new Cartographic(); - function createPoint(that, entity, geometry, geometries) { + function createPoint(that, entity, geometry, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; @@ -157,12 +177,31 @@ define([ } pointGeometry.appendChild(coordinates); - geometries.push(pointGeometry); + + // Create style + // TODO: Create style cache and use styleUri instead + var iconStyle = (geometry instanceof BillboardGraphics) ? + createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); + styles.push(iconStyle); } - function createIconStyleFromPoint(pointGraphics) { + function createIconStyleFromPoint(that, pointGraphics) { + var kmlDoc = that._kmlDoc; + var iconStyle = kmlDoc.createElement('IconStyle'); + + var color = getValue(pointGraphics.color); + if (defined(color)) { + color = colorToString(color); + + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); + } + var pixelSize = getValue(pointGraphics.pixelSize); + if (defined(pixelSize)) { + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', pixelSize / BILLBOARD_SIZE)); + } } function createIconStyleFromBillboard(that, billboardGraphics) { @@ -239,14 +278,18 @@ define([ var rotation = getValue(billboardGraphics.rotation); var alignedAxis = getValue(billboardGraphics.alignedAxis); if (defined(rotation) && alignedAxis === Cartesian3.UNIT_Z) { + rotation = Math.toDegrees(-rotation); if (rotation === 0) { rotation = 360; } - rotation = Math.toDegrees(-rotation); + + iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'heading', rotation)); } + + return iconStyle; } - function createLineString(that, polyline, geometries) { + function createLineString(that, polyline, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; @@ -258,7 +301,7 @@ define([ geometries.push(lineStringGeometry); } - function createPolygon(that, polygonOrRectangle, geometries) { + function createPolygon(that, polygonOrRectangle, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; @@ -273,7 +316,7 @@ define([ function getAltitudeMode(kmlDoc, heightReferenceProperty) { // TODO: Time dynamic var heightReference = defined(heightReferenceProperty) ? - heightReferenceProperty.getValue(Iso8601.MINIMUM_VALUE) : HeightReference.NONE; + heightReferenceProperty.getValue(Iso8601.MINIMUM_VALUE) : HeightReference.CLAMP_TO_GROUND; var altitudeModeText; switch (heightReference) { case HeightReference.NONE: @@ -290,7 +333,7 @@ define([ return altitudeModeText; } - function getCoordinates(kmlDoc, coordinates, ellipsoid) { + function getCoordinates(coordinates, ellipsoid) { if (!Array.isArray(coordinates)) { coordinates = [coordinates]; } @@ -308,16 +351,18 @@ define([ } function createBasicElementWithText(kmlDoc, elementName, elementValue, namespace) { + elementValue = defaultValue(elementValue, ''); + if (typeof elementValue === 'boolean') { elementValue = elementValue ? '1' : '0'; } - if (elementValue.indexOf('<') !== -1) { - elementValue = ''; - } - + // Create element with optional namespace var element = defined(namespace) ? kmlDoc.createElementNS(namespace, elementName) : kmlDoc.createElement(elementName); - var text = kmlDoc.createTextNode(elementValue); + + // Wrap value in CDATA section if it contains HTML + var text = ((elementValue === 'string') && (elementValue.indexOf('<') !== -1)) ? + kmlDoc.createCDATASection(elementValue) : kmlDoc.createTextNode(elementValue); element.appendChild(text); diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index a61983a31e71..dd0db26cab9a 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -14,26 +14,40 @@ defineSuite([ pollToPromise) { 'use strict'; -var options = { - camera : { - positionWC : new Cartesian3(0.0, 0.0, 0.0), - directionWC : new Cartesian3(0.0, 0.0, 1.0), - upWC : new Cartesian3(0.0, 1.0, 0.0), - pitch : 0.0, - heading : 0.0, - frustum : new PerspectiveFrustum(), - computeViewRectangle : function() { - return Rectangle.MAX_VALUE; - }, - pickEllipsoid : function() { - return undefined; + function download(filename, data) { + var blob = new Blob([data], {type: 'application/xml'}); + if(window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else { + var elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); } - }, - canvas : { - clientWidth : 512, - clientHeight : 512 } -}; + + var options = { + camera : { + positionWC : new Cartesian3(0.0, 0.0, 0.0), + directionWC : new Cartesian3(0.0, 0.0, 1.0), + upWC : new Cartesian3(0.0, 1.0, 0.0), + pitch : 0.0, + heading : 0.0, + frustum : new PerspectiveFrustum(), + computeViewRectangle : function() { + return Rectangle.MAX_VALUE; + }, + pickEllipsoid : function() { + return undefined; + } + }, + canvas : { + clientWidth : 512, + clientHeight : 512 + } + }; it('test', function() { return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) @@ -41,7 +55,7 @@ var options = { var exporter = new KmlExporter(datasource.entities); var kml = exporter.toString(); - console.log(kml); + download('test.kml', kml); }); }); From f83a20741b9640ce69211ff945e4a6dc52d723b7 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Tue, 21 May 2019 14:29:49 -0400 Subject: [PATCH 04/27] First cut off LineStrings with materials --- Source/DataSources/KmlExporter.js | 88 ++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 0dea691cdd76..010c19513452 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -1,8 +1,11 @@ define([ './BillboardGraphics', + './ColorMaterialProperty', + './PolylineOutlineMaterialProperty', '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartographic', + '../Core/Color', '../Core/defaultValue', '../Core/defined', '../Core/Ellipsoid', @@ -14,9 +17,12 @@ define([ '../Scene/VerticalOrigin' ], function( BillboardGraphics, + ColorMaterialProperty, + PolylineOutlineMaterialProperty, Cartesian2, Cartesian3, Cartographic, + Color, defaultValue, defined, Ellipsoid, @@ -289,16 +295,66 @@ define([ return iconStyle; } - function createLineString(that, polyline, geometries, styles) { + function createLineString(that, polylineGraphics, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; - if (!defined(polyline)) { + if (!defined(polylineGraphics)) { return; } var lineStringGeometry = kmlDoc.createElement('LineString'); + + // Set altitude mode + var altitudeMode = kmlDoc.createElement('altitudeMode'); + var clampToGround = getValue(polylineGraphics.clampToGround, false); + var altitudeModeText; + if (clampToGround) { + lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tesselate', true)); + altitudeModeText = kmlDoc.createTextNode('clampToGround'); + } else { + altitudeModeText = kmlDoc.createTextNode('absolute'); + } + altitudeMode.appendChild(altitudeModeText); + lineStringGeometry.appendChild(altitudeMode); + + // Set coordinates + var coordinates; + var positionsProperty = polylineGraphics.positions; + if (positionsProperty.isConstant) { + var cartesians = positionsProperty.getValue(Iso8601.MINIMUM_VALUE); + coordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(cartesians, ellipsoid)); + } else { + // TODO: Time dynamic + } + + // Set draw order + var zIndex = getValue(polylineGraphics.zIndex); + if (clampToGround && defined(zIndex)) { + lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); + } + + lineStringGeometry.appendChild(coordinates); geometries.push(lineStringGeometry); + + // Create style + // TODO: Create style cache and use styleUri instead + var lineStyle = kmlDoc.createElement('LineStyle'); + + var width = getValue(polylineGraphics.width); + if (defined(width)) { + lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); + } + + var material = getValue(polylineGraphics.material); + if (defined(material)) { + processMaterial(that, material, lineStyle); + } + + // TODO: and gx:labelVisibility> + + styles.push(lineStyle); } function createPolygon(that, polygonOrRectangle, geometries, styles) { @@ -313,6 +369,34 @@ define([ geometries.push(polygonGeometry); } + function processMaterial(that, material, style) { + var kmlDoc = that._kmlDoc; + + var color; + if (material instanceof ColorMaterialProperty) { + color = getValue(material.color, Color.WHITE); + color = colorToString(color); + + style.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); + style.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); + } else if (material instanceof PolylineOutlineMaterialProperty) { + color = getValue(material.color, Color.WHITE); + color = colorToString(color); + + var outlineColor = getValue(material.outlineColor, Color.BLACK); + outlineColor = colorToString(outlineColor); + + var outlineWidth = getValue(material.outlineWidth, 1.0); + + style.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); + style.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); + style.appendChild(createBasicElementWithText(kmlDoc, 'outerColor', outlineColor, gxNamespace)); + style.appendChild(createBasicElementWithText(kmlDoc, 'outerWidth', outlineWidth, gxNamespace)); + } + + // TODO: Other materials + } + function getAltitudeMode(kmlDoc, heightReferenceProperty) { // TODO: Time dynamic var heightReference = defined(heightReferenceProperty) ? From e498f177cb38eb6472903d64cf6760aa0c414e2c Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Wed, 22 May 2019 15:50:00 -0400 Subject: [PATCH 05/27] Added support for Rectangle and Polygon graphics. --- Source/DataSources/KmlExporter.js | 205 ++++++++++++++++++++++++++---- 1 file changed, 183 insertions(+), 22 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 010c19513452..592539f8d5e5 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -1,7 +1,13 @@ define([ './BillboardGraphics', './ColorMaterialProperty', + './CompositeMaterialProperty', + './GridMaterialProperty', + './ImageMaterialProperty', + './PolylineGlowMaterialProperty', './PolylineOutlineMaterialProperty', + './StripeMaterialProperty', + './RectangleGraphics', '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartographic', @@ -11,6 +17,7 @@ define([ '../Core/Ellipsoid', '../Core/Iso8601', '../Core/Math', + '../Core/Rectangle', '../Core/Resource', '../Scene/HeightReference', '../Scene/HorizontalOrigin', @@ -18,7 +25,13 @@ define([ ], function( BillboardGraphics, ColorMaterialProperty, + CompositeMaterialProperty, + GridMaterialProperty, + ImageMaterialProperty, + PolylineGlowMaterialProperty, PolylineOutlineMaterialProperty, + StripeMaterialProperty, + RectangleGraphics, Cartesian2, Cartesian3, Cartographic, @@ -28,6 +41,7 @@ define([ Ellipsoid, Iso8601, CesiumMath, + Rectangle, Resource, HeightReference, HorizontalOrigin, @@ -105,8 +119,8 @@ define([ createPoint(that, entity, entity.point, geometries, styles); createPoint(that, entity, entity.billboard, geometries, styles); createLineString(that, entity.polyline, geometries, styles); - createPolygon(that, entity.polygon, geometries, styles); createPolygon(that, entity.rectangle, geometries, styles); + createPolygon(that, entity.polygon, geometries, styles); // TODO: Handle the rest of the geometries @@ -123,6 +137,7 @@ define([ var styleCount = styles.length; if (styleCount > 0) { + // TODO: Merge if we went up with multiple of same type var style = kmlDoc.createElement('Style'); for (var styleIndex = 0; styleIndex < geometryCount; ++styleIndex) { style.appendChild(styles[styleIndex]); @@ -328,6 +343,7 @@ define([ } else { // TODO: Time dynamic } + lineStringGeometry.appendChild(coordinates); // Set draw order var zIndex = getValue(polylineGraphics.zIndex); @@ -335,7 +351,6 @@ define([ lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); } - lineStringGeometry.appendChild(coordinates); geometries.push(lineStringGeometry); // Create style @@ -357,44 +372,190 @@ define([ styles.push(lineStyle); } - function createPolygon(that, polygonOrRectangle, geometries, styles) { + function getRectangleBoundaries(kmlDoc, rectangleGraphics) { + var coordinates; + var height = getValue(rectangleGraphics.height, 0.0); + + var coordinatesProperty = rectangleGraphics.coordinates; + if (coordinatesProperty.isConstant) { + var rectangle = coordinatesProperty.getValue(Iso8601.MINIMUM_VALUE); + + var coordinateStrings = []; + var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest]; + + for (var i=0;i<4;++i) { + cornerFunction[i](rectangle, scratchCartographic); + coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' + + CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height); + } + + coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' ')); + } else { + // TODO: Time dynamic + } + + var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs'); + var linearRing = kmlDoc.createElement('LinearRing'); + linearRing.appendChild(coordinates); + outerBoundaryIs.appendChild(linearRing); + + return [outerBoundaryIs]; + } + + function getLinearRing(that, positions, height, perPositionHeight) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; - if (!defined(polygonOrRectangle)) { + var coordinateStrings = []; + var positionCount = positions.length; + for (var i=0;i Date: Fri, 24 May 2019 10:22:21 -0400 Subject: [PATCH 06/27] Started adding time support --- Source/DataSources/KmlExporter.js | 101 ++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 592539f8d5e5..d2b200acefde 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -2,10 +2,13 @@ define([ './BillboardGraphics', './ColorMaterialProperty', './CompositeMaterialProperty', + './CompositePositionProperty', './GridMaterialProperty', './ImageMaterialProperty', './PolylineGlowMaterialProperty', './PolylineOutlineMaterialProperty', + './SampledPositionProperty', + './SampledProperty', './StripeMaterialProperty', './RectangleGraphics', '../Core/Cartesian2', @@ -16,6 +19,7 @@ define([ '../Core/defined', '../Core/Ellipsoid', '../Core/Iso8601', + '../Core/JulianDate', '../Core/Math', '../Core/Rectangle', '../Core/Resource', @@ -26,10 +30,13 @@ define([ BillboardGraphics, ColorMaterialProperty, CompositeMaterialProperty, + CompositePositionProperty, GridMaterialProperty, ImageMaterialProperty, PolylineGlowMaterialProperty, PolylineOutlineMaterialProperty, + SampledPositionProperty, + SampledProperty, StripeMaterialProperty, RectangleGraphics, Cartesian2, @@ -40,6 +47,7 @@ define([ defined, Ellipsoid, Iso8601, + JulianDate, CesiumMath, Rectangle, Resource, @@ -80,11 +88,13 @@ define([ * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file + * @param {JulianDate} [options.time=JulianDate.fromIso8601(Iso8601.MINIMUM_VALUE)] The time value to use to get properties that are not time varying in KML * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas */ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); + this._time = defined(options.time) ? options.time.toIso8601() : Iso8601.MINIMUM_VALUE; // TODO this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); var kmlDoc = this._kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml'); @@ -129,9 +139,14 @@ define([ var placemark = kmlDoc.createElement('Placemark'); placemark.setAttribute('id', entity.id); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', getValue(entity.name))); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', getValue(entity.show, true))); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', getValue(entity.description))); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); + + var availability = entity.availability; + if (defined(availability)) { + // TODO: TimeSpan + } parentNode.appendChild(placemark); @@ -179,13 +194,6 @@ define([ return; } - var pointGeometry = kmlDoc.createElement('Point'); - - // Set altitude mode - var altitudeMode = kmlDoc.createElement('altitudeMode'); - altitudeMode.appendChild(getAltitudeMode(kmlDoc, geometry.heightReference)); - pointGeometry.appendChild(altitudeMode); - // Set coordinates var coordinates; var positionProperty = entity.position; @@ -193,10 +201,21 @@ define([ positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); coordinates = createBasicElementWithText(kmlDoc, 'coordinates', getCoordinates(scratchCartesian3, ellipsoid)); + } else if (positionProperty instanceof SampledPositionProperty || positionProperty instanceof SampledProperty) { + return createTrack(that, entity, geometries, styles); + } else if (positionProperty instanceof CompositePositionProperty) { + // TODO: Multitrack } else { - // TODO: Time dynamic + // TODO: Something else time dynamic } + var pointGeometry = kmlDoc.createElement('Point'); + + // Set altitude mode + var altitudeMode = kmlDoc.createElement('altitudeMode'); + altitudeMode.appendChild(getAltitudeMode(kmlDoc, geometry.heightReference)); + pointGeometry.appendChild(altitudeMode); + pointGeometry.appendChild(coordinates); geometries.push(pointGeometry); @@ -207,6 +226,52 @@ define([ styles.push(iconStyle); } + function createTrack(that, entity, geometry, geometries, styles) { + var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; + + var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track'); + + var positions = entity.position; + var positionTimes = positions._times; + var positionValues = positions._values; + var count = positionTimes.length; + + for (var i = 0; i < count; ++i) { + var when = createBasicElementWithText(kmlDoc, 'when', JulianDate.toIso8601(positionTimes[i])); + var coord = createBasicElementWithText(kmlDoc, 'coord', getCoordinates(positionValues[i], ellipsoid), gxNamespace); + + trackGeometry.appendChild(when); + trackGeometry.appendChild(coord); + } + + geometries.push(trackGeometry); + + // Create style + // TODO: Create style cache and use styleUri instead + var iconStyle = (geometry instanceof BillboardGraphics) ? + createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); + styles.push(iconStyle); + + // See if we have a line that needs to be drawn + var path = entity.path; + if (defined(path)) { + var width = getValue(path.width); + var material = getValue(path.material); + if (defined(material) || defined(width)) { + var lineStyle = kmlDoc.createElement('LineStyle'); + if (defined(width)) { + lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); + } + + if (defined(material)) { + processMaterial(that, material, lineStyle); + } + styles.push(lineStyle); + } + } + } + function createIconStyleFromPoint(that, pointGraphics) { var kmlDoc = that._kmlDoc; var iconStyle = kmlDoc.createElement('IconStyle'); @@ -383,7 +448,7 @@ define([ var coordinateStrings = []; var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest]; - for (var i=0;i<4;++i) { + for (var i = 0; i < 4; ++i) { cornerFunction[i](rectangle, scratchCartographic); coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' + CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height); @@ -408,11 +473,11 @@ define([ var coordinateStrings = []; var positionCount = positions.length; - for (var i=0;i=0;--i) { + for (var i = 3; i >= 0; --i) { result += (bytes[i] < 16) ? ('0' + bytes[i].toString(16)) : bytes[i].toString(16); } From 9dd947e65c334082e1cec98e3edaec00af3e1f4f Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Fri, 24 May 2019 16:43:18 -0400 Subject: [PATCH 07/27] Got style caching working --- Source/DataSources/KmlExporter.js | 197 +++++++++++++++++++----------- 1 file changed, 129 insertions(+), 68 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index d2b200acefde..fd24f9405904 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -81,6 +81,62 @@ define([ return ''; } + function ValueGetter(time) { + this._time = time; + } + + ValueGetter.prototype.get = function(property, defaultVal) { + var value; + if (defined(property)) { + value = property.getValue(this._time); + } + + return defaultValue(value, defaultVal); + }; + + ValueGetter.prototype.getColor = function(property, defaultVal) { + var result = this.get(property, defaultVal); + if (defined(result)) { + return colorToString(result); + } + }; + + function StyleCache() { + this._ids = {}; + this._styles = {}; + this._count = 0; + } + + StyleCache.prototype.get = function(element) { + // TODO: Recursively sort for better caching + + var ids = this._ids; + var key = element.innerHTML; // TODO: Maybe use hash + if (defined(ids[key])) { + return ids[key]; + } + + var styleId = 'style-' + (++this._count); + element.setAttribute('id', styleId); + + // Store with # + styleId = '#' + styleId; + ids[key] = styleId; + this._styles[key] = element; + + return styleId; + }; + + StyleCache.prototype.save = function(parentElement) { + var styles = this._styles; + + for (var key in styles) { + if (styles.hasOwnProperty(key)) { + parentElement.appendChild(styles[key]); + } + } + }; + /** * @alias KmlExporter * @constructor @@ -94,7 +150,8 @@ define([ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); - this._time = defined(options.time) ? options.time.toIso8601() : Iso8601.MINIMUM_VALUE; // TODO + this._valueGetter = new ValueGetter(defined(options.time) ? options.time.toIso8601() : Iso8601.MINIMUM_VALUE); + var styleCache = this._styleCache = new StyleCache(); this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); var kmlDoc = this._kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml'); @@ -108,6 +165,8 @@ define([ return !defined(entity.parent); }); recurseEntities(this, kmlDocumentElement, rootEntities); + + styleCache.save(kmlDocumentElement); } KmlExporter.prototype.toString = function() { @@ -117,6 +176,7 @@ define([ function recurseEntities(that, parentNode, entities) { var kmlDoc = that._kmlDoc; + var styleCache = that._styleCache; var count = entities.length; var geometries; @@ -145,7 +205,13 @@ define([ var availability = entity.availability; if (defined(availability)) { - // TODO: TimeSpan + var timeSpan = kmlDoc.createElement('TimeSpan'); + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin', + JulianDate.toIso8601(availability.start))); + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end', + JulianDate.toIso8601(availability.stop))); + + placemark.appendChild(timeSpan); } parentNode.appendChild(placemark); @@ -153,12 +219,13 @@ define([ var styleCount = styles.length; if (styleCount > 0) { // TODO: Merge if we went up with multiple of same type + // TODO: Multigeometries may need to be split up var style = kmlDoc.createElement('Style'); for (var styleIndex = 0; styleIndex < geometryCount; ++styleIndex) { style.appendChild(styles[styleIndex]); } - placemark.appendChild(style); + placemark.appendChild(createBasicElementWithText(kmlDoc, 'styleUrl', styleCache.get(style))); } if (geometries.length === 1) { @@ -213,14 +280,13 @@ define([ // Set altitude mode var altitudeMode = kmlDoc.createElement('altitudeMode'); - altitudeMode.appendChild(getAltitudeMode(kmlDoc, geometry.heightReference)); + altitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); pointGeometry.appendChild(altitudeMode); pointGeometry.appendChild(coordinates); geometries.push(pointGeometry); // Create style - // TODO: Create style cache and use styleUri instead var iconStyle = (geometry instanceof BillboardGraphics) ? createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); styles.push(iconStyle); @@ -229,6 +295,7 @@ define([ function createTrack(that, entity, geometry, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; + var valueGetter = that._valueGetter; var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track'); @@ -248,7 +315,6 @@ define([ geometries.push(trackGeometry); // Create style - // TODO: Create style cache and use styleUri instead var iconStyle = (geometry instanceof BillboardGraphics) ? createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); styles.push(iconStyle); @@ -256,17 +322,15 @@ define([ // See if we have a line that needs to be drawn var path = entity.path; if (defined(path)) { - var width = getValue(path.width); - var material = getValue(path.material); + var width = valueGetter.get(path.width); + var material = path.material; if (defined(material) || defined(width)) { var lineStyle = kmlDoc.createElement('LineStyle'); if (defined(width)) { lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); } - if (defined(material)) { - processMaterial(that, material, lineStyle); - } + processMaterial(that, material, lineStyle); styles.push(lineStyle); } } @@ -274,9 +338,11 @@ define([ function createIconStyleFromPoint(that, pointGraphics) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + var iconStyle = kmlDoc.createElement('IconStyle'); - var color = getValue(pointGraphics.color); + var color = valueGetter.get(pointGraphics.color); if (defined(color)) { color = colorToString(color); @@ -284,7 +350,7 @@ define([ iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); } - var pixelSize = getValue(pointGraphics.pixelSize); + var pixelSize = valueGetter.get(pointGraphics.pixelSize); if (defined(pixelSize)) { iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', pixelSize / BILLBOARD_SIZE)); } @@ -292,16 +358,18 @@ define([ function createIconStyleFromBillboard(that, billboardGraphics) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + var iconStyle = kmlDoc.createElement('IconStyle'); - var image = getValue(billboardGraphics.image); + var image = valueGetter.get(billboardGraphics.image); if (defined(image)) { image = that._textureCallback(image); var icon = kmlDoc.createElement('Icon'); icon.appendChild(createBasicElementWithText(kmlDoc, 'href', image)); - var imageSubRegion = getValue(billboardGraphics.imageSubRegion); + var imageSubRegion = valueGetter.get(billboardGraphics.imageSubRegion); if (defined(imageSubRegion)) { icon.appendChild(createBasicElementWithText(kmlDoc, 'x', imageSubRegion.x, gxNamespace)); icon.appendChild(createBasicElementWithText(kmlDoc, 'y', imageSubRegion.y, gxNamespace)); @@ -312,30 +380,28 @@ define([ iconStyle.appendChild(icon); } - var color = getValue(billboardGraphics.color); + var color = valueGetter.getColor(billboardGraphics.color); if (defined(color)) { - color = colorToString(color); - iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); } - var scale = getValue(billboardGraphics.scale, 1.0); + var scale = valueGetter.get(billboardGraphics.scale, 1.0); if (defined(scale)) { iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale)); } - var pixelOffset = getValue(billboardGraphics.pixelOffset); + var pixelOffset = valueGetter.get(billboardGraphics.pixelOffset); if (defined(pixelOffset)) { Cartesian2.divideByScalar(pixelOffset, scale, pixelOffset); - var width = getValue(billboardGraphics.width, BILLBOARD_SIZE); - var height = getValue(billboardGraphics.height, BILLBOARD_SIZE); + var width = valueGetter.get(billboardGraphics.width, BILLBOARD_SIZE); + var height = valueGetter.get(billboardGraphics.height, BILLBOARD_SIZE); // KML Hotspots are from the bottom left, but we work from the top left // Move to left - var horizontalOrigin = getValue(billboardGraphics.horizontalOrigin, HorizontalOrigin.CENTER); + var horizontalOrigin = valueGetter.get(billboardGraphics.horizontalOrigin, HorizontalOrigin.CENTER); if (horizontalOrigin === HorizontalOrigin.CENTER) { pixelOffset.x -= width * 0.5; } else if (horizontalOrigin === HorizontalOrigin.RIGHT) { @@ -343,7 +409,7 @@ define([ } // Move to bottom - var verticalOrigin = getValue(billboardGraphics.verticalOrigin, VerticalOrigin.CENTER); + var verticalOrigin = valueGetter.get(billboardGraphics.verticalOrigin, VerticalOrigin.CENTER); if (verticalOrigin === VerticalOrigin.TOP) { pixelOffset.y += height; } else if (verticalOrigin === VerticalOrigin.CENTER) { @@ -361,8 +427,8 @@ define([ // We can only specify heading so if axis isn't Z, then we skip the rotation // GE treats a heading of zero as no heading but can still point north using a 360 degree angle - var rotation = getValue(billboardGraphics.rotation); - var alignedAxis = getValue(billboardGraphics.alignedAxis); + var rotation = valueGetter.get(billboardGraphics.rotation); + var alignedAxis = valueGetter.get(billboardGraphics.alignedAxis); if (defined(rotation) && alignedAxis === Cartesian3.UNIT_Z) { rotation = Math.toDegrees(-rotation); if (rotation === 0) { @@ -378,6 +444,7 @@ define([ function createLineString(that, polylineGraphics, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; + var valueGetter = that._valueGetter; if (!defined(polylineGraphics)) { return; @@ -387,7 +454,7 @@ define([ // Set altitude mode var altitudeMode = kmlDoc.createElement('altitudeMode'); - var clampToGround = getValue(polylineGraphics.clampToGround, false); + var clampToGround = valueGetter.get(polylineGraphics.clampToGround, false); var altitudeModeText; if (clampToGround) { lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tesselate', true)); @@ -411,7 +478,7 @@ define([ lineStringGeometry.appendChild(coordinates); // Set draw order - var zIndex = getValue(polylineGraphics.zIndex); + var zIndex = valueGetter.get(polylineGraphics.zIndex); if (clampToGround && defined(zIndex)) { lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); } @@ -419,27 +486,26 @@ define([ geometries.push(lineStringGeometry); // Create style - // TODO: Create style cache and use styleUri instead var lineStyle = kmlDoc.createElement('LineStyle'); - var width = getValue(polylineGraphics.width); + var width = valueGetter.get(polylineGraphics.width); if (defined(width)) { lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); } - var material = getValue(polylineGraphics.material); - if (defined(material)) { - processMaterial(that, material, lineStyle); - } + processMaterial(that, polylineGraphics.material, lineStyle); // TODO: and gx:labelVisibility> styles.push(lineStyle); } - function getRectangleBoundaries(kmlDoc, rectangleGraphics) { + function getRectangleBoundaries(that, rectangleGraphics) { + var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + var coordinates; - var height = getValue(rectangleGraphics.height, 0.0); + var height = valueGetter.get(rectangleGraphics.height, 0.0); var coordinatesProperty = rectangleGraphics.coordinates; if (coordinatesProperty.isConstant) { @@ -489,9 +555,10 @@ define([ function getPolygonBoundaries(that, polygonGraphics) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; - var height = getValue(polygonGraphics.height, 0.0); - var perPositionHeight = getValue(polygonGraphics.perPositionHeight, false); + var height = valueGetter.get(polygonGraphics.height, 0.0); + var perPositionHeight = valueGetter.get(polygonGraphics.perPositionHeight, false); var boundaries = []; var hierarchyProperty = polygonGraphics.hierarchy; @@ -520,6 +587,7 @@ define([ function createPolygon(that, geometry, geometries, styles) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; if (!defined(geometry)) { return; @@ -531,7 +599,7 @@ define([ // Set boundaries var boundaries = (geometry instanceof RectangleGraphics) ? - getRectangleBoundaries(kmlDoc, geometry) : getPolygonBoundaries(kmlDoc, geometry); + getRectangleBoundaries(that, geometry) : getPolygonBoundaries(kmlDoc, geometry); var boundaryCount = boundaries.length; for (var i = 0; i < boundaryCount; ++i) { @@ -540,11 +608,11 @@ define([ // Set altitude mode var altitudeMode = kmlDoc.createElement('altitudeMode'); - altitudeMode.appendChild(getAltitudeMode(kmlDoc, geometry.heightReference)); + altitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); polygonGeometry.appendChild(altitudeMode); // Set draw order - var zIndex = getValue(geometry.zIndex); + var zIndex = valueGetter.get(geometry.zIndex); if (defined(zIndex)) { polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); } @@ -554,30 +622,26 @@ define([ geometries.push(polygonGeometry); // Create style - // TODO: Create style cache and use styleUri instead var polyStyle = kmlDoc.createElement('PolyStyle'); - var fill = getValue(geometry.fill, false); + var fill = valueGetter.get(geometry.fill, false); if (fill) { polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', fill)); } - var material = getValue(geometry.material); - if (defined(material)) { - processMaterial(that, material, polyStyle); - } + processMaterial(that, geometry.material, polyStyle); - var outline = getValue(geometry.outline, false); + var outline = valueGetter.get(geometry.outline, false); if (outline) { polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'outline', outline)); // Outline uses LineStyle var lineStyle = kmlDoc.createElement('LineStyle'); - var outlineWidth = getValue(geometry.outlineWidth, 1.0); + var outlineWidth = valueGetter.get(geometry.outlineWidth, 1.0); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', outlineWidth)); - var outlineColor = getValue(geometry.outlineColor, Color.BLACK); + var outlineColor = valueGetter.get(geometry.outlineColor, Color.BLACK); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', outlineColor)); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); @@ -589,8 +653,17 @@ define([ function processMaterial(that, materialProperty, style) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + + if (!defined(materialProperty)) { + return; + } + + var material = valueGetter.get(materialProperty); + if (!defined(material)) { + return; + } - var material = getValue(materialProperty); var color; if (materialProperty instanceof ColorMaterialProperty) { color = colorToString(material.color); @@ -623,10 +696,11 @@ define([ } } - function getAltitudeMode(kmlDoc, heightReferenceProperty) { - // TODO: Time dynamic - var heightReference = defined(heightReferenceProperty) ? - heightReferenceProperty.getValue(Iso8601.MINIMUM_VALUE) : HeightReference.CLAMP_TO_GROUND; + function getAltitudeMode(that, heightReferenceProperty) { + var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + + var heightReference = valueGetter.get(heightReferenceProperty, HeightReference.CLAMP_TO_GROUND); var altitudeModeText; switch (heightReference) { case HeightReference.NONE: @@ -689,18 +763,5 @@ define([ return result; } - function getValue(property, defaultVal) { - var value; - if (defined(property)) { - if (property.isConstant) { - value = property.getValue(Iso8601.MINIMUM_VALUE); - } else { - // TODO - } - } - - return defaultValue(value, defaultVal); - } - return KmlExporter; }); From 9ed5562c044dda0913cfcb696e6b8e2d75a6e7d6 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Wed, 29 May 2019 17:56:07 -0400 Subject: [PATCH 08/27] Beginnings of test helper functions --- Source/DataSources/KmlExporter.js | 10 +- Specs/DataSources/KmlExporterSpec.js | 150 +++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index fd24f9405904..e0924aa5fe63 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -130,6 +130,7 @@ define([ StyleCache.prototype.save = function(parentElement) { var styles = this._styles; + // TODO: Put in beginning for (var key in styles) { if (styles.hasOwnProperty(key)) { parentElement.appendChild(styles[key]); @@ -242,7 +243,12 @@ define([ var children = entity._children; if (children.length > 0) { var folderNode = kmlDoc.createElement('Folder'); - folderNode.setAttribute('name', entity.name); + // TODO: Maybe id, but can't duplicate if there is a Placemark + + folderNode.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + folderNode.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); + folderNode.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); + parentNode.appendChild(folderNode); recurseEntities(that, folderNode, children); @@ -354,6 +360,8 @@ define([ if (defined(pixelSize)) { iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', pixelSize / BILLBOARD_SIZE)); } + + return iconStyle; } function createIconStyleFromBillboard(that, billboardGraphics) { diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index dd0db26cab9a..c8b03b0e17c6 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -1,15 +1,21 @@ defineSuite([ 'DataSources/KmlExporter', 'Core/Cartesian3', + 'Core/defined', 'Core/PerspectiveFrustum', 'Core/Rectangle', + 'DataSources/Entity', + 'DataSources/EntityCollection', 'DataSources/KmlDataSource', 'Specs/pollToPromise' ], function( KmlExporter, Cartesian3, + defined, PerspectiveFrustum, Rectangle, + Entity, + EntityCollection, KmlDataSource, pollToPromise) { 'use strict'; @@ -59,4 +65,148 @@ defineSuite([ }); }); + function checkTagWithProperties(element, properties) { + // TODO: Maybe check child length + + var childNodes = element.childNodes; + for (var i=0;i + var kml = kmlDoc.documentElement; + var kmlChildNodes = kml.childNodes; + expect(kml.localName).toEqual('kml'); + expect(kmlChildNodes.length).toBe(1); + + var hierarchy = { + Document: { + children: { + Style: { + attributes: { + id: 'style-1' + } + }, + Folder: { + attributes: { + id: entity1.id + }, + children: { + name: entity1.name, + visibility: '0', + description: entity1.description, + Folder: { + attributes: { + id: entity2.id + }, + children: { + name: entity2.name, + visibility: '1', + description: entity2.description, + Placemark: { + attributes: { + id: entity3.id + }, + children: { + Point: { + + } + }, + name: entity3.name, + visibility: '1', + description: entity3.description + } + } + } + } + } + } + } + }; + + checkTagWithProperties(kml, hierarchy); + }); + + describe('Points', function() { + it('Constant Postion', function() { + var entities = new EntityCollection(); + + var entity1 = new Entity({ + id: 'e1', + name: 'entity1', + show: false, + description: 'This is an entity' + }); + + entities.add(); + }); + + it('Altitude modes', function() { + + }); + }); + + describe('Billboards', function() { + + }); + + describe('Polylines', function() { + + }); + + describe('Rectangles', function() { + + }); + + describe('Polygons', function() { + + }); }); From 0421376d8c1cdc5e3dff42991560ed4c974dc5e4 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Thu, 30 May 2019 17:58:13 -0400 Subject: [PATCH 09/27] More testing --- Source/DataSources/KmlExporter.js | 6 +- Specs/DataSources/KmlExporterSpec.js | 372 ++++++++++++++++----------- 2 files changed, 230 insertions(+), 148 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index e0924aa5fe63..b19099d7ce35 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -201,6 +201,7 @@ define([ placemark.setAttribute('id', entity.id); placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + // TODO: Graphics show property placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); @@ -243,7 +244,10 @@ define([ var children = entity._children; if (children.length > 0) { var folderNode = kmlDoc.createElement('Folder'); - // TODO: Maybe id, but can't duplicate if there is a Placemark + // The Placemark and Folder can't have the same ID + if (geometryCount === 0) { + folderNode.setAttribute('id', entity.id); + } folderNode.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); folderNode.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index c8b03b0e17c6..277eec3c5ca2 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -1,212 +1,290 @@ defineSuite([ 'DataSources/KmlExporter', 'Core/Cartesian3', + 'Core/Color', 'Core/defined', + 'Core/Iso8601', + 'Core/Math', 'Core/PerspectiveFrustum', 'Core/Rectangle', 'DataSources/Entity', 'DataSources/EntityCollection', 'DataSources/KmlDataSource', + 'Scene/HeightReference', 'Specs/pollToPromise' ], function( KmlExporter, Cartesian3, + Color, defined, + Iso8601, + CesiumMath, PerspectiveFrustum, Rectangle, Entity, EntityCollection, KmlDataSource, + HeightReference, pollToPromise) { -'use strict'; - - function download(filename, data) { - var blob = new Blob([data], {type: 'application/xml'}); - if(window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveBlob(blob, filename); - } else { - var elem = window.document.createElement('a'); - elem.href = window.URL.createObjectURL(blob); - elem.download = filename; - document.body.appendChild(elem); - elem.click(); - document.body.removeChild(elem); + 'use strict'; + + function download(filename, data) { + var blob = new Blob([data], { type: 'application/xml' }); + if (window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveBlob(blob, filename); + } else { + var elem = window.document.createElement('a'); + elem.href = window.URL.createObjectURL(blob); + elem.download = filename; + document.body.appendChild(elem); + elem.click(); + document.body.removeChild(elem); + } } - } - - var options = { - camera : { - positionWC : new Cartesian3(0.0, 0.0, 0.0), - directionWC : new Cartesian3(0.0, 0.0, 1.0), - upWC : new Cartesian3(0.0, 1.0, 0.0), - pitch : 0.0, - heading : 0.0, - frustum : new PerspectiveFrustum(), - computeViewRectangle : function() { - return Rectangle.MAX_VALUE; + + var options = { + camera: { + positionWC: new Cartesian3(0.0, 0.0, 0.0), + directionWC: new Cartesian3(0.0, 0.0, 1.0), + upWC: new Cartesian3(0.0, 1.0, 0.0), + pitch: 0.0, + heading: 0.0, + frustum: new PerspectiveFrustum(), + computeViewRectangle: function() { + return Rectangle.MAX_VALUE; + }, + pickEllipsoid: function() { + return undefined; + } }, - pickEllipsoid : function() { - return undefined; + canvas: { + clientWidth: 512, + clientHeight: 512 } - }, - canvas : { - clientWidth : 512, - clientHeight : 512 - } - }; + }; - it('test', function() { - return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) - .then(function(datasource) { - var exporter = new KmlExporter(datasource.entities); + xit('test', function() { + return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) + .then(function(datasource) { + var exporter = new KmlExporter(datasource.entities); - var kml = exporter.toString(); - download('test.kml', kml); - }); - }); + var kml = exporter.toString(); + download('test.kml', kml); + }); + }); - function checkTagWithProperties(element, properties) { - // TODO: Maybe check child length + function checkTagWithProperties(element, properties) { + var numberOfProperties = Object.keys(properties).length; + var attributes = properties._; + if (defined(attributes)) { + --numberOfProperties; // Attributes - var childNodes = element.childNodes; - for (var i=0;i - var kml = kmlDoc.documentElement; - var kmlChildNodes = kml.childNodes; - expect(kml.localName).toEqual('kml'); - expect(kmlChildNodes.length).toBe(1); + // + var kml = kmlDoc.documentElement; + var kmlChildNodes = kml.childNodes; + expect(kml.localName).toEqual('kml'); + expect(kmlChildNodes.length).toBe(1); - var hierarchy = { - Document: { - children: { + var hierarchy = { + Document: { Style: { - attributes: { + _: { id: 'style-1' - } + }, + IconStyle: {} }, Folder: { - attributes: { + _: { id: entity1.id }, - children: { - name: entity1.name, - visibility: '0', - description: entity1.description, - Folder: { - attributes: { - id: entity2.id + name: entity1.name, + visibility: '0', + description: entity1.description, + Folder: { + _: { + id: entity2.id + }, + name: entity2.name, + visibility: '1', + description: entity2.description, + Placemark: { + _: { + id: entity3.id + }, + Point: { + altitudeMode: 'clampToGround', + coordinates: [-75.59777, 40.03883, 0] }, - children: { - name: entity2.name, - visibility: '1', - description: entity2.description, - Placemark: { - attributes: { - id: entity3.id - }, - children: { - Point: { - - } - }, - name: entity3.name, - visibility: '1', - description: entity3.description - } - } + name: entity3.name, + visibility: '1', + description: entity3.description, + styleUrl: '#style-1' } } } } - } - }; + }; - checkTagWithProperties(kml, hierarchy); - }); + checkTagWithProperties(kml, hierarchy); + }); - describe('Points', function() { - it('Constant Postion', function() { - var entities = new EntityCollection(); + describe('Points', function() { + it('Constant Postion', function() { + var entities = new EntityCollection(); - var entity1 = new Entity({ - id: 'e1', - name: 'entity1', - show: false, - description: 'This is an entity' - }); + var entity1 = new Entity({ + id: 'e1', + name: 'entity1', + show: true, + description: 'This is an entity', + position: Cartesian3.fromDegrees(-75.59777, 40.03883, 12), + point: { + color: Color.LINEN, + pixelSize: 3, + heightReference: HeightReference.CLAMP_TO_GROUND + } + }); - entities.add(); - }); + entities.add(entity1); + + var kmlExporter = new KmlExporter(entities); + + var kmlDoc = kmlExporter._kmlDoc; + + // + var kml = kmlDoc.documentElement; + var kmlChildNodes = kml.childNodes; + expect(kml.localName).toEqual('kml'); + expect(kmlChildNodes.length).toBe(1); + + var hierarchy = { + Document: { + Style: { + _: { + id: 'style-1' + }, + IconStyle: { + color: 'ffe6f0fa', + colorMode: 'normal', + scale: 3 / 32 + } + }, + Placemark: { + _: { + id: entity1.id + }, + Point: { + altitudeMode: 'clampToGround', + coordinates: [-75.59777, 40.03883, 12] + }, + name: entity1.name, + visibility: '1', + description: entity1.description, + styleUrl: '#style-1' + } + } + }; + + checkTagWithProperties(kml, hierarchy); + }); - it('Altitude modes', function() { + it('Altitude modes', function() { + }); }); - }); - describe('Billboards', function() { + describe('Billboards', function() { - }); + }); - describe('Polylines', function() { + describe('Polylines', function() { - }); + }); - describe('Rectangles', function() { + describe('Rectangles', function() { - }); + }); - describe('Polygons', function() { + describe('Polygons', function() { + }); }); -}); From 2c7a19823232a84c48bb65658f41dde95d1b5072 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Wed, 5 Jun 2019 12:25:28 -0400 Subject: [PATCH 10/27] Verified all point/billboard exporting works as expected --- Source/DataSources/KmlExporter.js | 8 +- Specs/DataSources/KmlExporterSpec.js | 365 +++++++++++++++++++++------ 2 files changed, 298 insertions(+), 75 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index b19099d7ce35..4c743812f856 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -398,13 +398,15 @@ define([ iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); } - var scale = valueGetter.get(billboardGraphics.scale, 1.0); + var scale = valueGetter.get(billboardGraphics.scale); if (defined(scale)) { iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale)); } var pixelOffset = valueGetter.get(billboardGraphics.pixelOffset); if (defined(pixelOffset)) { + scale = defaultValue(scale, 1.0); + Cartesian2.divideByScalar(pixelOffset, scale, pixelOffset); var width = valueGetter.get(billboardGraphics.width, BILLBOARD_SIZE); @@ -441,8 +443,8 @@ define([ // GE treats a heading of zero as no heading but can still point north using a 360 degree angle var rotation = valueGetter.get(billboardGraphics.rotation); var alignedAxis = valueGetter.get(billboardGraphics.alignedAxis); - if (defined(rotation) && alignedAxis === Cartesian3.UNIT_Z) { - rotation = Math.toDegrees(-rotation); + if (defined(rotation) && Cartesian3.equals(Cartesian3.UNIT_Z, alignedAxis)) { + rotation = CesiumMath.toDegrees(-rotation); if (rotation === 0) { rotation = 360; } diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 277eec3c5ca2..8bdd7ebd51e0 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -1,5 +1,7 @@ defineSuite([ 'DataSources/KmlExporter', + 'Core/BoundingRectangle', + 'Core/Cartesian2', 'Core/Cartesian3', 'Core/Color', 'Core/defined', @@ -11,9 +13,12 @@ defineSuite([ 'DataSources/EntityCollection', 'DataSources/KmlDataSource', 'Scene/HeightReference', - 'Specs/pollToPromise' + 'Scene/HorizontalOrigin', + 'Scene/VerticalOrigin' ], function( KmlExporter, + BoundingRectangle, + Cartesian2, Cartesian3, Color, defined, @@ -25,7 +30,8 @@ defineSuite([ EntityCollection, KmlDataSource, HeightReference, - pollToPromise) { + HorizontalOrigin, + VerticalOrigin) { 'use strict'; function download(filename, data) { @@ -73,6 +79,15 @@ defineSuite([ }); }); + function checkKmlDoc(kmlDoc, properties) { + var kml = kmlDoc.documentElement; + var kmlChildNodes = kml.childNodes; + expect(kml.localName).toEqual('kml'); + expect(kmlChildNodes.length).toBe(1); + + checkTagWithProperties(kml, properties); + } + function checkTagWithProperties(element, properties) { var numberOfProperties = Object.keys(properties).length; var attributes = properties._; @@ -84,7 +99,14 @@ defineSuite([ for (var j = 0; j < elementAttributes.length; ++j) { var nodeAttribute = elementAttributes[j]; var attribute = attributes[nodeAttribute.name]; - expect(attribute).toEqual(nodeAttribute.value); + + if (typeof attribute === 'string') { + expect(nodeAttribute.value).toEqual(attribute); + } else if (typeof attribute === 'number') { + expect(Number(nodeAttribute.value)).toEqualEpsilon(attribute, CesiumMath.EPSILON7); + } else { + fail(); + } } } @@ -123,29 +145,68 @@ defineSuite([ } } + var counter = 0; + var expectedPointPosition = [-75.59777, 40.03883, 12]; + var pointPosition = Cartesian3.fromDegrees(expectedPointPosition[0], expectedPointPosition[1], expectedPointPosition[2]); + function createEntity(properties) { + ++counter; + var options = { + id: 'e' + counter, + name: 'entity' + counter, + show: true, + description: 'This is entity number ' + counter, + position: pointPosition + }; + + if (defined(properties)) { + for (var propertyName in properties) { + if (properties.hasOwnProperty(propertyName)) { + options[propertyName] = properties[propertyName]; + } + } + } + + return new Entity(options); + } + + function createExpectResult(entity) { + return { + Document: { + Style: { + _: { + id: 'style-1' + } + }, + Placemark: { + _: { + id: entity.id + }, + name: entity.name, + visibility: entity.show ? 1 : 0, + description: entity.description, + styleUrl: '#style-1' + } + } + }; + } + + beforeEach(function() { + counter = 0; + }); + it('Hierarchy', function() { - var entity1 = new Entity({ - id: 'e1', - name: 'entity1', + var entity1 = createEntity({ show: false, - description: 'This is an entity' + position: undefined }); - var entity2 = new Entity({ - id: 'e2', - name: 'entity2', - show: true, - description: 'This is another entity', + var entity2 = createEntity({ + position: undefined, parent: entity1 }); - var entity3 = new Entity({ - id: 'e3', - name: 'entity3', - show: true, - description: 'This is an entity with geometry', + var entity3 = createEntity({ parent: entity2, - position: Cartesian3.fromDegrees(-75.59777, 40.03883), point: {} }); @@ -154,16 +215,6 @@ defineSuite([ entities.add(entity2); entities.add(entity3); - var kmlExporter = new KmlExporter(entities); - - var kmlDoc = kmlExporter._kmlDoc; - - // - var kml = kmlDoc.documentElement; - var kmlChildNodes = kml.childNodes; - expect(kml.localName).toEqual('kml'); - expect(kmlChildNodes.length).toBe(1); - var hierarchy = { Document: { Style: { @@ -192,7 +243,7 @@ defineSuite([ }, Point: { altitudeMode: 'clampToGround', - coordinates: [-75.59777, 40.03883, 0] + coordinates: expectedPointPosition }, name: entity3.name, visibility: '1', @@ -204,19 +255,13 @@ defineSuite([ } }; - checkTagWithProperties(kml, hierarchy); + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, hierarchy); }); describe('Points', function() { it('Constant Postion', function() { - var entities = new EntityCollection(); - - var entity1 = new Entity({ - id: 'e1', - name: 'entity1', - show: true, - description: 'This is an entity', - position: Cartesian3.fromDegrees(-75.59777, 40.03883, 12), + var entity1 = createEntity({ point: { color: Color.LINEN, pixelSize: 3, @@ -224,56 +269,232 @@ defineSuite([ } }); + var entities = new EntityCollection(); entities.add(entity1); + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + color: 'ffe6f0fa', + colorMode: 'normal', + scale: 3 / 32 + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); - var kmlDoc = kmlExporter._kmlDoc; + it('Time-dynamic tracks', function() { + // TODO + }); + }); - // - var kml = kmlDoc.documentElement; - var kmlChildNodes = kml.childNodes; - expect(kml.localName).toEqual('kml'); - expect(kmlChildNodes.length).toBe(1); + describe('Billboards', function() { + it('Constant Postion', function() { + var entity1 = createEntity({ + billboard: { + image: 'http://test.invalid/image.jpg', + imageSubRegion: new BoundingRectangle(12,0,24,36), + color: Color.LINEN, + scale: 2, + pixelOffset: new Cartesian2(2, 3), + width: 24, + height: 36, + horizontalOrigin: HorizontalOrigin.LEFT, + verticalOrigin: VerticalOrigin.BOTTOM, + rotation: CesiumMath.toRadians(10), + alignedAxis: Cartesian3.UNIT_Z, + heightReference: HeightReference.CLAMP_TO_GROUND + } + }); - var hierarchy = { - Document: { - Style: { - _: { - id: 'style-1' - }, - IconStyle: { - color: 'ffe6f0fa', - colorMode: 'normal', - scale: 3 / 32 - } - }, - Placemark: { - _: { - id: entity1.id - }, - Point: { - altitudeMode: 'clampToGround', - coordinates: [-75.59777, 40.03883, 12] - }, - name: entity1.name, - visibility: '1', - description: entity1.description, - styleUrl: '#style-1' + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + Icon: { + href: 'http://test.invalid/image.jpg', + x: 12, + y: 0, + w: 24, + h: 36 + }, + color: 'ffe6f0fa', + colorMode: 'normal', + scale: 2, + hotSpot: { + _: { + x: -2 / 2, + y: 3 / 2, + xunits: 'pixels', + yunits: 'pixels' } + }, + heading: -10 + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Aligned Axis not Z', function() { + var entity1 = createEntity({ + billboard: { + rotation: CesiumMath.toRadians(10), + alignedAxis: Cartesian3.UNIT_Y } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = {}; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition }; - checkTagWithProperties(kml, hierarchy); + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('Altitude modes', function() { + it('0 degree heading should be 360', function() { + var entity1 = createEntity({ + billboard: { + rotation: CesiumMath.toRadians(0), + alignedAxis: Cartesian3.UNIT_Z + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + heading: 360 + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - }); - describe('Billboards', function() { + it('HotSpot Center', function() { + var entity1 = createEntity({ + billboard: { + pixelOffset: new Cartesian2(2, 3), + width: 24, + height: 36, + horizontalOrigin: HorizontalOrigin.CENTER, + verticalOrigin: VerticalOrigin.CENTER + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + hotSpot: { + _: { + x: -(2 - 12), + y: 3 + 18, + xunits: 'pixels', + yunits: 'pixels' + } + } + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('HotSpot TopRight', function() { + var entity1 = createEntity({ + billboard: { + pixelOffset: new Cartesian2(2, 3), + width: 24, + height: 36, + horizontalOrigin: HorizontalOrigin.RIGHT, + verticalOrigin: VerticalOrigin.TOP + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + hotSpot: { + _: { + x: -(2 - 24), + y: 3 + 36, + xunits: 'pixels', + yunits: 'pixels' + } + } + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Canvas image', function() { + var entity1 = createEntity({ + billboard: { + image: document.createElement('canvas') + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + Icon: { + href: 'http://test.invalid/images/myTexture.jpg' + } + }; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities, { + textureCallback: function(texture) { + if (texture instanceof HTMLCanvasElement) { + return 'http://test.invalid/images/myTexture.jpg'; + } + + fail(); + } + }); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Time-dynamic tracks', function() { + // TODO + }); }); describe('Polylines', function() { From 25c72102d51d00bddde314b140614be2f5a34bd8 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Wed, 5 Jun 2019 17:39:29 -0400 Subject: [PATCH 11/27] Fixed Tracks/Multitracks --- Source/DataSources/KmlExporter.js | 194 ++++++++++++++++++++------- Specs/DataSources/KmlExporterSpec.js | 38 +++--- 2 files changed, 170 insertions(+), 62 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 4c743812f856..0f64c0cb4827 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -9,6 +9,7 @@ define([ './PolylineOutlineMaterialProperty', './SampledPositionProperty', './SampledProperty', + './ScaledPositionProperty', './StripeMaterialProperty', './RectangleGraphics', '../Core/Cartesian2', @@ -23,6 +24,8 @@ define([ '../Core/Math', '../Core/Rectangle', '../Core/Resource', + '../Core/TimeInterval', + '../Core/TimeIntervalCollection', '../Scene/HeightReference', '../Scene/HorizontalOrigin', '../Scene/VerticalOrigin' @@ -37,6 +40,7 @@ define([ PolylineOutlineMaterialProperty, SampledPositionProperty, SampledProperty, + ScaledPositionProperty, StripeMaterialProperty, RectangleGraphics, Cartesian2, @@ -51,6 +55,8 @@ define([ CesiumMath, Rectangle, Resource, + TimeInterval, + TimeIntervalCollection, HeightReference, HorizontalOrigin, VerticalOrigin) { @@ -88,7 +94,7 @@ define([ ValueGetter.prototype.get = function(property, defaultVal) { var value; if (defined(property)) { - value = property.getValue(this._time); + value = defined(property.getValue) ? property.getValue(this._time) : property; } return defaultValue(value, defaultVal); @@ -145,8 +151,10 @@ define([ * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file - * @param {JulianDate} [options.time=JulianDate.fromIso8601(Iso8601.MINIMUM_VALUE)] The time value to use to get properties that are not time varying in KML - * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas + * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas. + * @param {JulianDate} [options.time=JulianDate.fromIso8601(Iso8601.MINIMUM_VALUE)] The time value to use to get properties that are not time varying in KML. + * @param {TimeInterval} [options.defaultAvailability=new TimeInterval()] The interval that will be sampled if an entity doesn't have an availability. + * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML. */ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); @@ -154,6 +162,8 @@ define([ this._valueGetter = new ValueGetter(defined(options.time) ? options.time.toIso8601() : Iso8601.MINIMUM_VALUE); var styleCache = this._styleCache = new StyleCache(); this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); + this._defaultAvailability = new TimeIntervalCollection([defaultValue(options.defaultAvailability, new TimeInterval())]); + this._sampleDuration = defaultValue(options.sampleDuration, 60); var kmlDoc = this._kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml'); var kmlElement = kmlDoc.documentElement; @@ -208,10 +218,16 @@ define([ var availability = entity.availability; if (defined(availability)) { var timeSpan = kmlDoc.createElement('TimeSpan'); - timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin', - JulianDate.toIso8601(availability.start))); - timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end', - JulianDate.toIso8601(availability.stop))); + + if (!JulianDate.equals(availability.start, Iso8601.MINIMUM_VALUE)) { + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin', + JulianDate.toIso8601(availability.start))); + } + + if (!JulianDate.equals(availability.stop, Iso8601.MAXIMUM_VALUE)) { + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end', + JulianDate.toIso8601(availability.stop))); + } placemark.appendChild(timeSpan); } @@ -223,7 +239,7 @@ define([ // TODO: Merge if we went up with multiple of same type // TODO: Multigeometries may need to be split up var style = kmlDoc.createElement('Style'); - for (var styleIndex = 0; styleIndex < geometryCount; ++styleIndex) { + for (var styleIndex = 0; styleIndex < styleCount; ++styleIndex) { style.appendChild(styles[styleIndex]); } @@ -262,6 +278,7 @@ define([ var scratchCartesian3 = new Cartesian3(); var scratchCartographic = new Cartographic(); + var scratchJulianDate = new JulianDate(); function createPoint(that, entity, geometry, geometries, styles) { var kmlDoc = that._kmlDoc; @@ -271,21 +288,111 @@ define([ return; } - // Set coordinates - var coordinates; - var positionProperty = entity.position; - if (positionProperty.isConstant) { - positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); - coordinates = createBasicElementWithText(kmlDoc, 'coordinates', - getCoordinates(scratchCartesian3, ellipsoid)); - } else if (positionProperty instanceof SampledPositionProperty || positionProperty instanceof SampledProperty) { - return createTrack(that, entity, geometries, styles); - } else if (positionProperty instanceof CompositePositionProperty) { - // TODO: Multitrack - } else { - // TODO: Something else time dynamic + // If the point isn't constant then create gx:Track or gx:MultiTrack + var entityPositionProperty = entity.position; + if (!entityPositionProperty.isConstant) { + var intervals; + var useEntityPositionProperty = true; + if (entityPositionProperty instanceof CompositePositionProperty) { + intervals = entityPositionProperty.intervals; + useEntityPositionProperty = false; + } else { + intervals = defaultValue(entity.availability, that._defaultAvailability); + } + + var tracks = []; + for (var i = 0; i < intervals.length; ++i) { + var interval = intervals.get(i); + var positionProperty = useEntityPositionProperty ? entityPositionProperty : interval.data; + + var trackAltitudeMode = kmlDoc.createElement('altitudeMode'); + // This is something that KML importing uses to handle clampToGround, + // so just extract the internal property and set the altitudeMode. + if (positionProperty instanceof ScaledPositionProperty) { + positionProperty = positionProperty._value; + trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.CLAMP_TO_GROUND)); + } else { + trackAltitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); + } + + // We need the raw samples, so just use the internal SampledProperty + if (positionProperty instanceof SampledPositionProperty) { + positionProperty = positionProperty._property; + } + + var positionTimes = []; + var positionValues = []; + + if (positionProperty.isConstant) { + positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(scratchCartesian3, ellipsoid)); + + // This interval is constant so add a track with the same position + positionTimes.push(JulianDate.toIso8601(interval.start)); + positionValues.push(constCoordinates); + positionTimes.push(JulianDate.toIso8601(interval.stop)); + positionValues.push(constCoordinates); + } else if (positionProperty instanceof SampledProperty) { + var times = positionProperty._times; + var values = positionProperty._values; + + for (var j = 0; j < times.length; ++j) { + positionTimes.push(JulianDate.toIso8601(times[j])); + Cartesian3.fromArray(values, j*3, scratchCartesian3); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + } + } else { + var duration = that._sampleDuration; + interval.start.clone(scratchJulianDate); + if (!interval.isStartIncluded) { + JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); + } + + var stopDate = interval.stop; + while (JulianDate.lessThan(scratchJulianDate, stopDate)) { + positionProperty.getValue(scratchJulianDate, scratchCartesian3); + + positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + + JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); + } + + if (interval.isStopIncluded && JulianDate.equals(scratchJulianDate, stopDate)) { + positionProperty.getValue(scratchJulianDate, scratchCartesian3); + + positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + } + } + + // Only create the style with the first track since they are all the same + createTrack(that, entity, positionTimes, positionValues, geometry, tracks, ((i === 0) ? styles : undefined)); + tracks[tracks.length-1].appendChild(trackAltitudeMode); + } + + // If one track, then use it otherwise combine into a multitrack + if (tracks.length === 1) { + geometries.push(tracks[0]); + } else if (tracks.length > 1) { + var multiTrackGeometry = kmlDoc.createElementNS(gxNamespace, 'MultiTrack'); + + var count = tracks.length; + for (var k = 0; k < count; ++k) { + multiTrackGeometry.appendChild(tracks[k]); + } + + geometries.push(multiTrackGeometry); + } + + return; } + entityPositionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(scratchCartesian3, ellipsoid)); + var pointGeometry = kmlDoc.createElement('Point'); // Set altitude mode @@ -302,21 +409,16 @@ define([ styles.push(iconStyle); } - function createTrack(that, entity, geometry, geometries, styles) { + function createTrack(that, entity, positionTimes, positionValues, geometry, geometries, styles) { var kmlDoc = that._kmlDoc; - var ellipsoid = that._ellipsoid; var valueGetter = that._valueGetter; var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track'); - var positions = entity.position; - var positionTimes = positions._times; - var positionValues = positions._values; var count = positionTimes.length; - for (var i = 0; i < count; ++i) { - var when = createBasicElementWithText(kmlDoc, 'when', JulianDate.toIso8601(positionTimes[i])); - var coord = createBasicElementWithText(kmlDoc, 'coord', getCoordinates(positionValues[i], ellipsoid), gxNamespace); + var when = createBasicElementWithText(kmlDoc, 'when', positionTimes[i]); + var coord = createBasicElementWithText(kmlDoc, 'coord', positionValues[i], gxNamespace); trackGeometry.appendChild(when); trackGeometry.appendChild(coord); @@ -324,24 +426,26 @@ define([ geometries.push(trackGeometry); - // Create style - var iconStyle = (geometry instanceof BillboardGraphics) ? - createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); - styles.push(iconStyle); + if (defined(styles)) { + // Create style + var iconStyle = (geometry instanceof BillboardGraphics) ? + createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); + styles.push(iconStyle); + + // See if we have a line that needs to be drawn + var path = entity.path; + if (defined(path)) { + var width = valueGetter.get(path.width); + var material = path.material; + if (defined(material) || defined(width)) { + var lineStyle = kmlDoc.createElement('LineStyle'); + if (defined(width)) { + lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); + } - // See if we have a line that needs to be drawn - var path = entity.path; - if (defined(path)) { - var width = valueGetter.get(path.width); - var material = path.material; - if (defined(material) || defined(width)) { - var lineStyle = kmlDoc.createElement('LineStyle'); - if (defined(width)) { - lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); + processMaterial(that, material, lineStyle); + styles.push(lineStyle); } - - processMaterial(that, material, lineStyle); - styles.push(lineStyle); } } } diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 8bdd7ebd51e0..21d19a0cc4a0 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -69,8 +69,8 @@ defineSuite([ } }; - xit('test', function() { - return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) + it('test', function() { + return KmlDataSource.load('../Apps/SampleData/kml/bikeRide.kml', options) .then(function(datasource) { var exporter = new KmlExporter(datasource.entities); @@ -259,8 +259,8 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, hierarchy); }); - describe('Points', function() { - it('Constant Postion', function() { + describe('Point Geometry', function() { + it('Point with constant position', function() { var entity1 = createEntity({ point: { color: Color.LINEN, @@ -287,13 +287,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('Time-dynamic tracks', function() { - // TODO - }); - }); - - describe('Billboards', function() { - it('Constant Postion', function() { + it('Billboard with constant position', function() { var entity1 = createEntity({ billboard: { image: 'http://test.invalid/image.jpg', @@ -345,7 +339,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('Aligned Axis not Z', function() { + it('Billboard with AlignedAxis not Z', function() { var entity1 = createEntity({ billboard: { rotation: CesiumMath.toRadians(10), @@ -367,7 +361,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('0 degree heading should be 360', function() { + it('Billboard with 0 degree heading should be 360', function() { var entity1 = createEntity({ billboard: { rotation: CesiumMath.toRadians(0), @@ -391,7 +385,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('HotSpot Center', function() { + it('Billboard with HotSpot at the center', function() { var entity1 = createEntity({ billboard: { pixelOffset: new Cartesian2(2, 3), @@ -425,7 +419,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('HotSpot TopRight', function() { + it('Billboard with HotSpot at the TopRight', function() { var entity1 = createEntity({ billboard: { pixelOffset: new Cartesian2(2, 3), @@ -459,7 +453,7 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); - it('Canvas image', function() { + it('Billboard with a Canvas image', function() { var entity1 = createEntity({ billboard: { image: document.createElement('canvas') @@ -491,8 +485,14 @@ defineSuite([ }); checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); + }); - it('Time-dynamic tracks', function() { + describe('Tracks', function() { + it('Point', function() { + // TODO + }); + + it('Billboard', function() { // TODO }); }); @@ -508,4 +508,8 @@ defineSuite([ describe('Polygons', function() { }); + + describe('Models', function() { + + }); }); From 3f8a29a9d592c8d64a2630b1d23282d09a8b670d Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Thu, 6 Jun 2019 17:07:28 -0400 Subject: [PATCH 12/27] Added tests for dynamic points. Added code for path graphics and labels. Fixed polygons. Fixed handling of default intervals --- Source/DataSources/KmlExporter.js | 388 ++++++++++++++++----------- Specs/DataSources/KmlExporterSpec.js | 119 +++++++- 2 files changed, 346 insertions(+), 161 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 0f64c0cb4827..36735497c5c4 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -19,6 +19,7 @@ define([ '../Core/defaultValue', '../Core/defined', '../Core/Ellipsoid', + '../Core/isArray', '../Core/Iso8601', '../Core/JulianDate', '../Core/Math', @@ -50,6 +51,7 @@ define([ defaultValue, defined, Ellipsoid, + isArray, Iso8601, JulianDate, CesiumMath, @@ -152,18 +154,42 @@ define([ * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas. - * @param {JulianDate} [options.time=JulianDate.fromIso8601(Iso8601.MINIMUM_VALUE)] The time value to use to get properties that are not time varying in KML. - * @param {TimeInterval} [options.defaultAvailability=new TimeInterval()] The interval that will be sampled if an entity doesn't have an availability. + * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML. + * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability. * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML. */ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); - this._valueGetter = new ValueGetter(defined(options.time) ? options.time.toIso8601() : Iso8601.MINIMUM_VALUE); + var styleCache = this._styleCache = new StyleCache(); this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); - this._defaultAvailability = new TimeIntervalCollection([defaultValue(options.defaultAvailability, new TimeInterval())]); - this._sampleDuration = defaultValue(options.sampleDuration, 60); + + // Use the start time as the default because just in case they define + // properties with an interval even if they don't change. + var entityAvailability = entities.computeAvailability(); + this._valueGetter = new ValueGetter(defined(options.time) ? options.time : entityAvailability.start); + + // Figure out how we will sample dynamic position properties + var defaultAvailability = defaultValue(options.defaultAvailability, entityAvailability); + var sampleDuration = this._sampleDuration = defaultValue(options.sampleDuration, 60); + + // Make sure we don't have infinite availability if we need to sample + if (defaultAvailability.start === Iso8601.MINIMUM_VALUE) { + if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) { + // Infinite, so just use the default + defaultAvailability = new TimeInterval(); + } else { + // No start time, so just sample 10 times before the stop + JulianDate.addSeconds(defaultAvailability.stop, -10*sampleDuration, defaultAvailability.start); + } + } else if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) { + // No stop time, so just sample 10 times after the start + JulianDate.addSeconds(defaultAvailability.start, 10*sampleDuration, defaultAvailability.stop); + } + + // Wrap it in a TimeIntervalCollection because that is what entity.availability is + this._defaultAvailability = new TimeIntervalCollection([defaultAvailability]); var kmlDoc = this._kmlDoc = document.implementation.createDocument(kmlNamespace, 'kml'); var kmlElement = kmlDoc.documentElement; @@ -188,6 +214,7 @@ define([ function recurseEntities(that, parentNode, entities) { var kmlDoc = that._kmlDoc; var styleCache = that._styleCache; + var valueGetter = that._valueGetter; var count = entities.length; var geometries; @@ -197,20 +224,43 @@ define([ geometries = []; styles = []; - createPoint(that, entity, entity.point, geometries, styles); - createPoint(that, entity, entity.billboard, geometries, styles); + createPoint(that, entity, geometries, styles); createLineString(that, entity.polyline, geometries, styles); createPolygon(that, entity.rectangle, geometries, styles); createPolygon(that, entity.polygon, geometries, styles); + createModel(that, entity.model, geometries, styles); - // TODO: Handle the rest of the geometries + // TODO: Labels var geometryCount = geometries.length; if (geometryCount > 0) { var placemark = kmlDoc.createElement('Placemark'); placemark.setAttribute('id', entity.id); - placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + var name = entity.name; + var labelGraphics = entity.label; + if (defined(labelGraphics)) { + var labelStyle = kmlDoc.createElement('LabelStyle'); + + // KML only shows the name as a label, so just change the name if we need to show a label + var text = valueGetter.get(labelGraphics.text); + name = (defined(text) && text.length > 0) ? text : name; + + var color = valueGetter.getColor(labelGraphics.fillColor); + if (defined(color)) { + labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); + labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); + } + + var scale = valueGetter.get(labelGraphics.scale); + if (defined(scale)) { + labelStyle.appendChild(createBasicElementWithText(kmlDoc, 'scale', scale)); + } + + styles.push(labelStyle); + } + + placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', name)); // TODO: Graphics show property placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); @@ -237,7 +287,6 @@ define([ var styleCount = styles.length; if (styleCount > 0) { // TODO: Merge if we went up with multiple of same type - // TODO: Multigeometries may need to be split up var style = kmlDoc.createElement('Style'); for (var styleIndex = 0; styleIndex < styleCount; ++styleIndex) { style.appendChild(styles[styleIndex]); @@ -280,112 +329,19 @@ define([ var scratchCartographic = new Cartographic(); var scratchJulianDate = new JulianDate(); - function createPoint(that, entity, geometry, geometries, styles) { + function createPoint(that, entity, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; - if (!defined(geometry)) { + var pointGraphics = defaultValue(entity.billboard, entity.point); + if (!defined(pointGraphics) && !defined(entity.path)) { return; } // If the point isn't constant then create gx:Track or gx:MultiTrack var entityPositionProperty = entity.position; if (!entityPositionProperty.isConstant) { - var intervals; - var useEntityPositionProperty = true; - if (entityPositionProperty instanceof CompositePositionProperty) { - intervals = entityPositionProperty.intervals; - useEntityPositionProperty = false; - } else { - intervals = defaultValue(entity.availability, that._defaultAvailability); - } - - var tracks = []; - for (var i = 0; i < intervals.length; ++i) { - var interval = intervals.get(i); - var positionProperty = useEntityPositionProperty ? entityPositionProperty : interval.data; - - var trackAltitudeMode = kmlDoc.createElement('altitudeMode'); - // This is something that KML importing uses to handle clampToGround, - // so just extract the internal property and set the altitudeMode. - if (positionProperty instanceof ScaledPositionProperty) { - positionProperty = positionProperty._value; - trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.CLAMP_TO_GROUND)); - } else { - trackAltitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); - } - - // We need the raw samples, so just use the internal SampledProperty - if (positionProperty instanceof SampledPositionProperty) { - positionProperty = positionProperty._property; - } - - var positionTimes = []; - var positionValues = []; - - if (positionProperty.isConstant) { - positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); - var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates', - getCoordinates(scratchCartesian3, ellipsoid)); - - // This interval is constant so add a track with the same position - positionTimes.push(JulianDate.toIso8601(interval.start)); - positionValues.push(constCoordinates); - positionTimes.push(JulianDate.toIso8601(interval.stop)); - positionValues.push(constCoordinates); - } else if (positionProperty instanceof SampledProperty) { - var times = positionProperty._times; - var values = positionProperty._values; - - for (var j = 0; j < times.length; ++j) { - positionTimes.push(JulianDate.toIso8601(times[j])); - Cartesian3.fromArray(values, j*3, scratchCartesian3); - positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); - } - } else { - var duration = that._sampleDuration; - interval.start.clone(scratchJulianDate); - if (!interval.isStartIncluded) { - JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); - } - - var stopDate = interval.stop; - while (JulianDate.lessThan(scratchJulianDate, stopDate)) { - positionProperty.getValue(scratchJulianDate, scratchCartesian3); - - positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); - positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); - - JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); - } - - if (interval.isStopIncluded && JulianDate.equals(scratchJulianDate, stopDate)) { - positionProperty.getValue(scratchJulianDate, scratchCartesian3); - - positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); - positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); - } - } - - // Only create the style with the first track since they are all the same - createTrack(that, entity, positionTimes, positionValues, geometry, tracks, ((i === 0) ? styles : undefined)); - tracks[tracks.length-1].appendChild(trackAltitudeMode); - } - - // If one track, then use it otherwise combine into a multitrack - if (tracks.length === 1) { - geometries.push(tracks[0]); - } else if (tracks.length > 1) { - var multiTrackGeometry = kmlDoc.createElementNS(gxNamespace, 'MultiTrack'); - - var count = tracks.length; - for (var k = 0; k < count; ++k) { - multiTrackGeometry.appendChild(tracks[k]); - } - - geometries.push(multiTrackGeometry); - } - + createTracks(that, entity, pointGraphics, geometries, styles); return; } @@ -397,55 +353,151 @@ define([ // Set altitude mode var altitudeMode = kmlDoc.createElement('altitudeMode'); - altitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); + altitudeMode.appendChild(getAltitudeMode(that, pointGraphics.heightReference)); pointGeometry.appendChild(altitudeMode); pointGeometry.appendChild(coordinates); geometries.push(pointGeometry); // Create style - var iconStyle = (geometry instanceof BillboardGraphics) ? - createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); + var iconStyle = (pointGraphics instanceof BillboardGraphics) ? + createIconStyleFromBillboard(that, pointGraphics) : createIconStyleFromPoint(that, pointGraphics); styles.push(iconStyle); } - function createTrack(that, entity, positionTimes, positionValues, geometry, geometries, styles) { + function createTracks(that, entity, pointGraphics, geometries, styles) { var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; var valueGetter = that._valueGetter; - var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track'); + var intervals; + var entityPositionProperty = entity.position; + var useEntityPositionProperty = true; + if (entityPositionProperty instanceof CompositePositionProperty) { + intervals = entityPositionProperty.intervals; + useEntityPositionProperty = false; + } else { + intervals = defaultValue(entity.availability, that._defaultAvailability); + } + + var i; + var tracks = []; + for (i = 0; i < intervals.length; ++i) { + var interval = intervals.get(i); + var positionProperty = useEntityPositionProperty ? entityPositionProperty : interval.data; + + var trackAltitudeMode = kmlDoc.createElement('altitudeMode'); + // This is something that KML importing uses to handle clampToGround, + // so just extract the internal property and set the altitudeMode. + if (positionProperty instanceof ScaledPositionProperty) { + positionProperty = positionProperty._value; + trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.CLAMP_TO_GROUND)); + } else if (defined(pointGraphics)){ + trackAltitudeMode.appendChild(getAltitudeMode(that, pointGraphics.heightReference)); + } else { + // Path graphics only, which has no height reference + trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.NONE)); + } - var count = positionTimes.length; - for (var i = 0; i < count; ++i) { - var when = createBasicElementWithText(kmlDoc, 'when', positionTimes[i]); - var coord = createBasicElementWithText(kmlDoc, 'coord', positionValues[i], gxNamespace); + // We need the raw samples, so just use the internal SampledProperty + if (positionProperty instanceof SampledPositionProperty) { + positionProperty = positionProperty._property; + } + + var positionTimes = []; + var positionValues = []; + + if (positionProperty.isConstant) { + positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(scratchCartesian3, ellipsoid)); + + // This interval is constant so add a track with the same position + positionTimes.push(JulianDate.toIso8601(interval.start)); + positionValues.push(constCoordinates); + positionTimes.push(JulianDate.toIso8601(interval.stop)); + positionValues.push(constCoordinates); + } else if (positionProperty instanceof SampledProperty) { + var times = positionProperty._times; + var values = positionProperty._values; + + for (var j = 0; j < times.length; ++j) { + positionTimes.push(JulianDate.toIso8601(times[j])); + Cartesian3.fromArray(values, j*3, scratchCartesian3); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + } + } else { + var duration = that._sampleDuration; + interval.start.clone(scratchJulianDate); + if (!interval.isStartIncluded) { + JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); + } + + var stopDate = interval.stop; + while (JulianDate.lessThan(scratchJulianDate, stopDate)) { + positionProperty.getValue(scratchJulianDate, scratchCartesian3); + + positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + + JulianDate.addSeconds(scratchJulianDate, duration, scratchJulianDate); + } + + if (interval.isStopIncluded && JulianDate.equals(scratchJulianDate, stopDate)) { + positionProperty.getValue(scratchJulianDate, scratchCartesian3); - trackGeometry.appendChild(when); - trackGeometry.appendChild(coord); + positionTimes.push(JulianDate.toIso8601(scratchJulianDate)); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + } + } + + var trackGeometry = kmlDoc.createElementNS(gxNamespace, 'Track'); + trackGeometry.appendChild(trackAltitudeMode); + + for (var k = 0; k < positionTimes.length; ++k) { + var when = createBasicElementWithText(kmlDoc, 'when', positionTimes[k]); + var coord = createBasicElementWithText(kmlDoc, 'coord', positionValues[k], gxNamespace); + + trackGeometry.appendChild(when); + trackGeometry.appendChild(coord); + } + + tracks.push(trackGeometry); } - geometries.push(trackGeometry); + // If one track, then use it otherwise combine into a multitrack + if (tracks.length === 1) { + geometries.push(tracks[0]); + } else if (tracks.length > 1) { + var multiTrackGeometry = kmlDoc.createElementNS(gxNamespace, 'MultiTrack'); - if (defined(styles)) { - // Create style - var iconStyle = (geometry instanceof BillboardGraphics) ? - createIconStyleFromBillboard(that, geometry) : createIconStyleFromPoint(that, geometry); - styles.push(iconStyle); + for (i = 0; i < tracks.length; ++i) { + multiTrackGeometry.appendChild(tracks[i]); + } - // See if we have a line that needs to be drawn - var path = entity.path; - if (defined(path)) { - var width = valueGetter.get(path.width); - var material = path.material; - if (defined(material) || defined(width)) { - var lineStyle = kmlDoc.createElement('LineStyle'); - if (defined(width)) { - lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); - } + geometries.push(multiTrackGeometry); + } - processMaterial(that, material, lineStyle); - styles.push(lineStyle); + // Create style + if (defined(pointGraphics)) { + var iconStyle = (pointGraphics instanceof BillboardGraphics) ? + createIconStyleFromBillboard(that, pointGraphics) : createIconStyleFromPoint(that, pointGraphics); + styles.push(iconStyle); + } + + // See if we have a line that needs to be drawn + var path = entity.path; + if (defined(path)) { + var width = valueGetter.get(path.width); + var material = path.material; + if (defined(material) || defined(width)) { + var lineStyle = kmlDoc.createElement('LineStyle'); + if (defined(width)) { + lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', width)); } + + processMaterial(that, material, lineStyle); + styles.push(lineStyle); } } } @@ -456,10 +508,8 @@ define([ var iconStyle = kmlDoc.createElement('IconStyle'); - var color = valueGetter.get(pointGraphics.color); + var color = valueGetter.getColor(pointGraphics.color); if (defined(color)) { - color = colorToString(color); - iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', color)); iconStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); } @@ -618,13 +668,19 @@ define([ styles.push(lineStyle); } - function getRectangleBoundaries(that, rectangleGraphics) { + function getRectangleBoundaries(that, rectangleGraphics, extrudedHeight) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; var coordinates; var height = valueGetter.get(rectangleGraphics.height, 0.0); + if (extrudedHeight > 0) { + // We extrude up and KML extrudes down, so if we extrude, set the polygon height to + // the extruded height so KML will look similar to Cesium + height = extrudedHeight; + } + var coordinatesProperty = rectangleGraphics.coordinates; if (coordinatesProperty.isConstant) { var rectangle = coordinatesProperty.getValue(Iso8601.MINIMUM_VALUE); @@ -671,30 +727,41 @@ define([ return linearRing; } - function getPolygonBoundaries(that, polygonGraphics) { + function getPolygonBoundaries(that, polygonGraphics, extrudedHeight) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; var height = valueGetter.get(polygonGraphics.height, 0.0); var perPositionHeight = valueGetter.get(polygonGraphics.perPositionHeight, false); + if (!perPositionHeight && (extrudedHeight > 0)) { + // We extrude up and KML extrudes down, so if we extrude, set the polygon height to + // the extruded height so KML will look similar to Cesium + height = extrudedHeight; + } + var boundaries = []; var hierarchyProperty = polygonGraphics.hierarchy; if (hierarchyProperty.isConstant) { - var hierarchy = hierarchyProperty.getValue(Iso8601.MINIMUM_VALUE); + var hierarchy = valueGetter.get(hierarchyProperty); + + // Polygon hierarchy can sometimes just be an array of positions + var positions = isArray(hierarchy) ? hierarchy : hierarchy.positions; // Polygon boundaries var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs'); - outerBoundaryIs.appendChild(getLinearRing(that, hierarchy.positions, height, perPositionHeight)); + outerBoundaryIs.appendChild(getLinearRing(that, positions, height, perPositionHeight)); boundaries.push(outerBoundaryIs); // Hole boundaries var holes = hierarchy.holes; - var holeCount = holes.length; - for (var i = 0; i < holeCount; ++i) { - var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs'); - innerBoundaryIs.appendChild(getLinearRing(that, holes[i].positions, height, perPositionHeight)); - boundaries.push(innerBoundaryIs); + if (defined(holes)) { + var holeCount = holes.length; + for (var i = 0; i < holeCount; ++i) { + var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs'); + innerBoundaryIs.appendChild(getLinearRing(that, holes[i].positions, height, perPositionHeight)); + boundaries.push(innerBoundaryIs); + } } } else { // TODO: Time dynamic @@ -715,9 +782,14 @@ define([ var polygonGeometry = kmlDoc.createElement('Polygon'); + var extrudedHeight = valueGetter.get(geometry.extrudedHeight, 0.0); + if (extrudedHeight > 0) { + polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'extrude', true)); + } + // Set boundaries var boundaries = (geometry instanceof RectangleGraphics) ? - getRectangleBoundaries(that, geometry) : getPolygonBoundaries(kmlDoc, geometry); + getRectangleBoundaries(that, geometry, extrudedHeight) : getPolygonBoundaries(that, geometry, extrudedHeight); var boundaryCount = boundaries.length; for (var i = 0; i < boundaryCount; ++i) { @@ -735,7 +807,7 @@ define([ polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); } - // TODO: extrudedHeight, rotation, stRotation, closeTop, closeBottom + // TODO: extrudedHeight, stRotation geometries.push(polygonGeometry); @@ -769,6 +841,20 @@ define([ styles.push(polyStyle); } + function createModel(that, geometry, geometries, styles) { + var kmlDoc = that._kmlDoc; + + if (!defined(geometry)) { + return; + } + + var modelGeometry = kmlDoc.createElement('Model'); + + // TODO: Populate info - fire callback + + geometries.push(modelGeometry); + } + function processMaterial(that, materialProperty, style) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; @@ -836,7 +922,7 @@ define([ } function getCoordinates(coordinates, ellipsoid) { - if (!Array.isArray(coordinates)) { + if (!isArray(coordinates)) { coordinates = [coordinates]; } diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 21d19a0cc4a0..ff30a4724cc4 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -3,15 +3,20 @@ defineSuite([ 'Core/BoundingRectangle', 'Core/Cartesian2', 'Core/Cartesian3', + 'Core/Cartographic', 'Core/Color', 'Core/defined', 'Core/Iso8601', + 'Core/JulianDate', 'Core/Math', 'Core/PerspectiveFrustum', 'Core/Rectangle', + 'Core/TimeInterval', + 'DataSources/CallbackProperty', 'DataSources/Entity', 'DataSources/EntityCollection', 'DataSources/KmlDataSource', + 'DataSources/SampledPositionProperty', 'Scene/HeightReference', 'Scene/HorizontalOrigin', 'Scene/VerticalOrigin' @@ -20,15 +25,20 @@ defineSuite([ BoundingRectangle, Cartesian2, Cartesian3, + Cartographic, Color, defined, Iso8601, + JulianDate, CesiumMath, PerspectiveFrustum, Rectangle, + TimeInterval, + CallbackProperty, Entity, EntityCollection, KmlDataSource, + SampledPositionProperty, HeightReference, HorizontalOrigin, VerticalOrigin) { @@ -69,7 +79,7 @@ defineSuite([ } }; - it('test', function() { + xit('test', function() { return KmlDataSource.load('../Apps/SampleData/kml/bikeRide.kml', options) .then(function(datasource) { var exporter = new KmlExporter(datasource.entities); @@ -89,11 +99,8 @@ defineSuite([ } function checkTagWithProperties(element, properties) { - var numberOfProperties = Object.keys(properties).length; var attributes = properties._; if (defined(attributes)) { - --numberOfProperties; // Attributes - var elementAttributes = element.attributes; expect(elementAttributes.length).toBe(Object.keys(attributes).length); for (var j = 0; j < elementAttributes.length; ++j) { @@ -111,7 +118,6 @@ defineSuite([ } var childNodes = element.childNodes; - expect(childNodes.length).toBe(numberOfProperties); for (var i = 0; i < childNodes.length; ++i) { var node = childNodes[i]; var property = properties[node.tagName]; @@ -121,7 +127,9 @@ defineSuite([ property = property.getValue(Iso8601.MINIMUM_VALUE); } - if (Array.isArray(property)) { + if (typeof property === 'function') { + expect(property(node.textContent)).toBe(true); + } else if (Array.isArray(property)) { var values = node.textContent.split(/\s*,\s*/); expect(values.length).toBe(property.length); for (var k = 0; k < property.length; ++k) { @@ -488,12 +496,103 @@ defineSuite([ }); describe('Tracks', function() { - it('Point', function() { - // TODO + var times = [ + JulianDate.fromIso8601('2019-06-17'), + JulianDate.fromIso8601('2019-06-18'), + JulianDate.fromIso8601('2019-06-19') + ]; + var positions = [ + Cartesian3.fromDegrees(-75.59777, 40.03883, 12), + Cartesian3.fromDegrees(-76.59777, 39.03883, 12), + Cartesian3.fromDegrees(-77.59777, 38.03883, 12) + ]; + + function checkWhen(textContent) { + var count = times.length; + for (var i=0;i Date: Thu, 6 Jun 2019 18:18:42 -0400 Subject: [PATCH 13/27] Added a bunch of tests --- Source/DataSources/KmlExporter.js | 4 +- Specs/DataSources/KmlExporterSpec.js | 231 ++++++++++++++++++++++++++- 2 files changed, 230 insertions(+), 5 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 36735497c5c4..62ff2da5945d 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -807,7 +807,7 @@ define([ polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); } - // TODO: extrudedHeight, stRotation + // TODO: stRotation geometries.push(polygonGeometry); @@ -904,7 +904,7 @@ define([ var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; - var heightReference = valueGetter.get(heightReferenceProperty, HeightReference.CLAMP_TO_GROUND); + var heightReference = valueGetter.get(heightReferenceProperty, HeightReference.NONE); var altitudeModeText; switch (heightReference) { case HeightReference.NONE: diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index ff30a4724cc4..b2922caaeaf3 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -13,9 +13,11 @@ defineSuite([ 'Core/Rectangle', 'Core/TimeInterval', 'DataSources/CallbackProperty', + 'DataSources/ColorMaterialProperty', 'DataSources/Entity', 'DataSources/EntityCollection', 'DataSources/KmlDataSource', + 'DataSources/PolylineOutlineMaterialProperty', 'DataSources/SampledPositionProperty', 'Scene/HeightReference', 'Scene/HorizontalOrigin', @@ -35,9 +37,11 @@ defineSuite([ Rectangle, TimeInterval, CallbackProperty, + ColorMaterialProperty, Entity, EntityCollection, KmlDataSource, + PolylineOutlineMaterialProperty, SampledPositionProperty, HeightReference, HorizontalOrigin, @@ -80,7 +84,7 @@ defineSuite([ }; xit('test', function() { - return KmlDataSource.load('../Apps/SampleData/kml/bikeRide.kml', options) + return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) .then(function(datasource) { var exporter = new KmlExporter(datasource.entities); @@ -597,15 +601,236 @@ defineSuite([ }); describe('Polylines', function() { + var positions = [ + Cartesian3.fromDegrees(-1, -1, 12), + Cartesian3.fromDegrees(1, -1, 12), + Cartesian3.fromDegrees(1, 1, 12), + Cartesian3.fromDegrees(-1, 1, 12) + ]; - }); + it('Clamped to ground', function() { + var entity1 = createEntity({ + polyline: { + positions: positions, + clampToGround: true, + material: new ColorMaterialProperty(Color.GREEN), + width: 5, + zIndex: 2 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polyline = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Not clamped to ground', function() { + var entity1 = createEntity({ + polyline: { + positions: positions, + clampToGround: false, + material: new ColorMaterialProperty(Color.GREEN), + width: 5, + zIndex: 2 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polyline = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; - describe('Rectangles', function() { + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('With outline', function() { + var entity1 = createEntity({ + polyline: { + positions: positions, + clampToGround: false, + material: new PolylineOutlineMaterialProperty({ + color: Color.GREEN, + outlineColor: Color.BLUE, + outlineWidth: 2 + }), + width: 5, + zIndex: 2 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polyline = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); }); describe('Polygons', function() { + var positions = [ + Cartesian3.fromDegrees(-1, -1, 12), + Cartesian3.fromDegrees(1, -1, 12), + Cartesian3.fromDegrees(1, 1, 12), + Cartesian3.fromDegrees(-1, 1, 12) + ]; + + it('Polygon with outline', function(){ + var entity1 = createEntity({ + polygon: { + hierarchy: positions, + height: 10, + perPositionHeight: false, + heightReference: HeightReference.CLAMP_TO_GROUND, + extrudedHeight: 0, + fill: true, + material: new ColorMaterialProperty(Color.GREEN), + outline: true, + outlineWidth: 5, + outlineColor: Color.BLUE, + zIndex: 2 + } + }); + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.PolyStyle = { + + }; + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polygon = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Polygon with extrusion', function(){ + var entity1 = createEntity({ + polygon: { + hierarchy: positions, + height: 10, + perPositionHeight: false, + extrudedHeight: 20 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.PolyStyle = { + + }; + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polygon = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Polygon with extrusion and perPositionHeights', function(){ + var entity1 = createEntity({ + polygon: { + hierarchy: positions, + height: 10, + perPositionHeight: true, + extrudedHeight: 20 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.PolyStyle = { + + }; + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polygon = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('Rectangle', function(){ + var entity1 = createEntity({ + rectangle: { + hierarchy: Rectangle.fromDegrees(-1, -1, 1, 1), + height: 10, + perPositionHeight: false, + heightReference: HeightReference.CLAMP_TO_GROUND, + extrudedHeight: 0, + fill: true, + material: new ColorMaterialProperty(Color.GREEN), + outline: true, + outlineWidth: 5, + outlineColor: Color.BLUE, + zIndex: 2 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.PolyStyle = { + + }; + expectedResult.Document.Style.LineStyle = { + + }; + expectedResult.Document.Placemark.Polygon = { + altitudeMode: 'clampToGround', + coordinates: expectedPointPosition + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); }); describe('Models', function() { From 11bd9ad577cec98841f1d71386a2062ac1bc06cc Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Fri, 7 Jun 2019 11:40:54 -0400 Subject: [PATCH 14/27] Polygon and Polyline tests work. --- Source/DataSources/KmlExporter.js | 84 ++++----- Specs/DataSources/KmlExporterSpec.js | 270 ++++++++++++++++++++------- 2 files changed, 245 insertions(+), 109 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 62ff2da5945d..c5f36178d809 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -109,6 +109,14 @@ define([ } }; + ValueGetter.prototype.getMaterialType = function(property) { + if (!defined(property)) { + return; + } + + return property.getValue(this._time); + }; + function StyleCache() { this._ids = {}; this._styles = {}; @@ -116,10 +124,8 @@ define([ } StyleCache.prototype.get = function(element) { - // TODO: Recursively sort for better caching - var ids = this._ids; - var key = element.innerHTML; // TODO: Maybe use hash + var key = element.innerHTML; if (defined(ids[key])) { return ids[key]; } @@ -138,10 +144,10 @@ define([ StyleCache.prototype.save = function(parentElement) { var styles = this._styles; - // TODO: Put in beginning + var firstElement = parentElement.childNodes[0]; for (var key in styles) { if (styles.hasOwnProperty(key)) { - parentElement.appendChild(styles[key]); + parentElement.insertBefore(styles[key], firstElement); } } }; @@ -230,8 +236,6 @@ define([ createPolygon(that, entity.polygon, geometries, styles); createModel(that, entity.model, geometries, styles); - // TODO: Labels - var geometryCount = geometries.length; if (geometryCount > 0) { var placemark = kmlDoc.createElement('Placemark'); @@ -261,7 +265,6 @@ define([ } placemark.appendChild(createBasicElementWithText(kmlDoc, 'name', name)); - // TODO: Graphics show property placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); @@ -663,7 +666,7 @@ define([ processMaterial(that, polylineGraphics.material, lineStyle); - // TODO: and gx:labelVisibility> + // TODO: ? styles.push(lineStyle); } @@ -778,7 +781,13 @@ define([ return; } - // TODO: Detect textured quads and use ground overlays instead + // Detect textured quads and use ground overlays instead + var isRectangle = (geometry instanceof RectangleGraphics); + if (isRectangle && valueGetter.getMaterialType(geometry.material) === 'Image') { + // TODO + // TODO: stRotation + return; + } var polygonGeometry = kmlDoc.createElement('Polygon'); @@ -788,8 +797,8 @@ define([ } // Set boundaries - var boundaries = (geometry instanceof RectangleGraphics) ? - getRectangleBoundaries(that, geometry, extrudedHeight) : getPolygonBoundaries(that, geometry, extrudedHeight); + var boundaries = isRectangle ? getRectangleBoundaries(that, geometry, extrudedHeight) : + getPolygonBoundaries(that, geometry, extrudedHeight); var boundaryCount = boundaries.length; for (var i = 0; i < boundaryCount; ++i) { @@ -801,14 +810,6 @@ define([ altitudeMode.appendChild(getAltitudeMode(that, geometry.heightReference)); polygonGeometry.appendChild(altitudeMode); - // Set draw order - var zIndex = valueGetter.get(geometry.zIndex); - if (defined(zIndex)) { - polygonGeometry.appendChild(createBasicElementWithText(kmlDoc, 'drawOrder', zIndex, gxNamespace)); - } - - // TODO: stRotation - geometries.push(polygonGeometry); // Create style @@ -816,7 +817,7 @@ define([ var fill = valueGetter.get(geometry.fill, false); if (fill) { - polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', fill)); + polyStyle.appendChild(createBasicElementWithText(kmlDoc, 'fill', fill)); } processMaterial(that, geometry.material, polyStyle); @@ -831,7 +832,7 @@ define([ var outlineWidth = valueGetter.get(geometry.outlineWidth, 1.0); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'width', outlineWidth)); - var outlineColor = valueGetter.get(geometry.outlineColor, Color.BLACK); + var outlineColor = valueGetter.getColor(geometry.outlineColor, Color.BLACK); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'color', outlineColor)); lineStyle.appendChild(createBasicElementWithText(kmlDoc, 'colorMode', 'normal')); @@ -869,29 +870,24 @@ define([ } var color; - if (materialProperty instanceof ColorMaterialProperty) { - color = colorToString(material.color); - } else if (materialProperty instanceof CompositeMaterialProperty) { - // TODO - } else if (materialProperty instanceof GridMaterialProperty) { - color = colorToString(material.color); + var type = valueGetter.getMaterialType(materialProperty); + switch(type) { + case 'Color': + case 'Grid': + case 'PolylineGlow': + color = colorToString(material.color); + break; + case 'PolylineOutline': + color = colorToString(material.color); - // TODO - } else if (materialProperty instanceof ImageMaterialProperty) { - // TODO: We'll need to use GroundOverlays for this - } else if (materialProperty instanceof PolylineGlowMaterialProperty) { - // TODO - } else if (materialProperty instanceof PolylineOutlineMaterialProperty) { - color = colorToString(material.color); - - var outlineColor = colorToString(material.outlineColor); - var outlineWidth = material.outlineWidth; - style.appendChild(createBasicElementWithText(kmlDoc, 'outerColor', outlineColor, gxNamespace)); - style.appendChild(createBasicElementWithText(kmlDoc, 'outerWidth', outlineWidth, gxNamespace)); - } else if (materialProperty instanceof StripeMaterialProperty) { - // TODO - } else { - // TODO: Unknown Material + var outlineColor = colorToString(material.outlineColor); + var outlineWidth = material.outlineWidth; + style.appendChild(createBasicElementWithText(kmlDoc, 'outerColor', outlineColor, gxNamespace)); + style.appendChild(createBasicElementWithText(kmlDoc, 'outerWidth', outlineWidth, gxNamespace)); + break; + case 'Stripe': + color = colorToString(material.oddColor); + break; } if (defined(color)) { diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index b2922caaeaf3..fd7fb42eef05 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -10,6 +10,7 @@ defineSuite([ 'Core/JulianDate', 'Core/Math', 'Core/PerspectiveFrustum', + 'Core/PolygonHierarchy', 'Core/Rectangle', 'Core/TimeInterval', 'DataSources/CallbackProperty', @@ -34,6 +35,7 @@ defineSuite([ JulianDate, CesiumMath, PerspectiveFrustum, + PolygonHierarchy, Rectangle, TimeInterval, CallbackProperty, @@ -133,24 +135,12 @@ defineSuite([ if (typeof property === 'function') { expect(property(node.textContent)).toBe(true); - } else if (Array.isArray(property)) { - var values = node.textContent.split(/\s*,\s*/); - expect(values.length).toBe(property.length); - for (var k = 0; k < property.length; ++k) { - var p = property[k]; - var v = values[k]; - if (typeof p === 'string') { - expect(v).toEqual(p); - } else if (typeof p === 'number') { - expect(Number(v)).toEqualEpsilon(p, CesiumMath.EPSILON7); - } else { - fail(); - } - } } else if (typeof property === 'string') { expect(node.textContent).toEqual(property); } else if (typeof property === 'number') { expect(Number(node.textContent)).toEqualEpsilon(property, CesiumMath.EPSILON7); + } else if (typeof property === 'boolean') { + expect(Number(node.textContent)).toBe(property ? 1 : 0); } else { checkTagWithProperties(node, property); } @@ -158,8 +148,16 @@ defineSuite([ } var counter = 0; - var expectedPointPosition = [-75.59777, 40.03883, 12]; - var pointPosition = Cartesian3.fromDegrees(expectedPointPosition[0], expectedPointPosition[1], expectedPointPosition[2]); + var pointPosition = Cartesian3.fromDegrees(-75.59777, 40.03883, 12); + function checkPointCoord(textContent) { + var values = textContent.split(/\s*,\s*/); + expect(values.length).toBe(3); + + var cartographic1 = Cartographic.fromCartesian(pointPosition); + var cartographic2 = Cartographic.fromDegrees(Number(values[0]), Number(values[1]), Number(values[2])); + return Cartographic.equalsEpsilon(cartographic1, cartographic2, CesiumMath.EPSILON7); + } + function createEntity(properties) { ++counter; var options = { @@ -247,18 +245,18 @@ defineSuite([ id: entity2.id }, name: entity2.name, - visibility: '1', + visibility: true, description: entity2.description, Placemark: { _: { id: entity3.id }, Point: { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }, name: entity3.name, - visibility: '1', + visibility: true, description: entity3.description, styleUrl: '#style-1' } @@ -292,7 +290,7 @@ defineSuite([ }; expectedResult.Document.Placemark.Point = { altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -344,7 +342,7 @@ defineSuite([ }; expectedResult.Document.Placemark.Point = { altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -365,8 +363,8 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.IconStyle = {}; expectedResult.Document.Placemark.Point = { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -389,8 +387,8 @@ defineSuite([ heading: 360 }; expectedResult.Document.Placemark.Point = { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -423,8 +421,8 @@ defineSuite([ } }; expectedResult.Document.Placemark.Point = { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -457,8 +455,8 @@ defineSuite([ } }; expectedResult.Document.Placemark.Point = { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities); @@ -482,8 +480,8 @@ defineSuite([ } }; expectedResult.Document.Placemark.Point = { - altitudeMode: 'clampToGround', - coordinates: expectedPointPosition + altitudeMode: 'absolute', + coordinates: checkPointCoord }; var kmlExporter = new KmlExporter(entities, { @@ -546,7 +544,9 @@ defineSuite([ var entity1 = createEntity({ position: position, - point: {} + point: { + heightReference: HeightReference.CLAMP_TO_GROUND + } }); var entities = new EntityCollection(); @@ -584,7 +584,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.IconStyle = {}; expectedResult.Document.Placemark.Track = { - altitudeMode: 'clampToGround', + altitudeMode: 'absolute', when: checkWhen, coord: checkCoord }; @@ -608,6 +608,26 @@ defineSuite([ Cartesian3.fromDegrees(-1, 1, 12) ]; + function checkCoords(textContent) { + var coordinates = textContent.split(' '); + expect(coordinates.length).toBe(4); + + var cartographic1 = new Cartographic(); + var cartographic2 = new Cartographic(); + var count = positions.length; + for (var i=0;i Date: Fri, 7 Jun 2019 15:10:44 -0400 Subject: [PATCH 15/27] Models --- Source/DataSources/KmlExporter.js | 169 ++++++++++++++++----------- Specs/DataSources/KmlExporterSpec.js | 62 ++++++++++ 2 files changed, 163 insertions(+), 68 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index c5f36178d809..127a2ca66700 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -1,16 +1,10 @@ define([ './BillboardGraphics', - './ColorMaterialProperty', - './CompositeMaterialProperty', './CompositePositionProperty', - './GridMaterialProperty', - './ImageMaterialProperty', - './PolylineGlowMaterialProperty', - './PolylineOutlineMaterialProperty', + './ModelGraphics', './SampledPositionProperty', './SampledProperty', './ScaledPositionProperty', - './StripeMaterialProperty', './RectangleGraphics', '../Core/Cartesian2', '../Core/Cartesian3', @@ -32,17 +26,11 @@ define([ '../Scene/VerticalOrigin' ], function( BillboardGraphics, - ColorMaterialProperty, - CompositeMaterialProperty, CompositePositionProperty, - GridMaterialProperty, - ImageMaterialProperty, - PolylineGlowMaterialProperty, - PolylineOutlineMaterialProperty, + ModelGraphics, SampledPositionProperty, SampledProperty, ScaledPositionProperty, - StripeMaterialProperty, RectangleGraphics, Cartesian2, Cartesian3, @@ -82,11 +70,21 @@ define([ return texture.toDataURL(); } - if (texture instanceof HTMLImageElement) { - return ''; // TODO + return ''; + } + + function defaultModelCallback(model, time) { + var uri = model.uri; + if (!defined(uri)) { + return ''; } - return ''; + uri = uri.getValue(time); + if (typeof uri !== 'string') { + uri = uri.url; + } + + return uri; } function ValueGetter(time) { @@ -170,11 +168,13 @@ define([ var styleCache = this._styleCache = new StyleCache(); this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); + this._modelCallback = defaultValue(options.modelCallback, defaultModelCallback); // Use the start time as the default because just in case they define // properties with an interval even if they don't change. var entityAvailability = entities.computeAvailability(); - this._valueGetter = new ValueGetter(defined(options.time) ? options.time : entityAvailability.start); + var time = this._time = (defined(options.time) ? options.time : entityAvailability.start); + this._valueGetter = new ValueGetter(time); // Figure out how we will sample dynamic position properties var defaultAvailability = defaultValue(options.defaultAvailability, entityAvailability); @@ -234,7 +234,7 @@ define([ createLineString(that, entity.polyline, geometries, styles); createPolygon(that, entity.rectangle, geometries, styles); createPolygon(that, entity.polygon, geometries, styles); - createModel(that, entity.model, geometries, styles); + createModel(that, entity, entity.model, geometries, styles); var geometryCount = geometries.length; if (geometryCount > 0) { @@ -289,7 +289,6 @@ define([ var styleCount = styles.length; if (styleCount > 0) { - // TODO: Merge if we went up with multiple of same type var style = kmlDoc.createElement('Style'); for (var styleIndex = 0; styleIndex < styleCount; ++styleIndex) { style.appendChild(styles[styleIndex]); @@ -383,6 +382,8 @@ define([ intervals = defaultValue(entity.availability, that._defaultAvailability); } + var isModel = (pointGraphics instanceof ModelGraphics); + var i; var tracks = []; for (i = 0; i < intervals.length; ++i) { @@ -465,6 +466,10 @@ define([ trackGeometry.appendChild(coord); } + if (isModel) { + trackGeometry.appendChild(createModelGeometry(that, pointGraphics)); + } + tracks.push(trackGeometry); } @@ -482,7 +487,7 @@ define([ } // Create style - if (defined(pointGraphics)) { + if (defined(pointGraphics) && !isModel) { var iconStyle = (pointGraphics instanceof BillboardGraphics) ? createIconStyleFromBillboard(that, pointGraphics) : createIconStyleFromPoint(that, pointGraphics); styles.push(iconStyle); @@ -637,15 +642,10 @@ define([ lineStringGeometry.appendChild(altitudeMode); // Set coordinates - var coordinates; var positionsProperty = polylineGraphics.positions; - if (positionsProperty.isConstant) { - var cartesians = positionsProperty.getValue(Iso8601.MINIMUM_VALUE); - coordinates = createBasicElementWithText(kmlDoc, 'coordinates', - getCoordinates(cartesians, ellipsoid)); - } else { - // TODO: Time dynamic - } + var cartesians = valueGetter.get(positionsProperty); + var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(cartesians, ellipsoid)); lineStringGeometry.appendChild(coordinates); // Set draw order @@ -685,23 +685,19 @@ define([ } var coordinatesProperty = rectangleGraphics.coordinates; - if (coordinatesProperty.isConstant) { - var rectangle = coordinatesProperty.getValue(Iso8601.MINIMUM_VALUE); + var rectangle = valueGetter.get(coordinatesProperty); - var coordinateStrings = []; - var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest]; - - for (var i = 0; i < 4; ++i) { - cornerFunction[i](rectangle, scratchCartographic); - coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' + - CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height); - } + var coordinateStrings = []; + var cornerFunction = [Rectangle.northeast, Rectangle.southeast, Rectangle.southwest, Rectangle.northwest]; - coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' ')); - } else { - // TODO: Time dynamic + for (var i = 0; i < 4; ++i) { + cornerFunction[i](rectangle, scratchCartographic); + coordinateStrings.push(CesiumMath.toDegrees(scratchCartographic.longitude) + ',' + + CesiumMath.toDegrees(scratchCartographic.latitude) + ',' + height); } + coordinates = createBasicElementWithText(kmlDoc, 'coordinates', coordinateStrings.join(' ')); + var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs'); var linearRing = kmlDoc.createElement('LinearRing'); linearRing.appendChild(coordinates); @@ -745,29 +741,25 @@ define([ var boundaries = []; var hierarchyProperty = polygonGraphics.hierarchy; - if (hierarchyProperty.isConstant) { - var hierarchy = valueGetter.get(hierarchyProperty); - - // Polygon hierarchy can sometimes just be an array of positions - var positions = isArray(hierarchy) ? hierarchy : hierarchy.positions; - - // Polygon boundaries - var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs'); - outerBoundaryIs.appendChild(getLinearRing(that, positions, height, perPositionHeight)); - boundaries.push(outerBoundaryIs); - - // Hole boundaries - var holes = hierarchy.holes; - if (defined(holes)) { - var holeCount = holes.length; - for (var i = 0; i < holeCount; ++i) { - var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs'); - innerBoundaryIs.appendChild(getLinearRing(that, holes[i].positions, height, perPositionHeight)); - boundaries.push(innerBoundaryIs); - } + var hierarchy = valueGetter.get(hierarchyProperty); + + // Polygon hierarchy can sometimes just be an array of positions + var positions = isArray(hierarchy) ? hierarchy : hierarchy.positions; + + // Polygon boundaries + var outerBoundaryIs = kmlDoc.createElement('outerBoundaryIs'); + outerBoundaryIs.appendChild(getLinearRing(that, positions, height, perPositionHeight)); + boundaries.push(outerBoundaryIs); + + // Hole boundaries + var holes = hierarchy.holes; + if (defined(holes)) { + var holeCount = holes.length; + for (var i = 0; i < holeCount; ++i) { + var innerBoundaryIs = kmlDoc.createElement('innerBoundaryIs'); + innerBoundaryIs.appendChild(getLinearRing(that, holes[i].positions, height, perPositionHeight)); + boundaries.push(innerBoundaryIs); } - } else { - // TODO: Time dynamic } return boundaries; @@ -842,16 +834,57 @@ define([ styles.push(polyStyle); } - function createModel(that, geometry, geometries, styles) { + function createModelGeometry(that, modelGraphics) { var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; - if (!defined(geometry)) { + var modelGeometry = kmlDoc.createElement('Model'); + + var scale = valueGetter.get(modelGraphics.scale); + if (defined(scale)) { + var scaleElement = kmlDoc.createElement('scale'); + scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'x', scale)); + scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'y', scale)); + scaleElement.appendChild(createBasicElementWithText(kmlDoc, 'z', scale)); + modelGeometry.appendChild(scaleElement); + } + + var link = kmlDoc.createElement('Link'); + var uri = that._modelCallback(modelGraphics, that._time); + + link.appendChild(createBasicElementWithText(kmlDoc, 'href', uri)); + modelGeometry.appendChild(link); + + return modelGeometry; + } + + function createModel(that, entity, modelGraphics, geometries, styles) { + var kmlDoc = that._kmlDoc; + var ellipsoid = that._ellipsoid; + + if (!defined(modelGraphics)) { return; } - var modelGeometry = kmlDoc.createElement('Model'); + // If the point isn't constant then create gx:Track or gx:MultiTrack + var entityPositionProperty = entity.position; + if (!entityPositionProperty.isConstant) { + createTracks(that, entity, modelGraphics, geometries, styles); + return; + } + + var modelGeometry = createModelGeometry(that, modelGraphics); + + // Set altitude mode + var altitudeMode = kmlDoc.createElement('altitudeMode'); + altitudeMode.appendChild(getAltitudeMode(that, modelGraphics.heightReference)); + modelGeometry.appendChild(altitudeMode); + + entityPositionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', + getCoordinates(scratchCartesian3, ellipsoid)); - // TODO: Populate info - fire callback + modelGeometry.appendChild(coordinates); geometries.push(modelGeometry); } diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index fd7fb42eef05..561fa9030af3 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -598,6 +598,36 @@ defineSuite([ }); checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); + + it('With Model', function() { + var position = new SampledPositionProperty(); + position.addSamples(times, positions); + + var entity1 = createEntity({ + position: position, + model: { + uri: 'http://test.invalid/test' + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Placemark.Track = { + altitudeMode: 'absolute', + when: checkWhen, + coord: checkCoord, + Model: { + Link: { + href: 'http://test.invalid/test' + } + } + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); }); describe('Polylines', function() { @@ -974,6 +1004,38 @@ defineSuite([ }); describe('Models', function() { + it('Model with constant position', function() { + var entity1 = createEntity({ + model: { + uri: 'http://test.invalid/test.glb', + scale: 3, + heightReference: HeightReference.CLAMP_TO_GROUND + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Placemark.Model = { + altitudeMode: 'clampToGround', + coordinates: checkPointCoord, + Link: { + href: 'http://test.invalid/test.dae' + }, + scale: { + x: 3, + y: 3, + z: 3 + } + }; + var kmlExporter = new KmlExporter(entities, { + modelCallback: function(model, time) { + return model.uri.getValue(time).replace('.glb', '.dae'); + } + }); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); }); }); From 7504837a8625fda49cd8cba58877dc26bf459b5c Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Fri, 7 Jun 2019 16:09:17 -0400 Subject: [PATCH 16/27] Fixed typo in LineString --- Source/DataSources/KmlExporter.js | 28 +++++++++++++++++----------- Specs/DataSources/KmlExporterSpec.js | 25 +++++++++++++++---------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 127a2ca66700..9655d566bd44 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -91,10 +91,10 @@ define([ this._time = time; } - ValueGetter.prototype.get = function(property, defaultVal) { + ValueGetter.prototype.get = function(property, defaultVal, result) { var value; if (defined(property)) { - value = defined(property.getValue) ? property.getValue(this._time) : property; + value = defined(property.getValue) ? property.getValue(this._time, result) : property; } return defaultValue(value, defaultVal); @@ -112,7 +112,7 @@ define([ return; } - return property.getValue(this._time); + return property.getType(this._time); }; function StyleCache() { @@ -334,6 +334,7 @@ define([ function createPoint(that, entity, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; + var valueGetter = that._valueGetter; var pointGraphics = defaultValue(entity.billboard, entity.point); if (!defined(pointGraphics) && !defined(entity.path)) { @@ -347,7 +348,7 @@ define([ return; } - entityPositionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + valueGetter.get(entityPositionProperty, undefined, scratchCartesian3); var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', getCoordinates(scratchCartesian3, ellipsoid)); @@ -412,7 +413,7 @@ define([ var positionValues = []; if (positionProperty.isConstant) { - positionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); + valueGetter.get(positionProperty, undefined, scratchCartesian3); var constCoordinates = createBasicElementWithText(kmlDoc, 'coordinates', getCoordinates(scratchCartesian3, ellipsoid)); @@ -633,7 +634,7 @@ define([ var clampToGround = valueGetter.get(polylineGraphics.clampToGround, false); var altitudeModeText; if (clampToGround) { - lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tesselate', true)); + lineStringGeometry.appendChild(createBasicElementWithText(kmlDoc, 'tessellate', true)); altitudeModeText = kmlDoc.createTextNode('clampToGround'); } else { altitudeModeText = kmlDoc.createTextNode('absolute'); @@ -861,6 +862,7 @@ define([ function createModel(that, entity, modelGraphics, geometries, styles) { var kmlDoc = that._kmlDoc; var ellipsoid = that._ellipsoid; + var valueGetter = that._valueGetter; if (!defined(modelGraphics)) { return; @@ -880,11 +882,13 @@ define([ altitudeMode.appendChild(getAltitudeMode(that, modelGraphics.heightReference)); modelGeometry.appendChild(altitudeMode); - entityPositionProperty.getValue(Iso8601.MINIMUM_VALUE, scratchCartesian3); - var coordinates = createBasicElementWithText(kmlDoc, 'coordinates', - getCoordinates(scratchCartesian3, ellipsoid)); - - modelGeometry.appendChild(coordinates); + valueGetter.get(entityPositionProperty, undefined, scratchCartesian3); + Cartographic.fromCartesian(scratchCartesian3, ellipsoid, scratchCartographic); + var location = kmlDoc.createElement('Location'); + location.appendChild(createBasicElementWithText(kmlDoc, 'longitude', CesiumMath.toDegrees(scratchCartographic.longitude))); + location.appendChild(createBasicElementWithText(kmlDoc, 'latitude', CesiumMath.toDegrees(scratchCartographic.latitude))); + location.appendChild(createBasicElementWithText(kmlDoc, 'altitude', scratchCartographic.height)); + modelGeometry.appendChild(location); geometries.push(modelGeometry); } @@ -908,6 +912,8 @@ define([ case 'Color': case 'Grid': case 'PolylineGlow': + case 'PolylineArrow': + case 'PolylineDash': color = colorToString(material.color); break; case 'PolylineOutline': diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 561fa9030af3..df9b8cc5b2ff 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -674,14 +674,14 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.LineStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal', width: 5 }; expectedResult.Document.Placemark.LineString = { altitudeMode: 'clampToGround', coordinates: checkCoords, - tesselate: true, + tessellate: true, drawOrder: 2 }; @@ -705,7 +705,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.LineStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal', width: 5 }; @@ -738,11 +738,11 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.LineStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal', width: 5, - outlineColor: 'ffff0000', - outlineWidth: 2 + outerColor: 'ffff0000', + outerWidth: 2 }; expectedResult.Document.Placemark.LineString = { altitudeMode: 'absolute', @@ -810,7 +810,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.PolyStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal', fill: true, outline: true @@ -947,7 +947,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.PolyStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal', fill: true, outline: true @@ -986,7 +986,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.PolyStyle = { - color: 'ff00ff00', + color: 'ff008000', colorMode: 'normal' }; expectedResult.Document.Placemark.Polygon = { @@ -1016,10 +1016,15 @@ defineSuite([ var entities = new EntityCollection(); entities.add(entity1); + var cartographic = Cartographic.fromCartesian(pointPosition); var expectedResult = createExpectResult(entity1); expectedResult.Document.Placemark.Model = { altitudeMode: 'clampToGround', - coordinates: checkPointCoord, + Location: { + longitude: CesiumMath.toDegrees(cartographic.longitude), + latitude: CesiumMath.toDegrees(cartographic.latitude), + altitude: cartographic.height + }, Link: { href: 'http://test.invalid/test.dae' }, From 5bad1a4b96dd6761c62ec808621cef3fd5a3411a Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Mon, 10 Jun 2019 11:37:36 -0400 Subject: [PATCH 17/27] Ground Overlay support --- Source/DataSources/KmlExporter.js | 139 +++++++++++++++++++++------ Specs/DataSources/KmlExporterSpec.js | 49 ++++++++++ 2 files changed, 157 insertions(+), 31 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index 9655d566bd44..acda37bf51b6 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -10,6 +10,7 @@ define([ '../Core/Cartesian3', '../Core/Cartographic', '../Core/Color', + '../Core/createGuid', '../Core/defaultValue', '../Core/defined', '../Core/Ellipsoid', @@ -36,6 +37,7 @@ define([ Cartesian3, Cartographic, Color, + createGuid, defaultValue, defined, Ellipsoid, @@ -150,6 +152,24 @@ define([ } }; + function IdManager() { + this._ids = {}; + } + + IdManager.prototype.get = function(id) { + if (!defined(id)) { + return this.get(createGuid()); + } + + var ids = this._ids; + if (!defined(ids[id])) { + ids[id] = 0; + return id; + } + + return id.toString() + '-' + (++ids[id]); + }; + /** * @alias KmlExporter * @constructor @@ -157,7 +177,8 @@ define([ * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file - * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas. By default it will use the URI or a data URI of the image or canvas. + * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas and should return the URI to use in the KML. By default it will use the URI or a data URI of the image or canvas. + * @param {Function} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. By default it will just return the model's uri property directly. * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML. * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability. * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML. @@ -165,6 +186,7 @@ define([ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); + this._idManager = new IdManager(); var styleCache = this._styleCache = new StyleCache(); this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); @@ -187,11 +209,11 @@ define([ defaultAvailability = new TimeInterval(); } else { // No start time, so just sample 10 times before the stop - JulianDate.addSeconds(defaultAvailability.stop, -10*sampleDuration, defaultAvailability.start); + JulianDate.addSeconds(defaultAvailability.stop, -10 * sampleDuration, defaultAvailability.start); } } else if (defaultAvailability.stop === Iso8601.MAXIMUM_VALUE) { // No stop time, so just sample 10 times after the start - JulianDate.addSeconds(defaultAvailability.start, 10*sampleDuration, defaultAvailability.stop); + JulianDate.addSeconds(defaultAvailability.start, 10 * sampleDuration, defaultAvailability.stop); } // Wrap it in a TimeIntervalCollection because that is what entity.availability is @@ -221,25 +243,59 @@ define([ var kmlDoc = that._kmlDoc; var styleCache = that._styleCache; var valueGetter = that._valueGetter; + var idManager = that._idManager; var count = entities.length; + var overlays; var geometries; var styles; for (var i = 0; i < count; ++i) { var entity = entities[i]; + overlays = []; geometries = []; styles = []; createPoint(that, entity, geometries, styles); createLineString(that, entity.polyline, geometries, styles); - createPolygon(that, entity.rectangle, geometries, styles); - createPolygon(that, entity.polygon, geometries, styles); + createPolygon(that, entity.rectangle, geometries, styles, overlays); + createPolygon(that, entity.polygon, geometries, styles, overlays); createModel(that, entity, entity.model, geometries, styles); + var timeSpan; + var availability = entity.availability; + if (defined(availability)) { + timeSpan = kmlDoc.createElement('TimeSpan'); + + if (!JulianDate.equals(availability.start, Iso8601.MINIMUM_VALUE)) { + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin', + JulianDate.toIso8601(availability.start))); + } + + if (!JulianDate.equals(availability.stop, Iso8601.MAXIMUM_VALUE)) { + timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end', + JulianDate.toIso8601(availability.stop))); + } + } + + for (var overlayIndex = 0; overlayIndex < overlays.length; ++overlayIndex) { + var overlay = overlays[overlayIndex]; + + overlay.setAttribute('id', idManager.get(entity.id)); + overlay.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); + overlay.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); + overlay.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); + + if (defined(timeSpan)) { + overlay.appendChild(timeSpan); + } + + parentNode.appendChild(overlay); + } + var geometryCount = geometries.length; if (geometryCount > 0) { var placemark = kmlDoc.createElement('Placemark'); - placemark.setAttribute('id', entity.id); + placemark.setAttribute('id', idManager.get(entity.id)); var name = entity.name; var labelGraphics = entity.label; @@ -268,20 +324,7 @@ define([ placemark.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); placemark.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); - var availability = entity.availability; - if (defined(availability)) { - var timeSpan = kmlDoc.createElement('TimeSpan'); - - if (!JulianDate.equals(availability.start, Iso8601.MINIMUM_VALUE)) { - timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'begin', - JulianDate.toIso8601(availability.start))); - } - - if (!JulianDate.equals(availability.stop, Iso8601.MAXIMUM_VALUE)) { - timeSpan.appendChild(createBasicElementWithText(kmlDoc, 'end', - JulianDate.toIso8601(availability.stop))); - } - + if (defined(timeSpan)) { placemark.appendChild(timeSpan); } @@ -311,11 +354,7 @@ define([ var children = entity._children; if (children.length > 0) { var folderNode = kmlDoc.createElement('Folder'); - // The Placemark and Folder can't have the same ID - if (geometryCount === 0) { - folderNode.setAttribute('id', entity.id); - } - + folderNode.setAttribute('id', idManager.get(entity.id)); folderNode.appendChild(createBasicElementWithText(kmlDoc, 'name', entity.name)); folderNode.appendChild(createBasicElementWithText(kmlDoc, 'visibility', entity.show)); folderNode.appendChild(createBasicElementWithText(kmlDoc, 'description', entity.description)); @@ -397,7 +436,7 @@ define([ if (positionProperty instanceof ScaledPositionProperty) { positionProperty = positionProperty._value; trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.CLAMP_TO_GROUND)); - } else if (defined(pointGraphics)){ + } else if (defined(pointGraphics)) { trackAltitudeMode.appendChild(getAltitudeMode(that, pointGraphics.heightReference)); } else { // Path graphics only, which has no height reference @@ -428,7 +467,7 @@ define([ for (var j = 0; j < times.length; ++j) { positionTimes.push(JulianDate.toIso8601(times[j])); - Cartesian3.fromArray(values, j*3, scratchCartesian3); + Cartesian3.fromArray(values, j * 3, scratchCartesian3); positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); } } else { @@ -766,7 +805,7 @@ define([ return boundaries; } - function createPolygon(that, geometry, geometries, styles) { + function createPolygon(that, geometry, geometries, styles, overlays) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; @@ -777,8 +816,7 @@ define([ // Detect textured quads and use ground overlays instead var isRectangle = (geometry instanceof RectangleGraphics); if (isRectangle && valueGetter.getMaterialType(geometry.material) === 'Image') { - // TODO - // TODO: stRotation + createGroundOverlay(that, geometry, overlays); return; } @@ -835,6 +873,45 @@ define([ styles.push(polyStyle); } + function createGroundOverlay(that, rectangleGraphics, overlays) { + var kmlDoc = that._kmlDoc; + var valueGetter = that._valueGetter; + + var groundOverlay = kmlDoc.createElement('GroundOverlay'); + + // Set altitude mode + var altitudeMode = kmlDoc.createElement('altitudeMode'); + altitudeMode.appendChild(getAltitudeMode(that, rectangleGraphics.heightReference)); + groundOverlay.appendChild(altitudeMode); + + var height = valueGetter.get(rectangleGraphics.height); + if (defined(height)) { + groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'altitude', height)); + } + + var rectangle = valueGetter.get(rectangleGraphics.coordinates); + var latLonBox = kmlDoc.createElement('LatLonBox'); + latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'north', CesiumMath.toDegrees(rectangle.north))); + latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'south', CesiumMath.toDegrees(rectangle.south))); + latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'east', CesiumMath.toDegrees(rectangle.east))); + latLonBox.appendChild(createBasicElementWithText(kmlDoc, 'west', CesiumMath.toDegrees(rectangle.west))); + groundOverlay.appendChild(latLonBox); + + // We should only end up here if we have an ImageMaterialProperty + var material = valueGetter.get(rectangleGraphics.material); + var href = that._textureCallback(material.image); + var icon = kmlDoc.createElement('Icon'); + icon.appendChild(createBasicElementWithText(kmlDoc, 'href', href)); + groundOverlay.appendChild(icon); + + var color = material.color; + if (defined(color)) { + groundOverlay.appendChild(createBasicElementWithText(kmlDoc, 'color', colorToString(material.color))); + } + + overlays.push(groundOverlay); + } + function createModelGeometry(that, modelGraphics) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; @@ -908,7 +985,7 @@ define([ var color; var type = valueGetter.getMaterialType(materialProperty); - switch(type) { + switch (type) { case 'Color': case 'Grid': case 'PolylineGlow': diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index df9b8cc5b2ff..48414125640f 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -17,6 +17,7 @@ defineSuite([ 'DataSources/ColorMaterialProperty', 'DataSources/Entity', 'DataSources/EntityCollection', + 'DataSources/ImageMaterialProperty', 'DataSources/KmlDataSource', 'DataSources/PolylineOutlineMaterialProperty', 'DataSources/SampledPositionProperty', @@ -42,6 +43,7 @@ defineSuite([ ColorMaterialProperty, Entity, EntityCollection, + ImageMaterialProperty, KmlDataSource, PolylineOutlineMaterialProperty, SampledPositionProperty, @@ -1043,4 +1045,51 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); }); + + describe('GroundOverlays', function() { + it('Rectangle', function(){ + var entity = createEntity({ + rectangle: { + coordinates: Rectangle.fromDegrees(-1, -1, 1, 1), + height: 10, + heightReference: HeightReference.CLAMP_TO_GROUND, + material: new ImageMaterialProperty({ + image: '../images/logo.jpg', + color: Color.GREEN + }) + } + }); + + var entities = new EntityCollection(); + entities.add(entity); + + var expectedResult = { + Document: { + GroundOverlay: { + _: { + id: entity.id + }, + name: entity.name, + visibility: entity.show ? 1 : 0, + description: entity.description, + altitude: 10, + altitudeMode: 'clampToGround', + LatLonBox: { + north: 1, + south: -1, + east: 1, + west: -1 + }, + Icon: { + href: '../images/logo.jpg' + }, + color: 'ff008000' + } + } + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + }); }); From 3712a4336ae14badc48f3766c1e9afe92d34b4af Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Wed, 19 Jun 2019 13:54:06 -0400 Subject: [PATCH 18/27] Added tests for missing coverage areas, we now return external files as a collection of blobs and we now handle inertial positions. --- Source/DataSources/KmlExporter.js | 119 +++++++++++++----- Specs/DataSources/KmlExporterSpec.js | 175 ++++++++++++++++++++++++--- 2 files changed, 242 insertions(+), 52 deletions(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index acda37bf51b6..a4872244aac1 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -13,18 +13,22 @@ define([ '../Core/createGuid', '../Core/defaultValue', '../Core/defined', + '../Core/defineProperties', '../Core/Ellipsoid', '../Core/isArray', '../Core/Iso8601', '../Core/JulianDate', '../Core/Math', '../Core/Rectangle', + '../Core/ReferenceFrame', '../Core/Resource', + '../Core/RuntimeError', '../Core/TimeInterval', '../Core/TimeIntervalCollection', '../Scene/HeightReference', '../Scene/HorizontalOrigin', - '../Scene/VerticalOrigin' + '../Scene/VerticalOrigin', + '../ThirdParty/when' ], function( BillboardGraphics, CompositePositionProperty, @@ -40,18 +44,22 @@ define([ createGuid, defaultValue, defined, + defineProperties, Ellipsoid, isArray, Iso8601, JulianDate, CesiumMath, Rectangle, + ReferenceFrame, Resource, + RuntimeError, TimeInterval, TimeIntervalCollection, HeightReference, HorizontalOrigin, - VerticalOrigin) { + VerticalOrigin, + when) { 'use strict'; var BILLBOARD_SIZE = 32; @@ -59,7 +67,14 @@ define([ var gxNamespace = 'http://www.google.com/kml/ext/2.2'; var xmlnsNamespace = 'http://www.w3.org/2000/xmlns/'; - function defaultTextureCallback(texture) { + function ExternalFileHandler(modelCallback) { + this._files = {}; + this._promises = []; + this._count = 0; + this._modelCallback = modelCallback; + } + + ExternalFileHandler.prototype.texture = function(texture) { if (typeof texture === 'string') { return texture; } @@ -69,25 +84,53 @@ define([ } if (texture instanceof HTMLCanvasElement) { - return texture.toDataURL(); + var deferred = when.defer(); + this._promises.push(deferred.promise); + + var filename = 'texture_' + (++this._count) + '.png'; + var that = this; + texture.toBlob(function(blob) { + that._files[filename] = blob; + deferred.resolve(); + }); + + return filename; } return ''; - } + }; - function defaultModelCallback(model, time) { - var uri = model.uri; - if (!defined(uri)) { - return ''; + ExternalFileHandler.prototype.model = function(model, time) { + var modelCallback = this._modelCallback; + if (!defined(modelCallback)) { + throw new RuntimeError('Model callback must be specified if there are models in the entity collection'); } - uri = uri.getValue(time); - if (typeof uri !== 'string') { - uri = uri.url; - } + return modelCallback(model, time); + }; - return uri; - } + defineProperties(ExternalFileHandler.prototype, { + /** + * Resolves when all external files are ready + * @memberof GlExternalFileHandlerobe.prototype + * @type {Promise} + */ + promise : { + get : function() { + return when.all(this._promises); + } + }, + /** + * An object with filename and blobs for external files + * @memberof ExternalFileHandler.prototype + * @type {ObjectId} + */ + files : { + get : function() { + return this._files; + } + } + }); function ValueGetter(time) { this._time = time; @@ -177,7 +220,6 @@ define([ * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file - * @param {Function} [options.textureCallback] A callback that will be called with an image, URI or Canvas and should return the URI to use in the KML. By default it will use the URI or a data URI of the image or canvas. * @param {Function} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. By default it will just return the model's uri property directly. * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML. * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability. @@ -189,8 +231,7 @@ define([ this._idManager = new IdManager(); var styleCache = this._styleCache = new StyleCache(); - this._textureCallback = defaultValue(options.textureCallback, defaultTextureCallback); - this._modelCallback = defaultValue(options.modelCallback, defaultModelCallback); + this._externalFileHandler = new ExternalFileHandler(options.modelCallback); // Use the start time as the default because just in case they define // properties with an interval even if they don't change. @@ -234,9 +275,17 @@ define([ styleCache.save(kmlDocumentElement); } - KmlExporter.prototype.toString = function() { - var serializer = new XMLSerializer(); - return serializer.serializeToString(this._kmlDoc); + KmlExporter.prototype.export = function() { + var externalFileHandler = this._externalFileHandler; + var that = this; + return externalFileHandler.promise + .then(function() { + var serializer = new XMLSerializer(); + return { + kml: serializer.serializeToString(that._kmlDoc), + externalFiles: externalFileHandler.files + }; + }); }; function recurseEntities(that, parentNode, entities) { @@ -424,7 +473,7 @@ define([ var isModel = (pointGraphics instanceof ModelGraphics); - var i; + var i, j, times; var tracks = []; for (i = 0; i < intervals.length; ++i) { var interval = intervals.get(i); @@ -443,11 +492,6 @@ define([ trackAltitudeMode.appendChild(getAltitudeMode(that, HeightReference.NONE)); } - // We need the raw samples, so just use the internal SampledProperty - if (positionProperty instanceof SampledPositionProperty) { - positionProperty = positionProperty._property; - } - var positionTimes = []; var positionValues = []; @@ -461,11 +505,19 @@ define([ positionValues.push(constCoordinates); positionTimes.push(JulianDate.toIso8601(interval.stop)); positionValues.push(constCoordinates); + } else if (positionProperty instanceof SampledPositionProperty) { + times = positionProperty._property._times; + + for (j = 0; j < times.length; ++j) { + positionTimes.push(JulianDate.toIso8601(times[j])); + positionProperty.getValueInReferenceFrame(times[j], ReferenceFrame.FIXED, scratchCartesian3); + positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); + } } else if (positionProperty instanceof SampledProperty) { - var times = positionProperty._times; + times = positionProperty._times; var values = positionProperty._values; - for (var j = 0; j < times.length; ++j) { + for (j = 0; j < times.length; ++j) { positionTimes.push(JulianDate.toIso8601(times[j])); Cartesian3.fromArray(values, j * 3, scratchCartesian3); positionValues.push(getCoordinates(scratchCartesian3, ellipsoid)); @@ -573,12 +625,13 @@ define([ function createIconStyleFromBillboard(that, billboardGraphics) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; + var externalFileHandler = that._externalFileHandler; var iconStyle = kmlDoc.createElement('IconStyle'); var image = valueGetter.get(billboardGraphics.image); if (defined(image)) { - image = that._textureCallback(image); + image = externalFileHandler.texture(image); var icon = kmlDoc.createElement('Icon'); icon.appendChild(createBasicElementWithText(kmlDoc, 'href', image)); @@ -876,6 +929,7 @@ define([ function createGroundOverlay(that, rectangleGraphics, overlays) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; + var externalFileHandler = that._externalFileHandler; var groundOverlay = kmlDoc.createElement('GroundOverlay'); @@ -899,7 +953,7 @@ define([ // We should only end up here if we have an ImageMaterialProperty var material = valueGetter.get(rectangleGraphics.material); - var href = that._textureCallback(material.image); + var href = externalFileHandler.texture(material.image); var icon = kmlDoc.createElement('Icon'); icon.appendChild(createBasicElementWithText(kmlDoc, 'href', href)); groundOverlay.appendChild(icon); @@ -915,6 +969,7 @@ define([ function createModelGeometry(that, modelGraphics) { var kmlDoc = that._kmlDoc; var valueGetter = that._valueGetter; + var externalFileHandler = that._externalFileHandler; var modelGeometry = kmlDoc.createElement('Model'); @@ -928,7 +983,7 @@ define([ } var link = kmlDoc.createElement('Link'); - var uri = that._modelCallback(modelGraphics, that._time); + var uri = externalFileHandler.model(modelGraphics, that._time); link.appendChild(createBasicElementWithText(kmlDoc, 'href', uri)); modelGeometry.appendChild(link); diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 48414125640f..7d9002be66a3 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -87,16 +87,6 @@ defineSuite([ } }; - xit('test', function() { - return KmlDataSource.load('../Apps/SampleData/kml/facilities/facilities.kml', options) - .then(function(datasource) { - var exporter = new KmlExporter(datasource.entities); - - var kml = exporter.toString(); - download('test.kml', kml); - }); - }); - function checkKmlDoc(kmlDoc, properties) { var kml = kmlDoc.documentElement; var kmlChildNodes = kml.childNodes; @@ -299,6 +289,44 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); + it('Point with label', function() { + var entity1 = createEntity({ + point: { + color: Color.LINEN, + pixelSize: 3, + heightReference: HeightReference.CLAMP_TO_GROUND + }, + label: { + text: 'Im a label', + color: Color.ORANGE, + scale: 2 + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = { + color: 'ffe6f0fa', + colorMode: 'normal', + scale: 3 / 32 + }; + expectedResult.Document.Style.LabelStyle = { + color: 'ff00a5ff', + colorMode: 'normal', + scale: 2 + }; + expectedResult.Document.Placemark.name = 'Im a label'; + expectedResult.Document.Placemark.Point = { + altitudeMode: 'clampToGround', + coordinates: checkPointCoord + }; + + var kmlExporter = new KmlExporter(entities); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + it('Billboard with constant position', function() { var entity1 = createEntity({ billboard: { @@ -478,7 +506,7 @@ defineSuite([ var expectedResult = createExpectResult(entity1); expectedResult.Document.Style.IconStyle = { Icon: { - href: 'http://test.invalid/images/myTexture.jpg' + href: 'texture_1.png' } }; expectedResult.Document.Placemark.Point = { @@ -486,16 +514,15 @@ defineSuite([ coordinates: checkPointCoord }; - var kmlExporter = new KmlExporter(entities, { - textureCallback: function(texture) { - if (texture instanceof HTMLCanvasElement) { - return 'http://test.invalid/images/myTexture.jpg'; - } - - fail(); - } - }); + var kmlExporter = new KmlExporter(entities); checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + + return kmlExporter.export() + .then(function(result) { + expect(result.kml).toBeDefined(); + expect(Object.keys(result.externalFiles).length).toBe(1); + expect(result.externalFiles['texture_1.png']).toBeDefined(); + }); }); }); @@ -627,6 +654,45 @@ defineSuite([ } }; + var kmlExporter = new KmlExporter(entities, { + modelCallback: function(model) { + return model.uri; + } + }); + checkKmlDoc(kmlExporter._kmlDoc, expectedResult); + }); + + it('With Path', function() { + var position = new SampledPositionProperty(); + position.addSamples(times, positions); + + var entity1 = createEntity({ + position: position, + point: { + heightReference: HeightReference.CLAMP_TO_GROUND + }, + path: { + width: 2, + material: new ColorMaterialProperty(Color.GREEN) + } + }); + + var entities = new EntityCollection(); + entities.add(entity1); + + var expectedResult = createExpectResult(entity1); + expectedResult.Document.Style.IconStyle = {}; + expectedResult.Document.Style.LineStyle = { + color: 'ff008000', + colorMode: 'normal', + width: 2 + }; + expectedResult.Document.Placemark.Track = { + altitudeMode: 'clampToGround', + when: checkWhen, + coord: checkCoord + }; + var kmlExporter = new KmlExporter(entities); checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); @@ -1092,4 +1158,73 @@ defineSuite([ checkKmlDoc(kmlExporter._kmlDoc, expectedResult); }); }); + + describe('Multigeometry', function() { + var positions = [ + Cartesian3.fromDegrees(-1, -1, 12), + Cartesian3.fromDegrees(1, -1, 12), + Cartesian3.fromDegrees(1, 1, 12), + Cartesian3.fromDegrees(-1, 1, 12) + ]; + + function getCheckCoords() { + return function(textContent) { + var coordinates = textContent.split(' '); + expect(coordinates.length).toBe(4); + + var cartographic1 = new Cartographic(); + var cartographic2 = new Cartographic(); + var count = positions.length; + for (var i=0;i Date: Wed, 19 Jun 2019 13:55:24 -0400 Subject: [PATCH 19/27] eslint --- Specs/DataSources/KmlExporterSpec.js | 39 ---------------------------- 1 file changed, 39 deletions(-) diff --git a/Specs/DataSources/KmlExporterSpec.js b/Specs/DataSources/KmlExporterSpec.js index 7d9002be66a3..75ffdc5a4da3 100644 --- a/Specs/DataSources/KmlExporterSpec.js +++ b/Specs/DataSources/KmlExporterSpec.js @@ -9,7 +9,6 @@ defineSuite([ 'Core/Iso8601', 'Core/JulianDate', 'Core/Math', - 'Core/PerspectiveFrustum', 'Core/PolygonHierarchy', 'Core/Rectangle', 'Core/TimeInterval', @@ -18,7 +17,6 @@ defineSuite([ 'DataSources/Entity', 'DataSources/EntityCollection', 'DataSources/ImageMaterialProperty', - 'DataSources/KmlDataSource', 'DataSources/PolylineOutlineMaterialProperty', 'DataSources/SampledPositionProperty', 'Scene/HeightReference', @@ -35,7 +33,6 @@ defineSuite([ Iso8601, JulianDate, CesiumMath, - PerspectiveFrustum, PolygonHierarchy, Rectangle, TimeInterval, @@ -44,7 +41,6 @@ defineSuite([ Entity, EntityCollection, ImageMaterialProperty, - KmlDataSource, PolylineOutlineMaterialProperty, SampledPositionProperty, HeightReference, @@ -52,41 +48,6 @@ defineSuite([ VerticalOrigin) { 'use strict'; - function download(filename, data) { - var blob = new Blob([data], { type: 'application/xml' }); - if (window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveBlob(blob, filename); - } else { - var elem = window.document.createElement('a'); - elem.href = window.URL.createObjectURL(blob); - elem.download = filename; - document.body.appendChild(elem); - elem.click(); - document.body.removeChild(elem); - } - } - - var options = { - camera: { - positionWC: new Cartesian3(0.0, 0.0, 0.0), - directionWC: new Cartesian3(0.0, 0.0, 1.0), - upWC: new Cartesian3(0.0, 1.0, 0.0), - pitch: 0.0, - heading: 0.0, - frustum: new PerspectiveFrustum(), - computeViewRectangle: function() { - return Rectangle.MAX_VALUE; - }, - pickEllipsoid: function() { - return undefined; - } - }, - canvas: { - clientWidth: 512, - clientHeight: 512 - } - }; - function checkKmlDoc(kmlDoc, properties) { var kml = kmlDoc.documentElement; var kmlChildNodes = kml.childNodes; From c61717d4870c9c71a3b8c7c5b6668e430d5859cd Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Thu, 20 Jun 2019 16:09:47 -0400 Subject: [PATCH 20/27] Updated doc --- Source/DataSources/KmlExporter.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/KmlExporter.js index a4872244aac1..e5a42d9643af 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/KmlExporter.js @@ -214,16 +214,38 @@ define([ }; /** + * Exports an EntityCollection as a KML document. Only Point, Billboard, Model, Path, Polygon, Polyline geometries will be exported. If your + * document contains a Model, you must specify a modelCallback to handle the conversion as KML doesn't allow for glTF models. There is not + * a 1 to 1 mapping of Entity properties to KML Feature properties, so there are some caveats. Entity properties that are time dynamic but + * cannot be dynamic in KML are exported with their values at options.time or the beginning of the EntityCollection if not specified. For time-dynamic + * properties that are supported in KML, we use the samples if it is a SampledProperty otherwise we sample the value using the options.sampleDuration. + * Point, Billboard, Model and Path geometries with time-dynamic positions will be exported as gx:Track Features. Not all Materials are representable + * in KML, so for more advanced Materials just the primary color is used. Canvas objects are exported as png images. + * * @alias KmlExporter * @constructor * * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file - * @param {Function} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. By default it will just return the model's uri property directly. + * @param {Function} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. This will throw a RuntimeError if there is a model and this is undefined. * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML. * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability. * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML. + * + * @example + * var exporter = new Cesium.KmlExporter(entityCollection); + * var kml = exporter.export() + * .then(function(result) { + * // The XML string is in result.kml + * + * var externalFiles = result.externalFiles + * for(var file in externalFiles) { + * // file is the name of the file used in the KML document as the href + * // externalFiles[file] is a blob with the contents of the file + * } + * }); + * */ function KmlExporter(entities, options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); @@ -1041,6 +1063,11 @@ define([ var color; var type = valueGetter.getMaterialType(materialProperty); switch (type) { + case 'Image': + // Image materials are only able to be represented on rectangles, so if we make it + // here we can't texture a generic polygon or polyline in KML, so just use white. + color = colorToString(Color.WHITE); + break; case 'Color': case 'Grid': case 'PolylineGlow': From eb608e1da54da4d7de95ed28b241518089e9f185 Mon Sep 17 00:00:00 2001 From: Tom Fili Date: Fri, 21 Jun 2019 10:28:25 -0400 Subject: [PATCH 21/27] Changed class to a function, updated doc and fixed tests --- .../{KmlExporter.js => exportKml.js} | 317 ++++++++++-------- .../{KmlExporterSpec.js => exportKmlSpec.js} | 107 +++--- 2 files changed, 229 insertions(+), 195 deletions(-) rename Source/DataSources/{KmlExporter.js => exportKml.js} (83%) rename Specs/DataSources/{KmlExporterSpec.js => exportKmlSpec.js} (92%) diff --git a/Source/DataSources/KmlExporter.js b/Source/DataSources/exportKml.js similarity index 83% rename from Source/DataSources/KmlExporter.js rename to Source/DataSources/exportKml.js index e5a42d9643af..32a63899489a 100644 --- a/Source/DataSources/KmlExporter.js +++ b/Source/DataSources/exportKml.js @@ -14,6 +14,7 @@ define([ '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', + '../Core/DeveloperError', '../Core/Ellipsoid', '../Core/isArray', '../Core/Iso8601', @@ -45,6 +46,7 @@ define([ defaultValue, defined, defineProperties, + DeveloperError, Ellipsoid, isArray, Iso8601, @@ -67,6 +69,9 @@ define([ var gxNamespace = 'http://www.google.com/kml/ext/2.2'; var xmlnsNamespace = 'http://www.w3.org/2000/xmlns/'; + // + // Handles files external to the KML (eg. textures and models) + // function ExternalFileHandler(modelCallback) { this._files = {}; this._promises = []; @@ -110,21 +115,11 @@ define([ }; defineProperties(ExternalFileHandler.prototype, { - /** - * Resolves when all external files are ready - * @memberof GlExternalFileHandlerobe.prototype - * @type {Promise} - */ promise : { get : function() { return when.all(this._promises); } }, - /** - * An object with filename and blobs for external files - * @memberof ExternalFileHandler.prototype - * @type {ObjectId} - */ files : { get : function() { return this._files; @@ -132,6 +127,9 @@ define([ } }); + // + // Handles getting values from properties taking the desired time and default values into account + // function ValueGetter(time) { this._time = time; } @@ -160,6 +158,9 @@ define([ return property.getType(this._time); }; + // + // Caches styles so we don't generate a ton of duplicate styles + // function StyleCache() { this._ids = {}; this._styles = {}; @@ -195,6 +196,9 @@ define([ } }; + // + // Manages the generation of IDs because an entity may have geometry and a Folder for children + // function IdManager() { this._ids = {}; } @@ -222,20 +226,20 @@ define([ * Point, Billboard, Model and Path geometries with time-dynamic positions will be exported as gx:Track Features. Not all Materials are representable * in KML, so for more advanced Materials just the primary color is used. Canvas objects are exported as png images. * - * @alias KmlExporter - * @constructor + * @exports exportKml * - * @param {EntityCollection} entities The EntityCollection to export as KML * @param {Object} options An object with the following properties: - * @param {Function} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file - * @param {Function} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. This will throw a RuntimeError if there is a model and this is undefined. + * @param {EntityCollection} options.entities The EntityCollection to export as KML + * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid for the output file + * @param {exportKml~ModelCallback} [options.modelCallback] A callback that will be called with a ModelGraphics instance and should return the URI to use in the KML. This will throw a RuntimeError if there is a model and this is undefined. * @param {JulianDate} [options.time=entities.computeAvailability().start] The time value to use to get properties that are not time varying in KML. * @param {TimeInterval} [options.defaultAvailability=entities.computeAvailability()] The interval that will be sampled if an entity doesn't have an availability. * @param {Number} [options.sampleDuration=60] The number of seconds to sample properties that are varying in KML. * + * @returns {Promise} A promise that resolved to an object containing the KML string and a dictionary of external file blobs. + * * @example - * var exporter = new Cesium.KmlExporter(entityCollection); - * var kml = exporter.export() + * Cesium.exportKml(entityCollection) * .then(function(result) { * // The XML string is in result.kml * @@ -247,23 +251,63 @@ define([ * }); * */ - function KmlExporter(entities, options) { + function exportKml(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); - this._ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84); - this._idManager = new IdManager(); + var entities = options.entities; + + //>>includeStart('debug', pragmas.debug); + if (!defined(entities)) { + throw new DeveloperError('entities is required.'); + } + //>>includeEnd('debug'); + + // Get the state that is passed around during the recursion + // This is separated out for testing. + var state = exportKml._createState(options); + + // Filter EntityCollection so we only have top level entities + var rootEntities = entities.values.filter(function(entity) { + return !defined(entity.parent); + }); + + // Add the + var kmlDoc = state.kmlDoc; + var kmlElement = kmlDoc.documentElement; + kmlElement.setAttributeNS(xmlnsNamespace, 'xmlns:gx', gxNamespace); + var kmlDocumentElement = kmlDoc.createElement('Document'); + kmlElement.appendChild(kmlDocumentElement); + + // Create the KML Hierarchy + recurseEntities(state, kmlDocumentElement, rootEntities); + + // Write out the