Source: shapes/Text.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports Text
 */
define([
        '../error/ArgumentError',
        '../shaders/BasicTextureProgram',
        '../util/Color',
        '../util/Font',
        '../util/Logger',
        '../geom/Matrix',
        '../pick/PickedObject',
        '../render/Renderable',
        '../shapes/TextAttributes',
        '../error/UnsupportedOperationError',
        '../geom/Vec2',
        '../geom/Vec3',
        '../util/WWMath'
    ],
    function (ArgumentError,
              BasicTextureProgram,
              Color,
              Font,
              Logger,
              Matrix,
              PickedObject,
              Renderable,
              TextAttributes,
              UnsupportedOperationError,
              Vec2,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs a text shape. This constructor is intended to be called only by subclasses.
         * @alias Text
         * @constructor
         * @augments Renderable
         * @classdesc Represents a string of text displayed at a specified geographic or screen position.
         * This is an abstract class meant to be subclassed and not meant to be instantiated directly.
         * See {@link GeographicText} and {@link ScreenText} for concrete classes.
         *
         * @param {String} text The text to display.
         * @throws {ArgumentError} If the specified text is null or undefined.
         */
        var Text = function (text) {
            if (!text) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "Text", "constructor", "missingText"));
            }

            Renderable.call(this);

            /**
             * The text's attributes. If null and this text is not highlighted, this text is not drawn.
             * @type {TextAttributes}
             * @default see [TextAttributes]{@link TextAttributes}
             */
            this.attributes = new TextAttributes(null);

            /**
             * The attributes used when this text's highlighted flag is true. If null and the
             * highlighted flag is true, this text's normal attributes are used. If they, too, are null, this
             * text is not drawn.
             * @type {TextAttributes}
             * @default null
             */
            this.highlightAttributes = null;

            /**
             * Indicates whether this text uses its highlight attributes rather than its normal attributes.
             * @type {boolean}
             * @default false
             */
            this.highlighted = false;

            /**
             * Indicates whether this text is drawn.
             * @type {boolean}
             * @default true
             */
            this.enabled = true;

            /**
             * This shape's text. If null or empty, no text is drawn.
             * @type {String}
             * @default null
             */
            this.text = text;

            /**
             * This text's altitude mode. May be one of
             * <ul>
             *  <li>[WorldWind.ABSOLUTE]{@link WorldWind#ABSOLUTE}</li>
             *  <li>[WorldWind.RELATIVE_TO_GROUND]{@link WorldWind#RELATIVE_TO_GROUND}</li>
             *  <li>[WorldWind.CLAMP_TO_GROUND]{@link WorldWind#CLAMP_TO_GROUND}</li>
             * </ul>
             * @default WorldWind.ABSOLUTE
             */
            this.altitudeMode = WorldWind.ABSOLUTE;

            /**
             * Indicates the object to return as the userObject of this text when picked. If null,
             * then this text object is returned as the userObject.
             * @type {Object}
             * @default null
             * @see  [PickedObject.userObject]{@link PickedObject#userObject}
             */
            this.pickDelegate = null;

            /**
             * Indicates whether this text has visual priority over other shapes in the scene.
             * @type {Boolean}
             * @default false
             */
            this.alwaysOnTop = false;

            /**
             * This shape's target visibility, a value between 0 and 1. During ordered rendering this shape modifies its
             * [current visibility]{@link Text#currentVisibility} towards its target visibility at the rate
             * specified by the draw context's [fadeVelocity]{@link DrawContext#fadeVelocity} property. The target
             * visibility and current visibility are used to control the fading in and out of this shape.
             * @type {Number}
             * @default 1
             */
            this.targetVisibility = 1;

            /**
             * This shape's current visibility, a value between 0 and 1. This property scales the shape's effective
             * opacity. It is incremented or decremented each frame according to the draw context's
             * [fade velocity]{@link DrawContext#fadeVelocity} property in order to achieve this shape's current
             * [target visibility]{@link Text#targetVisibility}. This current visibility and target visibility are
             * used to control the fading in and out of this shape.
             * @type {Number}
             * @default 1
             * @readonly
             */
            this.currentVisibility = 1;

            /**
             * Indicates the group ID of the declutter group to include this Text shape. If non-zer0, this shape
             * is decluttered relative to all other shapes within its group.
             * @type {Number}
             * @default 0
             */
            this.declutterGroup = 0;

            /**
             * The image to display when this text shape is eliminated from the scene due to decluttering.
             * @type {String}
             * @default A round dot drawn in this shape's text color.
             */
            this.markerImageSource = WorldWind.configuration.baseUrl + "images/white-dot.png";

            /**
             * The scale to apply to the [markerImageSource]{@link Text#markerImageSource}.
             * @type {Number}
             * @default 0.1
             */
            this.markerImageScale = 0.1;

            // Internal use only. Intentionally not documented.
            this.activeAttributes = null;

            // Internal use only. Intentionally not documented.
            this.activeTexture = null;

            // Internal use only. Intentionally not documented.
            this.imageTransform = Matrix.fromIdentity();

            // Internal use only. Intentionally not documented.
            this.imageBounds = null;

            // Internal use only. Intentionally not documented.
            this.layer = null;

            // Internal use only. Intentionally not documented.
            this.depthOffset = -0.003;

            // Internal use only. Intentionally not documented.
            this.screenPoint = new Vec3(0, 0, 0);
        };

        // Internal use only. Intentionally not documented.
        Text.matrix = Matrix.fromIdentity(); // scratch variable
        Text.glPickPoint = new Vec3(0, 0, 0); // scratch variable

        Text.prototype = Object.create(Renderable.prototype);

        /**
         * Copies the contents of a specified text object to this text object.
         * @param {Text} that The text object to copy.
         */
        Text.prototype.copy = function (that) {
            this.text = that.text;
            this.attributes = that.attributes;
            this.highlightAttributes = that.highlightAttributes;
            this.highlighted = that.highlighted;
            this.enabled = that.enabled;
            this.altitudeMode = that.altitudeMode;
            this.pickDelegate = that.pickDelegate;
            this.alwaysOnTop = that.alwaysOnTop;
            this.depthOffset = that.depthOffset;
            this.declutterGroup = that.declutterGroup;
            this.targetVisibility = that.targetVisibility;
            this.currentVisibility = that.currentVisibility;

            return this;
        };

        Object.defineProperties(Text.prototype, {
            /**
             * Indicates the screen coordinate bounds of this shape during ordered rendering.
             * @type {Rectangle}
             * @readonly
             * @memberof Text.prototype
             */
            screenBounds: {
                get: function () {
                    return this.imageBounds;
                }
            }
        });

        /**
         * Renders this text. This method is typically not called by applications but is called by
         * [RenderableLayer]{@link RenderableLayer} during rendering. For this shape this method creates and
         * enques an ordered renderable with the draw context and does not actually draw the text.
         * @param {DrawContext} dc The current draw context.
         */
        Text.prototype.render = function (dc) {
            if (!this.enabled || (!this.text) || this.text.length === 0) {
                return;
            }

            if (!dc.accumulateOrderedRenderables) {
                return;
            }

            // Create an ordered renderable for this text. If one has already been created this frame then we're
            // in 2D-continuous mode and another needs to be created for one of the alternate globe offsets.
            var orderedText;
            if (this.lastFrameTime != dc.timestamp) {
                orderedText = this.makeOrderedRenderable(dc);
            } else {
                var textCopy = this.clone();
                orderedText = textCopy.makeOrderedRenderable(dc);
            }

            if (!orderedText) {
                return;
            }

            if (!orderedText.isVisible(dc)) {
                return;
            }

            orderedText.layer = dc.currentLayer;

            this.lastFrameTime = dc.timestamp;
            dc.addOrderedRenderable(orderedText);
        };

        /**
         * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
         * {@link WorldWindow} during rendering. Implements the {@link OrderedRenderable} interface.
         * @param {DrawContext} dc The current draw context.
         */
        Text.prototype.renderOrdered = function (dc) {
            // Optimize away the case of achieved target visibility of 0 and no marker image to display in that case.
            if (this.currentVisibility === 0 && this.targetVisibility === 0 && !this.markerImageSource) {
                return;
            }

            this.drawOrderedText(dc);

            if (dc.pickingMode) {
                var po = new PickedObject(this.pickColor.clone(), this.pickDelegate ? this.pickDelegate : this,
                    this.position, this.layer, false);

                dc.resolvePick(po);
            }
        };

        // Intentionally not documented.
        Text.prototype.makeOrderedRenderable = function (dc) {
            var w, h, s,
                offset;

            this.determineActiveAttributes(dc);
            if (!this.activeAttributes) {
                return null;
            }

            //// Compute the text's screen point and distance to the eye point.
            if (!this.computeScreenPointAndEyeDistance(dc)) {
                return null;
            }

            var labelFont = this.activeAttributes.font,
                textureKey = this.text + labelFont.toString();

            this.activeTexture = dc.gpuResourceCache.resourceForKey(textureKey);
            if (!this.activeTexture) {
                this.activeTexture = dc.textRenderer.renderText(this.text);
                dc.gpuResourceCache.putResource(textureKey, this.activeTexture, this.activeTexture.size);
            }

            w = this.activeTexture.imageWidth;
            h = this.activeTexture.imageHeight;
            s = this.activeAttributes.scale;
            offset = this.activeAttributes.offset.offsetForSize(w, h);

            this.imageTransform.setTranslation(
                this.screenPoint[0] - offset[0] * s,
                this.screenPoint[1] - offset[1] * s,
                this.screenPoint[2]);

            this.imageTransform.setScale(w * s, h * s, 1);

            this.imageBounds = WWMath.boundingRectForUnitQuad(this.imageTransform);

            return this;
        };

        /**
         * Computes this shape's screen point and eye distance. Subclasses must override this method.
         * @param {DrawContext} dc The current draw context.
         * @returns {Boolean} true if the screen point can be computed, otherwise false.
         * @protected
         */
        Text.prototype.computeScreenPointAndEyeDistance = function (dc) {
            throw new UnsupportedOperationError(
                Logger.logMessage(Logger.LEVEL_SEVERE, "Renderable", "render", "abstractInvocation"));
        };

        // Internal. Intentionally not documented.
        Text.prototype.determineActiveAttributes = function (dc) {
            if (this.highlighted && this.highlightAttributes) {
                this.activeAttributes = this.highlightAttributes;
            } else {
                this.activeAttributes = this.attributes;
            }
        };

        // Internal. Intentionally not documented.
        Text.prototype.isVisible = function (dc) {
            if (dc.pickingMode) {
                return dc.pickRectangle && this.imageBounds.intersects(dc.pickRectangle);
            } else {
                return this.imageBounds.intersects(dc.viewport);
            }
        };

        // Internal. Intentionally not documented.
        Text.prototype.drawOrderedText = function (dc) {
            this.beginDrawing(dc);

            try {
                this.doDrawOrderedText(dc);
                if (!dc.pickingMode) {
                    //this.drawBatchOrderedText(dc);
                }
            } finally {
                this.endDrawing(dc);
            }
        };

        // Internal. Intentionally not documented.
        Text.prototype.drawBatchOrderedText = function (dc) {
            // Draw any subsequent text in the ordered renderable queue, removing each from the queue as it's
            // processed. This avoids the overhead of setting up and tearing down OpenGL state for each text shape.

            var or;

            while ((or = dc.peekOrderedRenderable()) && or instanceof Text) {
                dc.popOrderedRenderable(); // remove it from the queue

                try {
                    or.doDrawOrderedText(dc)
                } catch (e) {
                    Logger.logMessage(Logger.LEVEL_WARNING, 'Text', 'drawBatchOrderedText',
                        "Error occurred while rendering text using batching: " + e.message);
                }
                // Keep going. Render the rest of the ordered renderables.
            }
        };

        // Internal. Intentionally not documented.
        Text.prototype.beginDrawing = function (dc) {
            var gl = dc.currentGlContext,
                program;

            dc.findAndBindProgram(BasicTextureProgram);

            // Configure GL to use the draw context's unit quad VBOs for both model coordinates and texture coordinates.
            // Most browsers can share the same buffer for vertex and texture coordinates, but Internet Explorer requires
            // that they be in separate buffers, so the code below uses the 3D buffer for vertex coords and the 2D
            // buffer for texture coords.
            program = dc.currentProgram;
            gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
            gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);
            gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer());
            gl.vertexAttribPointer(program.vertexTexCoordLocation, 2, gl.FLOAT, false, 0, 0);
            gl.enableVertexAttribArray(program.vertexPointLocation);
            gl.enableVertexAttribArray(program.vertexTexCoordLocation);

            // Tell the program which texture unit to use.
            program.loadTextureUnit(gl, gl.TEXTURE0);

            // Turn off color modulation since we want to pick against the text box and not just the text.
            program.loadModulateColor(gl, false);

            // Suppress depth-buffer writes.
            gl.depthMask(false);
        };

        // Internal. Intentionally not documented.
        Text.prototype.endDrawing = function (dc) {
            var gl = dc.currentGlContext,
                program = dc.currentProgram;

            // Restore the default GL vertex attribute state.
            gl.disableVertexAttribArray(program.vertexPointLocation);
            gl.disableVertexAttribArray(program.vertexTexCoordLocation);

            // Restore the default GL buffer and texture bindings.
            gl.bindBuffer(gl.ARRAY_BUFFER, null);
            gl.bindTexture(gl.TEXTURE_2D, null);

            // Restore the default GL depth mask state.
            gl.depthMask(true);
        };

        // Internal. Intentionally not documented.
        Text.prototype.doDrawOrderedText = function (dc) {
            var gl = dc.currentGlContext,
                program = dc.currentProgram;

            // Compute the text's current visibility, potentially requesting additional frames.
            if (!dc.pickingMode && this.currentVisibility !== this.targetVisibility) {
                var visibilityDelta = (dc.timestamp - dc.previousRedrawTimestamp) / dc.fadeTime;
                if (this.currentVisibility < this.targetVisibility) {
                    this.currentVisibility = Math.min(1, this.currentVisibility + visibilityDelta);
                } else {
                    this.currentVisibility = Math.max(0, this.currentVisibility - visibilityDelta);
                }
                dc.redrawRequested = true;
            }

            // Turn off depth testing for the text unless it's been requested.
            if (!this.activeAttributes.depthTest) {
                gl.disable(gl.DEPTH_TEST);
            }

            // Use the text color and opacity. When picking, use the pick color, 100% opacity and no texture.
            if (!dc.pickingMode) {
                program.loadColor(gl, this.activeAttributes.color);
                program.loadOpacity(gl, this.layer.opacity * this.currentVisibility);
            } else {
                this.pickColor = dc.uniquePickColor();
                program.loadColor(gl, this.pickColor);
                program.loadOpacity(gl, 1);
                program.loadTextureEnabled(gl, false);
            }

            // When the text is visible, draw the text label.
            if (this.currentVisibility > 0) {
                this.drawLabel(dc);
            }

            // When the text is not visible, draw a marker to indicate that something is there.
            if (this.currentVisibility < 1 && this.markerImageSource) {
                this.drawMarker(dc);
            }

            // Restore the default GL depth test state.
            if (!this.activeAttributes.depthTest) {
                gl.enable(gl.DEPTH_TEST);
            }
        };

        // Internal. Intentionally not documented.
        Text.prototype.drawLabel = function (dc) {
            var gl = dc.currentGlContext,
                program = dc.currentProgram,
                textureBound;

            // Use the label texture when not picking.
            if (!dc.pickingMode && this.activeTexture) {
                Text.matrix.setToIdentity();
                Text.matrix.multiplyByTextureTransform(this.activeTexture);
                textureBound = this.activeTexture.bind(dc); // returns false if texture is null or cannot be bound
                program.loadTextureEnabled(gl, textureBound);
                program.loadTextureMatrix(gl, Text.matrix);
            }

            // Compute and specify the text label's modelview-projection matrix.
            Text.matrix.copy(dc.screenProjection);
            Text.matrix.multiplyMatrix(this.imageTransform);
            program.loadModelviewProjection(gl, Text.matrix);

            // Draw the text as a two-triangle square.
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        };

        // Internal. Intentionally not documented.
        Text.prototype.drawMarker = function (dc) {
            var gl = dc.currentGlContext,
                program = dc.currentProgram,
                textureBound;

            var markerTexture = dc.gpuResourceCache.resourceForKey(this.markerImageSource);
            if (!markerTexture) {
                dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this.markerImageSource);
                return;
            }

            // Use the marker opacity and texture when not picking.
            if (!dc.pickingMode) {
                Text.matrix.setToIdentity();
                Text.matrix.multiplyByTextureTransform(markerTexture);
                textureBound = markerTexture.bind(dc); // returns false if texture is null or cannot be bound
                program.loadTextureEnabled(gl, textureBound);
                program.loadTextureMatrix(gl, Text.matrix);
                program.loadOpacity(gl, this.layer.opacity * (1 - this.currentVisibility));
            }

            // Compute and specify the marker's modelview-projection matrix.
            var s = this.markerImageScale;
            Text.matrix.copy(dc.screenProjection);
            Text.matrix.multiplyByTranslation(
                this.screenPoint[0] - s * markerTexture.imageWidth / 2,
                this.screenPoint[1] - s * markerTexture.imageWidth / 2,
                this.screenPoint[2]);
            Text.matrix.multiplyByScale(markerTexture.imageWidth * s, markerTexture.imageHeight * s, 1);
            program.loadModelviewProjection(gl, Text.matrix);

            // Draw the marker as a two-triangle square.
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        };

        return Text;
    });