Source: Scene/Label.js

/*global define*/
define([
        '../Core/BoundingRectangle',
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Color',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/DeveloperError',
        '../Core/DistanceDisplayCondition',
        '../Core/NearFarScalar',
        './Billboard',
        './HeightReference',
        './HorizontalOrigin',
        './LabelStyle',
        './VerticalOrigin'
    ], function(
        BoundingRectangle,
        Cartesian2,
        Cartesian3,
        Color,
        defaultValue,
        defined,
        defineProperties,
        DeveloperError,
        DistanceDisplayCondition,
        NearFarScalar,
        Billboard,
        HeightReference,
        HorizontalOrigin,
        LabelStyle,
        VerticalOrigin) {
    'use strict';

    function rebindAllGlyphs(label) {
        if (!label._rebindAllGlyphs && !label._repositionAllGlyphs) {
            // only push label if it's not already been marked dirty
            label._labelCollection._labelsToUpdate.push(label);
        }
        label._rebindAllGlyphs = true;
    }

    function repositionAllGlyphs(label) {
        if (!label._rebindAllGlyphs && !label._repositionAllGlyphs) {
            // only push label if it's not already been marked dirty
            label._labelCollection._labelsToUpdate.push(label);
        }
        label._repositionAllGlyphs = true;
    }

    /**
     * A Label draws viewport-aligned text positioned in the 3D scene.  This constructor
     * should not be used directly, instead create labels by calling {@link LabelCollection#add}.
     *
     * @alias Label
     * @internalConstructor
     *
     * @exception {DeveloperError} translucencyByDistance.far must be greater than translucencyByDistance.near
     * @exception {DeveloperError} pixelOffsetScaleByDistance.far must be greater than pixelOffsetScaleByDistance.near
     * @exception {DeveloperError} distanceDisplayCondition.far must be greater than distanceDisplayCondition.near
     *
     * @see LabelCollection
     * @see LabelCollection#add
     *
     * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Labels.html|Cesium Sandcastle Labels Demo}
     */
    function Label(options, labelCollection) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        //>>includeStart('debug', pragmas.debug);
        if (defined(options.translucencyByDistance) && options.translucencyByDistance.far <= options.translucencyByDistance.near) {
            throw new DeveloperError('translucencyByDistance.far must be greater than translucencyByDistance.near.');
        }
        if (defined(options.pixelOffsetScaleByDistance) && options.pixelOffsetScaleByDistance.far <= options.pixelOffsetScaleByDistance.near) {
            throw new DeveloperError('pixelOffsetScaleByDistance.far must be greater than pixelOffsetScaleByDistance.near.');
        }
        if (defined(options.distanceDisplayCondition) && options.distanceDisplayCondition.far <= options.distanceDisplayCondition.near) {
            throw new DeveloperError('distanceDisplayCondition.far must be greater than distanceDisplayCondition.near');
        }
        //>>includeEnd('debug');

        this._text = defaultValue(options.text, '');
        this._show = defaultValue(options.show, true);
        this._font = defaultValue(options.font, '30px sans-serif');
        this._fillColor = Color.clone(defaultValue(options.fillColor, Color.WHITE));
        this._outlineColor = Color.clone(defaultValue(options.outlineColor, Color.BLACK));
        this._outlineWidth = defaultValue(options.outlineWidth, 1.0);
        this._style = defaultValue(options.style, LabelStyle.FILL);
        this._verticalOrigin = defaultValue(options.verticalOrigin, VerticalOrigin.BOTTOM);
        this._horizontalOrigin = defaultValue(options.horizontalOrigin, HorizontalOrigin.LEFT);
        this._pixelOffset = Cartesian2.clone(defaultValue(options.pixelOffset, Cartesian2.ZERO));
        this._eyeOffset = Cartesian3.clone(defaultValue(options.eyeOffset, Cartesian3.ZERO));
        this._position = Cartesian3.clone(defaultValue(options.position, Cartesian3.ZERO));
        this._scale = defaultValue(options.scale, 1.0);
        this._id = options.id;
        this._translucencyByDistance = options.translucencyByDistance;
        this._pixelOffsetScaleByDistance = options.pixelOffsetScaleByDistance;
        this._heightReference = defaultValue(options.heightReference, HeightReference.NONE);
        this._distanceDisplayCondition = options.distanceDisplayCondition;

        this._labelCollection = labelCollection;
        this._glyphs = [];

        this._rebindAllGlyphs = true;
        this._repositionAllGlyphs = true;

        this._actualClampedPosition = undefined;
        this._removeCallbackFunc = undefined;
        this._mode = undefined;

        this._clusterShow = true;

        this._updateClamping();
    }

    defineProperties(Label.prototype, {
        /**
         * Determines if this label will be shown.  Use this to hide or show a label, instead
         * of removing it and re-adding it to the collection.
         * @memberof Label.prototype
         * @type {Boolean}
         */
        show : {
            get : function() {
                return this._show;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._show !== value) {
                    this._show = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var billboard = glyphs[i].billboard;
                        if (defined(billboard)) {
                            billboard.show = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets or sets the Cartesian position of this label.
         * @memberof Label.prototype
         * @type {Cartesian3}
         */
        position : {
            get : function() {
                return this._position;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                var position = this._position;
                if (!Cartesian3.equals(position, value)) {
                    Cartesian3.clone(value, position);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var billboard = glyphs[i].billboard;
                        if (defined(billboard)) {
                            billboard.position = value;
                        }
                    }

                    if (this._heightReference !== HeightReference.NONE) {
                        this._updateClamping();
                    }
                }
            }
        },

        /**
         * Gets or sets the height reference of this billboard.
         * @memberof Label.prototype
         * @type {HeightReference}
         */
        heightReference : {
            get : function() {
                return this._heightReference;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (value !== this._heightReference) {
                    this._heightReference = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var billboard = glyphs[i].billboard;
                        if (defined(billboard)) {
                            billboard.heightReference = value;
                        }
                    }

                    repositionAllGlyphs(this);

                    this._updateClamping();
                }
            }
        },

        /**
         * Gets or sets the text of this label.
         * @memberof Label.prototype
         * @type {String}
         */
        text : {
            get : function() {
                return this._text;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._text !== value) {
                    this._text = value;
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the font used to draw this label. Fonts are specified using the same syntax as the CSS 'font' property.
         * @memberof Label.prototype
         * @type {String}
         * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#text-styles|HTML canvas 2D context text styles}
         */
        font : {
            get : function() {
                return this._font;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._font !== value) {
                    this._font = value;
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the fill color of this label.
         * @memberof Label.prototype
         * @type {Color}
         * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
         */
        fillColor : {
            get : function() {
                return this._fillColor;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                var fillColor = this._fillColor;
                if (!Color.equals(fillColor, value)) {
                    Color.clone(value, fillColor);
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the outline color of this label.
         * @memberof Label.prototype
         * @type {Color}
         * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
         */
        outlineColor : {
            get : function() {
                return this._outlineColor;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                var outlineColor = this._outlineColor;
                if (!Color.equals(outlineColor, value)) {
                    Color.clone(value, outlineColor);
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the outline width of this label.
         * @memberof Label.prototype
         * @type {Number}
         * @see {@link http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#fill-and-stroke-styles|HTML canvas 2D context fill and stroke styles}
         */
        outlineWidth : {
            get : function() {
                return this._outlineWidth;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._outlineWidth !== value) {
                    this._outlineWidth = value;
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the style of this label.
         * @memberof Label.prototype
         * @type {LabelStyle}
         */
        style : {
            get : function() {
                return this._style;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._style !== value) {
                    this._style = value;
                    rebindAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the pixel offset in screen space from the origin of this label.  This is commonly used
         * to align multiple labels and billboards at the same position, e.g., an image and text.  The
         * screen space origin is the top, left corner of the canvas; <code>x</code> increases from
         * left to right, and <code>y</code> increases from top to bottom.
         * <br /><br />
         * <div align='center'>
         * <table border='0' cellpadding='5'><tr>
         * <td align='center'><code>default</code><br/><img src='images/Label.setPixelOffset.default.png' width='250' height='188' /></td>
         * <td align='center'><code>l.pixeloffset = new Cartesian2(25, 75);</code><br/><img src='images/Label.setPixelOffset.x50y-25.png' width='250' height='188' /></td>
         * </tr></table>
         * The label's origin is indicated by the yellow point.
         * </div>
         * @memberof Label.prototype
         * @type {Cartesian2}
         */
        pixelOffset : {
            get : function() {
                return this._pixelOffset;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                var pixelOffset = this._pixelOffset;
                if (!Cartesian2.equals(pixelOffset, value)) {
                    Cartesian2.clone(value, pixelOffset);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.pixelOffset = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets or sets near and far translucency properties of a Label based on the Label's distance from the camera.
         * A label's translucency will interpolate between the {@link NearFarScalar#nearValue} and
         * {@link NearFarScalar#farValue} while the camera distance falls within the upper and lower bounds
         * of the specified {@link NearFarScalar#near} and {@link NearFarScalar#far}.
         * Outside of these ranges the label's translucency remains clamped to the nearest bound.  If undefined,
         * translucencyByDistance will be disabled.
         * @memberof Label.prototype
         * @type {NearFarScalar}
         *
         * @example
         * // Example 1.
         * // Set a label's translucencyByDistance to 1.0 when the
         * // camera is 1500 meters from the label and disappear as
         * // the camera distance approaches 8.0e6 meters.
         * text.translucencyByDistance = new Cesium.NearFarScalar(1.5e2, 1.0, 8.0e6, 0.0);
         *
         * @example
         * // Example 2.
         * // disable translucency by distance
         * text.translucencyByDistance = undefined;
         */
        translucencyByDistance : {
            get : function() {
                return this._translucencyByDistance;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (defined(value) && value.far <= value.near) {
                    throw new DeveloperError('far distance must be greater than near distance.');
                }
                //>>includeEnd('debug');

                var translucencyByDistance = this._translucencyByDistance;
                if (!NearFarScalar.equals(translucencyByDistance, value)) {
                    this._translucencyByDistance = NearFarScalar.clone(value, translucencyByDistance);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.translucencyByDistance = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets or sets near and far pixel offset scaling properties of a Label based on the Label's distance from the camera.
         * A label's pixel offset will be scaled between the {@link NearFarScalar#nearValue} and
         * {@link NearFarScalar#farValue} while the camera distance falls within the upper and lower bounds
         * of the specified {@link NearFarScalar#near} and {@link NearFarScalar#far}.
         * Outside of these ranges the label's pixel offset scaling remains clamped to the nearest bound.  If undefined,
         * pixelOffsetScaleByDistance will be disabled.
         * @memberof Label.prototype
         * @type {NearFarScalar}
         *
         * @example
         * // Example 1.
         * // Set a label's pixel offset scale to 0.0 when the
         * // camera is 1500 meters from the label and scale pixel offset to 10.0 pixels
         * // in the y direction the camera distance approaches 8.0e6 meters.
         * text.pixelOffset = new Cesium.Cartesian2(0.0, 1.0);
         * text.pixelOffsetScaleByDistance = new Cesium.NearFarScalar(1.5e2, 0.0, 8.0e6, 10.0);
         *
         * @example
         * // Example 2.
         * // disable pixel offset by distance
         * text.pixelOffsetScaleByDistance = undefined;
         */
        pixelOffsetScaleByDistance : {
            get : function() {
                return this._pixelOffsetScaleByDistance;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (defined(value) && value.far <= value.near) {
                    throw new DeveloperError('far distance must be greater than near distance.');
                }
                //>>includeEnd('debug');

                var pixelOffsetScaleByDistance = this._pixelOffsetScaleByDistance;
                if (!NearFarScalar.equals(pixelOffsetScaleByDistance, value)) {
                    this._pixelOffsetScaleByDistance = NearFarScalar.clone(value, pixelOffsetScaleByDistance);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.pixelOffsetScaleByDistance = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets and sets the 3D Cartesian offset applied to this label in eye coordinates.  Eye coordinates is a left-handed
         * coordinate system, where <code>x</code> points towards the viewer's right, <code>y</code> points up, and
         * <code>z</code> points into the screen.  Eye coordinates use the same scale as world and model coordinates,
         * which is typically meters.
         * <br /><br />
         * An eye offset is commonly used to arrange multiple label or objects at the same position, e.g., to
         * arrange a label above its corresponding 3D model.
         * <br /><br />
         * Below, the label is positioned at the center of the Earth but an eye offset makes it always
         * appear on top of the Earth regardless of the viewer's or Earth's orientation.
         * <br /><br />
         * <div align='center'>
         * <table border='0' cellpadding='5'><tr>
         * <td align='center'><img src='images/Billboard.setEyeOffset.one.png' width='250' height='188' /></td>
         * <td align='center'><img src='images/Billboard.setEyeOffset.two.png' width='250' height='188' /></td>
         * </tr></table>
         * <code>l.eyeOffset = new Cartesian3(0.0, 8000000.0, 0.0);</code><br /><br />
         * </div>
         * @memberof Label.prototype
         * @type {Cartesian3}
         */
        eyeOffset : {
            get : function() {
                return this._eyeOffset;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                var eyeOffset = this._eyeOffset;
                if (!Cartesian3.equals(eyeOffset, value)) {
                    Cartesian3.clone(value, eyeOffset);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.eyeOffset = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets or sets the horizontal origin of this label, which determines if the label is drawn
         * to the left, center, or right of its position.
         * <br /><br />
         * <div align='center'>
         * <img src='images/Billboard.setHorizontalOrigin.png' width='400' height='300' /><br />
         * </div>
         * @memberof Label.prototype
         * @type {HorizontalOrigin}
         * @example
         * // Use a top, right origin
         * l.horizontalOrigin = Cesium.HorizontalOrigin.RIGHT;
         * l.verticalOrigin = Cesium.VerticalOrigin.TOP;
         */
        horizontalOrigin : {
            get : function() {
                return this._horizontalOrigin;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._horizontalOrigin !== value) {
                    this._horizontalOrigin = value;
                    repositionAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the vertical origin of this label, which determines if the label is
         * to the above, below, or at the center of its position.
         * <br /><br />
         * <div align='center'>
         * <img src='images/Billboard.setVerticalOrigin.png' width='400' height='300' /><br />
         * </div>
         * @memberof Label.prototype
         * @type {VerticalOrigin}
         * @example
         * // Use a top, right origin
         * l.horizontalOrigin = Cesium.HorizontalOrigin.RIGHT;
         * l.verticalOrigin = Cesium.VerticalOrigin.TOP;
         */
        verticalOrigin : {
            get : function() {
                return this._verticalOrigin;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._verticalOrigin !== value) {
                    this._verticalOrigin = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.verticalOrigin = value;
                        }
                    }

                    repositionAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the uniform scale that is multiplied with the label's size in pixels.
         * A scale of <code>1.0</code> does not change the size of the label; a scale greater than
         * <code>1.0</code> enlarges the label; a positive scale less than <code>1.0</code> shrinks
         * the label.
         * <br /><br />
         * Applying a large scale value may pixelate the label.  To make text larger without pixelation,
         * use a larger font size when calling {@link Label#font} instead.
         * <br /><br />
         * <div align='center'>
         * <img src='images/Label.setScale.png' width='400' height='300' /><br/>
         * From left to right in the above image, the scales are <code>0.5</code>, <code>1.0</code>,
         * and <code>2.0</code>.
         * </div>
         * @memberof Label.prototype
         * @type {Number}
         */
        scale : {
            get : function() {
                return this._scale;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                if (this._scale !== value) {
                    this._scale = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.scale = value;
                        }
                    }

                    repositionAllGlyphs(this);
                }
            }
        },

        /**
         * Gets or sets the condition specifying at what distance from the camera that this label will be displayed.
         * @memberof Label.prototype
         * @type {DistanceDisplayCondition}
         * @default undefined
         */
        distanceDisplayCondition : {
            get : function() {
                return this._distanceDisplayCondition;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (defined(value) && value.far <= value.near) {
                    throw new DeveloperError('far must be greater than near');
                }
                //>>includeEnd('debug');
                if (!DistanceDisplayCondition.equals(value, this._distanceDisplayCondition)) {
                    this._distanceDisplayCondition = DistanceDisplayCondition.clone(value, this._distanceDisplayCondition);

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.distanceDisplayCondition = value;
                        }
                    }
                }
            }
        },

        /**
         * Gets or sets the user-defined object returned when the label is picked.
         * @memberof Label.prototype
         * @type {Object}
         */
        id : {
            get : function() {
                return this._id;
            },
            set : function(value) {
                if (this._id !== value) {
                    this._id = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            glyph.billboard.id = value;
                        }
                    }
                }
            }
        },

        /**
         * Keeps track of the position of the label based on the height reference.
         * @memberof Label.prototype
         * @type {Cartesian3}
         * @private
         */
        _clampedPosition : {
            get : function() {
                return this._actualClampedPosition;
            },
            set : function(value) {
                this._actualClampedPosition = Cartesian3.clone(value, this._actualClampedPosition);

                var glyphs = this._glyphs;
                for (var i = 0, len = glyphs.length; i < len; i++) {
                    var glyph = glyphs[i];
                    if (defined(glyph.billboard)) {
                        // Set all the private values here, because we already clamped to ground
                        //  so we don't want to do it again for every glyph

                        glyph.billboard._clampedPosition = value;

                        value = defaultValue(value, this._position);
                        Cartesian3.clone(value, glyph.billboard._position);
                        Cartesian3.clone(value, glyph.billboard._actualPosition);
                    }
                }
            }
        },

        /**
         * Determines whether or not this label will be shown or hidden because it was clustered.
         * @memberof Label.prototype
         * @type {Boolean}
         * @private
         */
        clusterShow : {
            get : function() {
                return this._clusterShow;
            },
            set : function(value) {
                if (this._clusterShow !== value) {
                    this._clusterShow = value;

                    var glyphs = this._glyphs;
                    for (var i = 0, len = glyphs.length; i < len; i++) {
                        var glyph = glyphs[i];
                        if (defined(glyph.billboard)) {
                            // Set all the private values here, because we already clamped to ground
                            //  so we don't want to do it again for every glyph
                            glyph.billboard.clusterShow = value;
                        }
                    }
                }
            }
        }
    });

    Label.prototype._updateClamping = function() {
        Billboard._updateClamping(this._labelCollection, this);
    };

    /**
     * Computes the screen-space position of the label's origin, taking into account eye and pixel offsets.
     * The screen space origin is the top, left corner of the canvas; <code>x</code> increases from
     * left to right, and <code>y</code> increases from top to bottom.
     *
     * @param {Scene} scene The scene the label is in.
     * @param {Cartesian2} [result] The object onto which to store the result.
     * @returns {Cartesian2} The screen-space position of the label.
     *
     *
     * @example
     * console.log(l.computeScreenSpacePosition(scene).toString());
     *
     * @see Label#eyeOffset
     * @see Label#pixelOffset
     */
    Label.prototype.computeScreenSpacePosition = function(scene, result) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(scene)) {
            throw new DeveloperError('scene is required.');
        }
        //>>includeEnd('debug');

        if (!defined(result)) {
            result = new Cartesian2();
        }

        var labelCollection = this._labelCollection;
        var modelMatrix = labelCollection.modelMatrix;
        var actualPosition = defined(this._actualClampedPosition) ? this._actualClampedPosition : this._position;

        var windowCoordinates = Billboard._computeScreenSpacePosition(modelMatrix, actualPosition,
                this._eyeOffset, this._pixelOffset, scene, result);
        return windowCoordinates;
    };

    /**
     * Gets a label's screen space bounding box centered around screenSpacePosition.
     * @param {Label} label The label to get the screen space bounding box for.
     * @param {Cartesian2} screenSpacePosition The screen space center of the label.
     * @param {BoundingRectangle} [result] The object onto which to store the result.
     * @returns {BoundingRectangle} The screen space bounding box.
     *
     * @private
     */
    Label.getScreenSpaceBoundingBox = function(label, screenSpacePosition, result) {
        var width = 0;
        var height = 0;

        var glyphs = label._glyphs;
        var length = glyphs.length;
        for (var i = 0; i < length; ++i) {
            var glyph = glyphs[i];
            var billboard = glyph.billboard;
            if (!defined(billboard)) {
                continue;
            }

            width += billboard.width;
            height = Math.max(height, billboard.height);
        }

        var scale = label.scale;
        width *= scale;
        height *= scale;

        var x = screenSpacePosition.x;
        if (label.horizontalOrigin === HorizontalOrigin.RIGHT) {
            x -= width;
        } else if (label.horizontalOrigin === HorizontalOrigin.CENTER) {
            x -= width * 0.5;
        }

        var y = screenSpacePosition.y;
        if (label.verticalOrigin === VerticalOrigin.TOP) {
            y -= height;
        } else if (label.verticalOrigin === VerticalOrigin.CENTER) {
            y -= height * 0.5;
        }

        if (!defined(result)) {
            result = new BoundingRectangle();
        }

        result.x = x;
        result.y = y;
        result.width = width;
        result.height = height;

        return result;
    };

    /**
     * Determines if this label equals another label.  Labels are equal if all their properties
     * are equal.  Labels in different collections can be equal.
     *
     * @param {Label} other The label to compare for equality.
     * @returns {Boolean} <code>true</code> if the labels are equal; otherwise, <code>false</code>.
     */
    Label.prototype.equals = function(other) {
        return this === other ||
               defined(other) &&
               this._show === other._show &&
               this._scale === other._scale &&
               this._style === other._style &&
               this._verticalOrigin === other._verticalOrigin &&
               this._horizontalOrigin === other._horizontalOrigin &&
               this._heightReference === other._heightReference &&
               this._text === other._text &&
               this._font === other._font &&
               Cartesian3.equals(this._position, other._position) &&
               Color.equals(this._fillColor, other._fillColor) &&
               Color.equals(this._outlineColor, other._outlineColor) &&
               Cartesian2.equals(this._pixelOffset, other._pixelOffset) &&
               Cartesian3.equals(this._eyeOffset, other._eyeOffset) &&
               NearFarScalar.equals(this._translucencyByDistance, other._translucencyByDistance) &&
               NearFarScalar.equals(this._pixelOffsetScaleByDistance, other._pixelOffsetScaleByDistance) &&
               DistanceDisplayCondition.equals(this._distanceDisplayCondition, other._distanceDisplayCondition) &&
               this._id === other._id;
    };

    /**
     * Returns true if this object was destroyed; otherwise, false.
     * <br /><br />
     * If this object was destroyed, it should not be used; calling any function other than
     * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
     *
     * @returns {Boolean} True if this object was destroyed; otherwise, false.
     */
    Label.prototype.isDestroyed = function() {
        return false;
    };

    return Label;
});