diff --git a/Source/DynamicScene/GeoJsonDataSource.css b/Source/DynamicScene/GeoJsonDataSource.css
new file mode 100644
index 000000000000..1f46387a4bd7
--- /dev/null
+++ b/Source/DynamicScene/GeoJsonDataSource.css
@@ -0,0 +1,7 @@
+.geoJsonDataSourceTable tr:nth-child(even) {
+ background: #EEEEEE;
+}
+
+.geoJsonDataSourceTable tr:nth-child(odd) {
+ background: #CCCCCC;
+}
\ No newline at end of file
diff --git a/Source/DynamicScene/GeoJsonDataSource.js b/Source/DynamicScene/GeoJsonDataSource.js
index 63b5e2c512ca..ba43e4c85c05 100644
--- a/Source/DynamicScene/GeoJsonDataSource.js
+++ b/Source/DynamicScene/GeoJsonDataSource.js
@@ -56,6 +56,13 @@ define([
return value;
};
+ ConstantPositionProperty.prototype.getValueCartographic = function(time, result) {
+ if (Array.isArray(this._value)) {
+ return Ellipsoid.WGS84.cartesianArrayToCartographicArray(this._value, result);
+ }
+ return Ellipsoid.WGS84.cartesianToCartographic(this._value, result);
+ };
+
//GeoJSON specifies only the Feature object has a usable id property
//But since "multi" geometries create multiple dynamicObject,
//we can't use it for them either.
@@ -72,8 +79,25 @@ define([
}
id = finalId;
}
+
var dynamicObject = dynamicObjectCollection.getOrCreateObject(id);
dynamicObject.geoJson = geoJson;
+ dynamicObject.balloon = {
+ getValue : function() {
+ var html;
+ var properties = geoJson.properties;
+ if (typeof properties !== 'undefined') {
+ html = '
';
+ for ( var key in properties) {
+ if (properties.hasOwnProperty(key)) {
+ html += '' + key + ' | ' + properties[key] + ' |
';
+ }
+ }
+ html += '
';
+ }
+ return html;
+ }
+ };
return dynamicObject;
}
diff --git a/Source/Widgets/Balloon/Balloon.css b/Source/Widgets/Balloon/Balloon.css
new file mode 100644
index 000000000000..b3c5ad86d8d3
--- /dev/null
+++ b/Source/Widgets/Balloon/Balloon.css
@@ -0,0 +1,103 @@
+.cesium-balloon-wrapper {
+ position: absolute;
+ font-size: 10pt;
+ background-color: #fff;
+ border-radius: 5px;
+ z-index: 1;
+ border: 1px solid #ccc;
+ padding: 5px;
+ min-height: 28px;
+ min-width: 28px;
+}
+
+.cesium-balloon-content {
+ overflow: auto;
+ margin-right: 20px;
+}
+
+.cesium-balloon-point-container{
+ position: absolute;
+ overflow: hidden;
+ z-index: 2;
+}
+
+.cesium-balloon-point-container-downup {
+ height: 16px;
+ width: 32px;
+}
+
+.cesium-balloon-point-container-leftright {
+ height: 32px;
+ width: 16px;
+}
+
+.cesium-balloon-point {
+ background-color: #fff;
+ height: 20px;
+ width: 20px;
+ -webkit-transform: rotate(45deg);
+ -moz-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ -o-transform: rotate(45deg);
+ transform: rotate(45deg);
+ border: 1px solid #ccc;
+}
+
+.cesium-balloon-point-down {
+ margin-left: 5px;
+ margin-top: -11px;
+}
+
+.cesium-balloon-point-up {
+ margin-left: 5px;
+ margin-top: 5px;
+}
+
+.cesium-balloon-point-left {
+ margin-top: 5px;
+ margin-left: 5px;
+}
+
+.cesium-balloon-point-right {
+ margin-left: -11px;
+ margin-top: 5px;
+}
+
+.cesium-balloon-wrapper-visible {
+ visibility: visible;
+ opacity: 1;
+ transition: opacity 0.2s linear;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+}
+
+.cesium-balloon-wrapper-hidden {
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0s 0.2s, opacity 0.2s linear;
+ -webkit-transition: visibility 0s 0.2s, opacity 0.2s linear;
+ -moz-transition: visibility 0s 0.2s, opacity 0.2s linear;
+}
+
+.cesium-balloon-point-show{
+ visibility: visible;
+}
+
+.cesium-balloon-point-hide{
+ visibility: hidden;
+}
+
+.cesium-balloon-close {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ height: 16px;
+ width: 16px;
+ display: block;
+ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAgCAYAAAAbifjMAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAl9JREFUSEullL1OG0EUhbegXPc8AEXKSLigoKBISZkHQLZl8fcWLt3kp4gEic07+AEoIp7ABQWFSz+AS4rN983e3YzZmCBxpSPfe+ac2Zk74ylexpei7P8serPbovc0K3qVMJf7yljIuoGgvCl6d2Faggk4C0zBClRq1IatDsgS8gHBGnwOeivg98AIbNTqiaE0gV/W/CGonYHm2En0JMJ9QbjsT4kgyI/AfpTW+3JRWrut6ps9oUEzit8x1pg34BFoFOZy+SQrG+sEdvs6+OZrGlyVv3mer2qq18TBk+BTKASNsWM2qNM23j9BbGEU/Euzv3meb2GStmAjKBbBO/DWJi5TEz0KCr/QXlOF4LVjPAX1MRpcijnE1hJ3BZoDsGb586Dq/8Gv+io7ycegO8HYiWa1eoKuQ+JHUd4ieAYL4L3XIK7BPajQ3HTMeXwvykObE6eTjtFczrGQ7Y7BYNC/uLiYgaerq6tKmMs5FrJuICjPz8/vwrQEE3AWmIIVqNSoDVsdkOXl5eUDgjX453sAvwdGYKNWTwylCfyy5v++B2iOnURPIobDYR/CZbfvAfkRaO+EuVyU1m6r0uveZxTte6AQbMAj0CjM5fJJVnqdwG637wF5Y3BV/uZ5vqqpXhMHt/7OCkFj7JgN6rSN908QW2jfA4WgMfub5/kWJmkLNoKifQ/I39rEpd78GNtrqhC8doynoD5Gg0sxh9ha4q5AcwDWfP3ve8DVbK6yk+x8Dxg70axWT9B1SIzH41sEz2ABvPcaxDW4BxWam445j9FodGhz7LAGYS7nWMgiiuIPrDnc+wQrd0kAAAAASUVORK5CYII=) bottom;
+ text-indent: -99999px;
+}
+
+.cesium-balloon-close:hover {
+ background-position: 0 0;
+}
diff --git a/Source/Widgets/Balloon/Balloon.js b/Source/Widgets/Balloon/Balloon.js
new file mode 100644
index 000000000000..3c52492f7e67
--- /dev/null
+++ b/Source/Widgets/Balloon/Balloon.js
@@ -0,0 +1,136 @@
+/*global define*/
+define([
+ '../../Core/defineProperties',
+ '../../Core/DeveloperError',
+ '../../Core/destroyObject',
+ '../getElement',
+ './BalloonViewModel',
+ '../../ThirdParty/knockout'
+ ], function(
+ defineProperties,
+ DeveloperError,
+ destroyObject,
+ getElement,
+ BalloonViewModel,
+ knockout) {
+ "use strict";
+
+ /**
+ * A widget for displaying data in a balloon pointing to a picked object
+ *
+ * @alias Balloon
+ * @constructor
+ *
+ * @param {Element|String} container The DOM element or ID that will contain the widget.
+ * @param {Scene} scene The Scene instance to use.
+ *
+ * @exception {DeveloperError} container is required.
+ * @exception {DeveloperError} Element with id "container" does not exist in the document.
+ *
+ * @see Fullscreen
+ */
+ var Balloon = function(container, scene) {
+ if (typeof container === 'undefined') {
+ throw new DeveloperError('container is required.');
+ }
+
+ container = getElement(container);
+
+ this._container = container;
+ container.setAttribute('data-bind',
+ 'css: { "cesium-balloon-wrapper-visible" : showBalloon, "cesium-balloon-wrapper-hidden" : !showBalloon }');
+ var el = document.createElement('div');
+ this._element = el;
+ el.className = 'cesium-balloon-wrapper';
+ container.appendChild(el);
+ el.setAttribute('data-bind', 'style: { "bottom" : _positionY, "left" : _positionX}');
+
+ var contentWrapper = document.createElement('div');
+ contentWrapper.className = 'cesium-balloon-content';
+ contentWrapper.setAttribute('data-bind', 'style: {"max-width": _maxWidth, "max-height": _maxHeight}');
+ el.appendChild(contentWrapper);
+ var ex = document.createElement('a');
+ ex.href = '#';
+ ex.className = 'cesium-balloon-close';
+ ex.setAttribute('data-bind', 'click: function(){showBalloon = false; return false;}');
+ el.appendChild(ex);
+
+
+ this._content = document.createElement('div');
+ contentWrapper.appendChild(this._content);
+ this._content.setAttribute('data-bind', 'html: _contentHTML');
+ var pointContainer = document.createElement('div');
+ pointContainer.className = 'cesium-balloon-point-container';
+ pointContainer.setAttribute('data-bind',
+ 'css: { "cesium-balloon-point-container-downup" : _down || _up, "cesium-balloon-point-container-leftright" : _left || _right,\
+ "cesium-balloon-point-show" : showPoint, "cesium-balloon-point-hide" : !showPoint},\
+ style: { "bottom" : _pointY, "left" : _pointX}');
+ var point = document.createElement('div');
+ point.className = 'cesium-balloon-point';
+ point.setAttribute('data-bind',
+ 'css: { "cesium-balloon-point-down" : _down,\
+ "cesium-balloon-point-up" : _up,\
+ "cesium-balloon-point-left" : _left,\
+ "cesium-balloon-point-right" : _right}');
+ pointContainer.appendChild(point);
+ container.appendChild(pointContainer);
+
+ var viewModel = new BalloonViewModel(scene, this._element, this._container);
+ this._viewModel = viewModel;
+
+ this._pointContainer = pointContainer;
+ this._point = point;
+ this._contentWrapper = contentWrapper;
+
+ knockout.applyBindings(this._viewModel, this._container);
+ };
+
+ defineProperties(Balloon.prototype, {
+ /**
+ * Gets the parent container.
+ * @memberof Balloon.prototype
+ *
+ * @type {Element}
+ */
+ container : {
+ get : function() {
+ return this._container;
+ }
+ },
+
+ /**
+ * Gets the view model.
+ * @memberof Balloon.prototype
+ *
+ * @type {BalloonViewModel}
+ */
+ viewModel : {
+ get : function() {
+ return this._viewModel;
+ }
+ }
+ });
+
+ /**
+ * @memberof Balloon
+ * @returns {Boolean} true if the object has been destroyed, false otherwise.
+ */
+ Balloon.prototype.isDestroyed = function() {
+ return false;
+ };
+
+ /**
+ * Destroys the widget. Should be called if permanently
+ * removing the widget from layout.
+ * @memberof Balloon
+ */
+ Balloon.prototype.destroy = function() {
+ var container = this._container;
+ knockout.cleanNode(container);
+ container.removeChild(this._element);
+ container.removeChild(this._pointContainer);
+ return destroyObject(this);
+ };
+
+ return Balloon;
+});
\ No newline at end of file
diff --git a/Source/Widgets/Balloon/BalloonViewModel.js b/Source/Widgets/Balloon/BalloonViewModel.js
new file mode 100644
index 000000000000..5c7446f60611
--- /dev/null
+++ b/Source/Widgets/Balloon/BalloonViewModel.js
@@ -0,0 +1,412 @@
+/*global define*/
+define([
+ '../../Core/Cartesian2',
+ '../../Core/defaultValue',
+ '../../Core/defineProperties',
+ '../../Core/DeveloperError',
+ '../../Scene/SceneTransforms',
+ '../../ThirdParty/knockout'
+ ], function(
+ Cartesian2,
+ defaultValue,
+ defineProperties,
+ DeveloperError,
+ SceneTransforms,
+ knockout) {
+ "use strict";
+
+ var pointMin = 0;
+ var screenSpacePos = new Cartesian2();
+
+ function shiftPosition(viewModel, position, point, screen){
+ var pointX;
+ var pointY;
+ var posX;
+ var posY;
+ var container = viewModel._container;
+ var containerWidth = container.clientWidth;
+ var containerHeight = container.clientHeight;
+
+ viewModel._maxWidth = containerWidth*0.50 + 'px';
+ viewModel._maxHeight = containerHeight*0.50 + 'px';
+ var pointMaxY = containerHeight - 15;
+ var pointMaxX = containerWidth - 16;
+ var pointXOffset = position.x - 15;
+
+ var balloonElement = viewModel._balloonElement;
+ var width = balloonElement.offsetWidth;
+ var height = balloonElement.offsetHeight;
+
+ var posMaxY = containerHeight - height;
+ var posMaxX = containerWidth - width - 2;
+ var posMin = 0;
+ var posXOffset = position.x - width/2;
+
+ var top = position.y > containerHeight;
+ var bottom = position.y < -10;
+ var left = position.x < 0;
+ var right = position.x > containerWidth;
+
+ if (viewModel.showPoint) {
+ if (bottom) {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = 15;
+ pointX = Math.min(Math.max(pointXOffset, pointMin), pointMaxX - 15);
+ pointY = pointMin;
+ viewModel._down = true;
+ viewModel._up = false;
+ viewModel._left = false;
+ viewModel._right = false;
+ } else if (top) {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = containerHeight - height - 14;
+ pointX = Math.min(Math.max(pointXOffset, pointMin), pointMaxX - 15);
+ pointY = pointMaxY;
+ viewModel._down = false;
+ viewModel._up = true;
+ viewModel._left = false;
+ viewModel._right = false;
+ } else if (left) {
+ posX = 15;
+ posY = Math.min(Math.max((position.y - height/2), posMin), posMaxY);
+ pointX = pointMin;
+ pointY = Math.min(Math.max((position.y - 16), pointMin), pointMaxY - 15);
+ viewModel._down = false;
+ viewModel._up = false;
+ viewModel._left = true;
+ viewModel._right = false;
+ } else if (right) {
+ posX = containerWidth - width - 15;
+ posY = Math.min(Math.max((position.y - height/2), posMin), posMaxY);
+ pointX = pointMaxX;
+ pointY = Math.min(Math.max((position.y - 16), pointMin), pointMaxY - 15);
+ viewModel._down = false;
+ viewModel._up = false;
+ viewModel._left = false;
+ viewModel._right = true;
+ } else {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = Math.min(Math.max((position.y + 25), posMin), posMaxY);
+ pointX = pointXOffset;
+ pointY = Math.min(position.y + 10, posMaxY - 15);
+ viewModel._down = true;
+ viewModel._up = false;
+ viewModel._left = false;
+ viewModel._right = false;
+ }
+ } else {
+ if (bottom) {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = 0;
+ } else if (top) {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = containerHeight - height;
+ } else if (left) {
+ posX = 0;
+ posY = Math.min(Math.max((position.y - height/2), posMin), posMaxY);
+ } else if (right) {
+ posX = containerWidth - width;
+ posY = Math.min(Math.max((position.y - height/2), posMin), posMaxY);
+ } else {
+ posX = Math.min(Math.max(posXOffset, posMin), posMaxX);
+ posY = Math.min(Math.max(position.y, posMin), posMaxY);
+ }
+ }
+
+
+ viewModel._pointX = pointX + 'px';
+ viewModel._pointY = pointY + 'px';
+
+ viewModel._positionX = posX + 'px';
+ viewModel._positionY = posY + 'px';
+ }
+
+ /**
+ * The view model for {@link Balloon}.
+ * @alias BalloonViewModel
+ * @constructor
+ *
+ * @param {Scene} scene The scene instance to use.
+ * @param {Element} balloonElement The element containing all elements that make up the balloon.
+ * @param {Element} [container = document.body] The element containing the balloon.
+ *
+ * @exception {DeveloperError} scene is required.
+ * @exception {DeveloperError} balloonElement is required.
+ *
+ */
+ var BalloonViewModel = function(scene, balloonElement, container) {
+ if (typeof scene === 'undefined') {
+ throw new DeveloperError('scene is required.');
+ }
+
+ if (typeof balloonElement === 'undefined') {
+ throw new DeveloperError('balloonElement is required.');
+ }
+
+ this._scene = scene;
+ this._container = defaultValue(container, document.body);
+ this._balloonElement = balloonElement;
+ this._content = '';
+ this._position = undefined;
+ this._updateContent = false;
+ this._timerRunning = false;
+ this._defaultPosition = {x: this._container.clientWidth, y: this._container.clientHeight/2};
+ this._computeScreenSpacePosition = function(position, result) {
+ return SceneTransforms.wgs84ToWindowCoordinates(scene, position, result);
+ };
+ /**
+ * Stores the HTML content of the balloon as a string.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {String}
+ */
+ this._contentHTML = '';
+
+ /**
+ * The x screen position of the balloon.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Number}
+ */
+ this._positionX = '0';
+
+ /**
+ * The y screen position of the balloon.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Number}
+ */
+ this._positionY = '0';
+
+ /**
+ * The x screen position of the balloon point.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Number}
+ */
+ this._pointX = '0';
+
+ /**
+ * The y screen position of the balloon point
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this._pointY = '0';
+
+ /**
+ * Determines the visibility of the balloon
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this.showBalloon = false;
+
+ /**
+ * Determines the visibility of the balloon point
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this.showPoint = true;
+
+ /**
+ * True of the balloon point should be pointing down.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this._down = true;
+
+ /**
+ * True of the balloon point should be pointing up.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this._up = false;
+
+ /**
+ * True of the balloon point should be pointing left.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this._left = false;
+
+ /**
+ * True if the balloon point should be pointing right.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Boolean}
+ */
+ this._right = false;
+
+ /**
+ * The maximum width of the balloon element.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Number}
+ */
+ this._maxWidth = this._container.clientWidth*0.95 + 'px';
+
+ /**
+ * The maximum height of the balloon element.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Number}
+ */
+ this._maxHeight = this._container.clientHeight*0.50 + 'px';
+
+ knockout.track(this, ['showPoint', 'showBalloon', '_positionX', '_positionY', '_pointX', '_pointY',
+ '_down', '_up', '_left', '_right', '_maxWidth', '_maxHeight', '_contentHTML']);
+ };
+
+ /**
+ * Updates the view of the balloon to match the position and content properties of the view model
+ * @memberof BalloonViewModel
+ */
+ BalloonViewModel.prototype.update = function() {
+ if (!this._timerRunning) {
+ if (this._updateContent) {
+ this.showBalloon = false;
+ this._timerRunning = true;
+ var that = this;
+ //timeout needed so that re-positioning occurs after showBalloon=false transition is complete
+ setTimeout(function () {
+ that._contentHTML = that._content;
+ if (typeof that._position !== 'undefined') {
+ var pos = that._computeScreenSpacePosition(that._position, screenSpacePos);
+ pos = shiftPosition(that, pos);
+ }
+ that.showBalloon = true;
+ that._timerRunning = false;
+ }, 100);
+ this._updateContent = false;
+ } else if (this.showBalloon) {
+ var pos;
+ if (typeof this._position !== 'undefined'){
+ pos = this._computeScreenSpacePosition(this._position, screenSpacePos);
+ this.showPoint = true;
+ } else {
+ pos = this._defaultPosition;
+ this.showPoint = false;
+ }
+
+ pos = shiftPosition(this, pos);
+ }
+ }
+ };
+
+ defineProperties(BalloonViewModel.prototype, {
+ /**
+ * Gets or sets the HTML element containing the balloon
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Element}
+ */
+ container : {
+ get : function() {
+ return this._container;
+ },
+ set : function(value) {
+ if (!(value instanceof Element)) {
+ throw new DeveloperError('value must be a valid Element.');
+ }
+ this._container = value;
+ }
+ },
+ /**
+ * Gets or sets the HTML element that makes up the balloon
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Element}
+ */
+ balloonElement : {
+ get : function() {
+ return this._balloonElement;
+ },
+ set : function(value) {
+ if (!(value instanceof Element)) {
+ throw new DeveloperError('value must be a valid Element.');
+ }
+ this._balloonElement = value;
+ }
+ },
+ /**
+ * Gets or sets the content of the balloon
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Element}
+ */
+ content: {
+ set : function(value) {
+ if (typeof value === 'undefined') {
+ this._content = '';
+ } else {
+ this._content = value;
+ }
+ this._updateContent = true;
+ }
+ },
+ /**
+ * Gets the scene to control.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Scene}
+ */
+ scene : {
+ get : function() {
+ return this._scene;
+ }
+ },
+ /**
+ * Sets the default position of the balloon.
+ * @memberof BalloonViewModel.prototype
+ *
+ * @type {Cartesain2}
+ */
+ defaultPosition : {
+ set : function(value) {
+ if (typeof value !== 'undefined') {
+ this._defaultPosition.x = value.x;
+ this._defaultPosition.y = value.y;
+ }
+ }
+ },
+ /**
+ * Sets the function for converting the world position of the object to the screen space position.
+ * Expects the {Cartesian3} parameter for the position and the optional {Cartesian2} parameter for the result.
+ * Should return a {Cartesian2}.
+ *
+ * Defaults to SceneTransforms.wgs84ToWindowCoordinates
+ *
+ * @example
+ * balloonViewModel.computeScreenSpacePosition = function(position, result) {
+ * return Cartesian2.clone(position, result);
+ * };
+ *
+ * @memberof BalloonViewModel
+ *
+ * @type {Function}
+ */
+ computeScreenSpacePosition: {
+ set: function(value) {
+ this._computeScreenSpacePosition = value;
+ }
+ },
+ /**
+ * Sets the world position of the object for which to display the balloon.
+ * @memberof BalloonViewModel
+ *
+ * @type {Cartesian3}
+ */
+ position: {
+ set: function(value) {
+ this._position = value;
+ }
+ }
+ });
+
+ return BalloonViewModel;
+});
diff --git a/Source/Widgets/Viewer/Viewer.css b/Source/Widgets/Viewer/Viewer.css
index f9d696755c2a..b92e9318e060 100644
--- a/Source/Widgets/Viewer/Viewer.css
+++ b/Source/Widgets/Viewer/Viewer.css
@@ -1,10 +1,12 @@
@import url(../Animation/Animation.css);
@import url(../BaseLayerPicker/BaseLayerPicker.css);
+@import url(../Balloon/Balloon.css);
@import url(../CesiumWidget/CesiumWidget.css);
@import url(../FullscreenButton/FullscreenButton.css);
@import url(../HomeButton/HomeButton.css);
@import url(../SceneModePicker/SceneModePicker.css);
@import url(../Timeline/Timeline.css);
+@import url(../../DynamicScene/GeoJsonDataSource.css);
.cesium-viewer {
font-family: sans-serif;
@@ -18,6 +20,11 @@
height: 100%;
}
+.cesium-viewer-balloonContainer {
+ width: 100%;
+ height: 100%;
+}
+
.cesium-viewer-cesiumWidgetContainer {
width: 100%;
height: 100%;
diff --git a/Source/Widgets/Viewer/viewerDragDropMixin.js b/Source/Widgets/Viewer/viewerDragDropMixin.js
index 5f49c6cf9b14..52c6a180e758 100644
--- a/Source/Widgets/Viewer/viewerDragDropMixin.js
+++ b/Source/Widgets/Viewer/viewerDragDropMixin.js
@@ -151,6 +151,14 @@ define([
if (clearOnDrop) {
viewer.dataSources.removeAll();
+
+ if (viewer.hasOwnProperty('balloonedObject')) {
+ viewer.balloonedObject = undefined;
+ }
+
+ if (viewer.hasOwnProperty('trackedObject')) {
+ viewer.trackedObject = undefined;
+ }
}
var files = event.dataTransfer.files;
diff --git a/Source/Widgets/Viewer/viewerDynamicObjectMixin.js b/Source/Widgets/Viewer/viewerDynamicObjectMixin.js
index 2fa154491625..96ee13740e90 100644
--- a/Source/Widgets/Viewer/viewerDynamicObjectMixin.js
+++ b/Source/Widgets/Viewer/viewerDynamicObjectMixin.js
@@ -1,5 +1,6 @@
/*global define*/
define([
+ '../../Core/Cartesian2',
'../../Core/defaultValue',
'../../Core/defined',
'../../Core/DeveloperError',
@@ -8,8 +9,10 @@ define([
'../../Core/ScreenSpaceEventType',
'../../Core/wrapFunction',
'../../Scene/SceneMode',
+ '../Balloon/Balloon',
'../../DynamicScene/DynamicObjectView'
], function(
+ Cartesian2,
defaultValue,
defined,
DeveloperError,
@@ -18,6 +21,7 @@ define([
ScreenSpaceEventType,
wrapFunction,
SceneMode,
+ Balloon,
DynamicObjectView) {
"use strict";
@@ -33,6 +37,7 @@ define([
*
* @exception {DeveloperError} viewer is required.
* @exception {DeveloperError} trackedObject is already defined by another mixin.
+ * @exception {DeveloperError} balloonedObject is already defined by another mixin.
*
* @example
* // Add support for working with DynamicObject instances to the Viewer.
@@ -40,7 +45,9 @@ define([
* var viewer = new Cesium.Viewer('cesiumContainer');
* viewer.extend(Cesium.viewerDynamicObjectMixin);
* viewer.trackedObject = dynamicObject; //Camera will now track dynamicObject
+ * viewer.balloonedObject = object; //Balloon will now appear over object
*/
+
var viewerDynamicObjectMixin = function(viewer) {
if (!defined(viewer)) {
throw new DeveloperError('viewer is required.');
@@ -48,18 +55,35 @@ define([
if (viewer.hasOwnProperty('trackedObject')) {
throw new DeveloperError('trackedObject is already defined by another mixin.');
}
+ if (viewer.hasOwnProperty('balloonedObject')) {
+ throw new DeveloperError('balloonedObject is already defined by another mixin.');
+ }
+
+ //Balloon
+ var balloonContainer = document.createElement('div');
+ balloonContainer.className = 'cesium-viewer-balloonContainer';
+ viewer._viewerContainer.appendChild(balloonContainer);
+
+ var balloon = new Balloon(balloonContainer, viewer.scene);
+ var balloonViewModel = balloon.viewModel;
+ viewer._balloon = balloon;
var eventHelper = new EventHelper();
var trackedObject;
var dynamicObjectView;
+ var balloonedObject;
//Subscribe to onTick so that we can update the view each update.
- function updateView(clock) {
+ function onTick(clock) {
if (defined(dynamicObjectView)) {
dynamicObjectView.update(clock.currentTime);
}
+ if (typeof balloonedObject !== 'undefined' && typeof balloonedObject.position !== 'undefined') {
+ balloonViewModel.position = balloonedObject.position.getValueCartesian(clock.currentTime);
+ balloonViewModel.update();
+ }
}
- eventHelper.add(viewer.clock.onTick, updateView);
+ eventHelper.add(viewer.clock.onTick, onTick);
function pickAndTrackObject(e) {
var pickedPrimitive = viewer.scene.pick(e.position);
@@ -70,18 +94,27 @@ define([
}
}
- function clearTrackedObject() {
+ function pickAndShowBalloon(e) {
+ var pickedPrimitive = viewer.scene.pick(e.position);
+ if (typeof pickedPrimitive !== 'undefined' && typeof pickedPrimitive.dynamicObject !== 'undefined') {
+ viewer.balloonedObject = pickedPrimitive.dynamicObject;
+ }
+ }
+
+ function onHomeButtonClicked() {
viewer.trackedObject = undefined;
+ viewer.balloonedObject = undefined;
}
- //Subscribe to the home button click if it exists, so that we can
- //clear the trackedObject when it is clicked.
+ //Subscribe to the home button beforeExecute event if it exists,
+ // so that we can clear the trackedObject and balloon.
if (defined(viewer.homeButton)) {
- eventHelper.add(viewer.homeButton.viewModel.command.beforeExecute, clearTrackedObject);
+ eventHelper.add(viewer.homeButton.viewModel.command.beforeExecute, onHomeButtonClicked);
}
//Subscribe to left clicks and zoom to the picked object.
- viewer.screenSpaceEventHandler.setInputAction(pickAndTrackObject, ScreenSpaceEventType.LEFT_CLICK);
+ viewer.screenSpaceEventHandler.setInputAction(pickAndShowBalloon, ScreenSpaceEventType.LEFT_CLICK);
+ viewer.screenSpaceEventHandler.setInputAction(pickAndTrackObject, ScreenSpaceEventType.RIGHT_CLICK);
defineProperties(viewer, {
/**
@@ -97,9 +130,11 @@ define([
if (trackedObject !== value) {
trackedObject = value;
dynamicObjectView = defined(value) ? new DynamicObjectView(value, viewer.scene, viewer.centralBody.getEllipsoid()) : undefined;
+ //Hide the balloon if it's not the object we are following.
+ balloonViewModel.showBalloon = balloonedObject === trackedObject;
}
- var sceneMode = viewer.scene.getFrameState().mode;
+ var sceneMode = viewer.scene.getFrameState().mode;
if (sceneMode === SceneMode.COLUMBUS_VIEW || sceneMode === SceneMode.SCENE2D) {
viewer.scene.getScreenSpaceCameraController().enableTranslate = !defined(value);
}
@@ -108,14 +143,47 @@ define([
viewer.scene.getScreenSpaceCameraController().enableTilt = !defined(value);
}
}
+ },
+
+ /**
+ * Gets or sets the object instance for which to display a balloon
+ * @memberof viewerDynamicObjectMixin.prototype
+ * @type {DynamicObject}
+ */
+ balloonedObject : {
+ get : function() {
+ return balloonedObject;
+ },
+ set : function(value) {
+ var content;
+ var position;
+ if (typeof value !== 'undefined') {
+ if (typeof value.position !== 'undefined') {
+ position = value.position.getValueCartesian(viewer.clock.currentTime);
+ }
+
+ if (typeof value.balloon !== 'undefined') {
+ content = value.balloon.getValue(viewer.clock.currentTime);
+ }
+
+ if (typeof content === 'undefined') {
+ content = value.id;
+ }
+ balloonViewModel.content = content;
+ }
+ balloonedObject = value;
+ balloonViewModel.position = position;
+ balloonViewModel.showBalloon = typeof content !== 'undefined';
+ }
}
});
//Wrap destroy to clean up event subscriptions.
viewer.destroy = wrapFunction(viewer, viewer.destroy, function() {
eventHelper.removeAll();
-
+ balloon.destroy();
viewer.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
+ viewer.screenSpaceEventHandler.removeInputAction(ScreenSpaceEventType.RIGHT_CLICK);
});
};
diff --git a/Source/Widgets/widgets.css b/Source/Widgets/widgets.css
index c03114a1676a..b511ae10cba2 100644
--- a/Source/Widgets/widgets.css
+++ b/Source/Widgets/widgets.css
@@ -1,4 +1,5 @@
@import url(./Animation/Animation.css);
+@import url(./Balloon/Balloon.css);
@import url(./BaseLayerPicker/BaseLayerPicker.css);
@import url(./CesiumWidget/CesiumWidget.css);
@import url(./checkForChromeFrame.css);
@@ -6,4 +7,5 @@
@import url(./HomeButton/HomeButton.css);
@import url(./SceneModePicker/SceneModePicker.css);
@import url(./Timeline/Timeline.css);
-@import url(./Viewer/Viewer.css);
\ No newline at end of file
+@import url(./Viewer/Viewer.css);
+@import url(../DynamicScene/GeoJsonDataSource.css);
diff --git a/Specs/Widgets/Balloon/BalloonSpec.js b/Specs/Widgets/Balloon/BalloonSpec.js
new file mode 100644
index 000000000000..0245b1241fee
--- /dev/null
+++ b/Specs/Widgets/Balloon/BalloonSpec.js
@@ -0,0 +1,56 @@
+/*global defineSuite*/
+defineSuite([
+ 'Widgets/Balloon/Balloon',
+ 'Core/Ellipsoid',
+ 'Scene/SceneTransitioner',
+ 'Specs/createScene',
+ 'Specs/destroyScene'
+ ], function(
+ Balloon,
+ Ellipsoid,
+ SceneTransitioner,
+ createScene,
+ destroyScene) {
+ "use strict";
+ /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/
+
+ var scene;
+ beforeAll(function() {
+ scene = createScene();
+ });
+
+ afterAll(function() {
+ destroyScene(scene);
+ });
+
+ it('constructor sets expected values', function() {
+ var balloon = new Balloon(document.body, scene);
+ expect(balloon.container).toBe(document.body);
+ expect(balloon.viewModel.scene).toBe(scene);
+ expect(balloon.isDestroyed()).toEqual(false);
+ balloon.destroy();
+ expect(balloon.isDestroyed()).toEqual(true);
+ });
+
+ it('constructor works with string id container', function() {
+ var testElement = document.createElement('span');
+ testElement.id = 'testElement';
+ document.body.appendChild(testElement);
+ var balloon = new Balloon('testElement', scene);
+ expect(balloon.container).toBe(testElement);
+ document.body.removeChild(testElement);
+ balloon.destroy();
+ });
+
+ it('throws if container is undefined', function() {
+ expect(function() {
+ return new Balloon(undefined, scene);
+ }).toThrow();
+ });
+
+ it('throws if container string is undefined', function() {
+ expect(function() {
+ return new Balloon('testElement', scene);
+ }).toThrow();
+ });
+});
\ No newline at end of file
diff --git a/Specs/Widgets/Balloon/BalloonViewModelSpec.js b/Specs/Widgets/Balloon/BalloonViewModelSpec.js
new file mode 100644
index 000000000000..0029837603d2
--- /dev/null
+++ b/Specs/Widgets/Balloon/BalloonViewModelSpec.js
@@ -0,0 +1,46 @@
+/*global defineSuite*/
+defineSuite([
+ 'Widgets/Balloon/BalloonViewModel',
+ 'Core/Ellipsoid',
+ 'Scene/SceneTransitioner',
+ 'Specs/createScene',
+ 'Specs/destroyScene'
+ ], function(
+ BalloonViewModel,
+ Ellipsoid,
+ SceneTransitioner,
+ createScene,
+ destroyScene) {
+ "use strict";
+ /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/
+
+ var scene;
+ var balloonElement = document.createElement('div');
+ var container = document.createElement('div');
+ beforeAll(function() {
+ scene = createScene();
+ });
+
+ afterAll(function() {
+ destroyScene(scene);
+ });
+
+ it('constructor sets expected values', function() {
+ var viewModel = new BalloonViewModel(scene, balloonElement, container);
+ expect(viewModel.scene).toBe(scene);
+ expect(viewModel.balloonElement).toBe(balloonElement);
+ expect(viewModel.container).toBe(container);
+ });
+
+ it('throws if scene is undefined', function() {
+ expect(function() {
+ return new BalloonViewModel(undefined);
+ }).toThrow();
+ });
+
+ it('throws if balloonElement is undefined', function() {
+ expect(function() {
+ return new BalloonViewModel(scene);
+ }).toThrow();
+ });
+});
\ No newline at end of file
diff --git a/Specs/Widgets/Viewer/viewerDynamicObjectMixinSpec.js b/Specs/Widgets/Viewer/viewerDynamicObjectMixinSpec.js
index eeabad1170c3..f0109a6f7692 100644
--- a/Specs/Widgets/Viewer/viewerDynamicObjectMixinSpec.js
+++ b/Specs/Widgets/Viewer/viewerDynamicObjectMixinSpec.js
@@ -33,10 +33,11 @@ defineSuite([
document.body.removeChild(container);
});
- it('adds trackedObject property', function() {
+ it('adds properties', function() {
viewer = new Viewer(container);
viewer.extend(viewerDynamicObjectMixin);
expect(viewer.hasOwnProperty('trackedObject')).toEqual(true);
+ expect(viewer.hasOwnProperty('balloonedObject')).toEqual(true);
});
it('can get and set trackedObject', function() {
@@ -53,6 +54,23 @@ defineSuite([
expect(viewer.trackedObject).toBeUndefined();
});
+ it('can get and set balloonedObject', function() {
+ var viewer = new Viewer(container);
+ viewer.extend(viewerDynamicObjectMixin);
+
+ var dynamicObject = new DynamicObject();
+ dynamicObject.position = new MockProperty(new Cartesian3(123456, 123456, 123456));
+ dynamicObject.balloon = new MockProperty('content');
+
+ viewer.balloonedObject = dynamicObject;
+ expect(viewer.balloonedObject).toBe(dynamicObject);
+
+ viewer.balloonedObject = undefined;
+ expect(viewer.balloonedObject).toBeUndefined();
+
+ viewer.destroy();
+ });
+
it('home button resets tracked object', function() {
viewer = new Viewer(container);
viewer.extend(viewerDynamicObjectMixin);
@@ -78,11 +96,20 @@ defineSuite([
}).toThrow();
});
- it('throws if dropTarget property already added by another mixin.', function() {
+ it('throws if trackedObject property already added by another mixin.', function() {
viewer = new Viewer(container);
viewer.trackedObject = true;
expect(function() {
viewer.extend(viewerDynamicObjectMixin);
}).toThrow();
});
+
+ it('throws if balloonedObject property already added by another mixin.', function() {
+ var viewer = new Viewer(container);
+ viewer.balloonedObject = true;
+ expect(function() {
+ viewer.extend(viewerDynamicObjectMixin);
+ }).toThrow();
+ viewer.destroy();
+ });
});
\ No newline at end of file