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 += ''; + } + } + html += '
' + key + '' + properties[key] + '
'; + } + 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