Source: shapes/ScreenImage.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 ScreenImage
 */
define([
        '../error/ArgumentError',
        '../shaders/BasicTextureProgram',
        '../util/Color',
        '../util/ImageSource',
        '../util/Logger',
        '../geom/Matrix',
        '../util/Offset',
        '../pick/PickedObject',
        '../render/Renderable',
        '../geom/Vec3',
        '../util/WWMath'
    ],
    function (ArgumentError,
              BasicTextureProgram,
              Color,
              ImageSource,
              Logger,
              Matrix,
              Offset,
              PickedObject,
              Renderable,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs a screen image.
         * @alias ScreenImage
         * @constructor
         * @augments Renderable
         * @classdesc Displays an image at a specified screen location in the WorldWindow.
         * The image location is specified by an offset, which causes the image to maintain its relative position
         * when the window size changes.
         * @param {Offset} screenOffset The offset indicating the image's placement on the screen.
         * Use [the image offset property]{@link ScreenImage#imageOffset} to position the image relative to the
         * specified screen offset.
         * @param {String|ImageSource} imageSource The source of the image to display.
         * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
         * dynamically created image.
         * @throws {ArgumentError} If the specified screen offset or image source is null or undefined.
         */
        var ScreenImage = function (screenOffset, imageSource) {
            if (!screenOffset) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "constructor", "missingOffset"));
            }

            if (!imageSource) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "constructor", "missingImage"));
            }

            Renderable.call(this);

            /**
             * The offset indicating this screen image's placement on the screen.
             * @type {Offset}
             */
            this.screenOffset = screenOffset;

            // Documented with its property accessor below.
            this._imageSource = imageSource;

            /**
             * The image color. When displayed, this shape's image is multiplied by this image color to achieve the
             * final image color. The color white, the default, causes the image to be drawn in its native colors.
             * @type {Color}
             * @default White (1, 1, 1, 1)
             */
            this.imageColor = Color.WHITE;

            /**
             * Indicates the location within the image at which to align with the specified screen location.
             * May be null, in which case the image's bottom-left corner is placed at the screen location.
             * @type {Offset}
             * @default 0.5, 0.5, both fractional (Centers the image on the screen location.)
             */
            this.imageOffset = new Offset(WorldWind.OFFSET_FRACTION, 0.5, WorldWind.OFFSET_FRACTION, 0.5);

            /**
             * Indicates the amount to scale the image.
             * @type {Number}
             * @default 1
             */
            this.imageScale = 1;

            /**
             * The amount of rotation to apply to the image, measured in degrees clockwise from the top of the window.
             * @type {Number}
             * @default 0
             */
            this.imageRotation = 0;

            /**
             * The amount of tilt to apply to the image, measured in degrees.
             * @type {Number}
             * @default 0
             */
            this.imageTilt = 0;

            /**
             * Indicates whether to draw this screen image.
             * @type {Boolean}
             * @default true
             */
            this.enabled = true;

            /**
             * This image's opacity. When this screen image is drawn, the actual opacity is the product of
             * this opacity and the opacity of the layer containing this screen image.
             * @type {Number}
             */
            this.opacity = 1;

            /**
             * Indicates the object to return as the userObject of this shape when picked. If null,
             * then this shape is returned as the userObject.
             * @type {Object}
             * @default null
             * @see  [PickedObject.userObject]{@link PickedObject#userObject}
             */
            this.pickDelegate = 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.texCoordMatrix = 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.
        ScreenImage.matrix = Matrix.fromIdentity(); // scratch variable

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

        Object.defineProperties(ScreenImage.prototype, {
            /**
             * The source of the image to display.
             * May be either a string identifying the URL of the image, or an {@link ImageSource} object identifying a
             * dynamically created image.
             * @type {String|ImageSource}
             * @default null
             * @memberof ScreenImage.prototype
             */
            imageSource: {
                get: function () {
                    return this._imageSource;
                },
                set: function (imageSource) {
                    if (!imageSource) {
                        throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "ScreenImage", "imageSource",
                            "missingImage"));
                    }

                    this._imageSource = imageSource;
                    this.imageSourceWasUpdated = true;
                }
            }
        });

        /**
         * Renders this screen image. This method is typically not called by applications but is called by
         * {@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 image.
         * @param {DrawContext} dc The current draw context.
         */
        ScreenImage.prototype.render = function (dc) {
            if (!this.enabled) {
                return;
            }

            if (!dc.accumulateOrderedRenderables) {
                return;
            }

            // Create an ordered renderable, but don't create more than one per frame.
            var orderedScreenImage = null;
            if (this.lastFrameTime !== dc.timestamp) {
                orderedScreenImage = this.makeOrderedRenderable(dc);
            }

            if (!orderedScreenImage) {
                return;
            }

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

            orderedScreenImage.layer = dc.currentLayer;

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

        /**
         * Draws this shape as an ordered renderable. Applications do not call this function. It is called by
         * [WorldWindow]{@link WorldWindow} during rendering.
         * @param {DrawContext} dc The current draw context.
         */
        ScreenImage.prototype.renderOrdered = function (dc) {
            this.drawOrderedScreenImage(dc);

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

        // Internal. Intentionally not documented.
        ScreenImage.prototype.makeOrderedRenderable = function (dc) {
            var w, h, s, ws, hs,
                iOffset, sOffset;

            this.activeTexture = this.getActiveTexture(dc);
            if (!this.activeTexture || this.imageSourceWasUpdated) {
                this.activeTexture = dc.gpuResourceCache.retrieveTexture(dc.currentGlContext, this._imageSource);
                if (!this.activeTexture) {
                    return null;
                }
            }

            this.eyeDistance = 0;

            // Compute the image's transform matrix and texture coordinate matrix according to its screen point, image size,
            // image offset and image scale. The image offset is defined with its origin at the image's bottom-left corner and
            // axes that extend up and to the right from the origin point.
            w = this.activeTexture.imageWidth;
            h = this.activeTexture.imageHeight;
            s = this.imageScale;
            iOffset = this.imageOffset.offsetForSize(w, h);
            ws = dc.navigatorState.viewport.width;
            hs = dc.navigatorState.viewport.height;
            sOffset = this.screenOffset.offsetForSize(ws, hs);

            this.imageTransform.setTranslation(
                sOffset[0] - iOffset[0] * s,
                sOffset[1] - iOffset[1] * s,
                0);

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

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

            return this;
        };

        ScreenImage.prototype.getActiveTexture = function (dc) {
            return dc.gpuResourceCache.resourceForKey(this._imageSource);
        };

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

        // Internal. Intentionally not documented.
        ScreenImage.prototype.drawOrderedScreenImage = function (dc) {
            this.beginDrawing(dc);
            try {
                this.doDrawOrderedScreenImage(dc);
            } finally {
                this.endDrawing(dc);
            }
        };

        // Internal. Intentionally not documented.
        ScreenImage.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.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);
            program.loadModulateColor(gl, dc.pickingMode);

            // Turn off depth testing.
            gl.disable(gl.DEPTH_TEST);
        };

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

            // Clear the vertex attribute state.
            gl.disableVertexAttribArray(program.vertexPointLocation);
            gl.disableVertexAttribArray(program.vertexTexCoordLocation);

            // Clear GL bindings.
            gl.bindBuffer(gl.ARRAY_BUFFER, null);
            gl.bindTexture(gl.TEXTURE_2D, null);

            // Re-enable depth testing.
            gl.enable(gl.DEPTH_TEST);
        };

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

            gl.bindBuffer(gl.ARRAY_BUFFER, dc.unitQuadBuffer3());
            gl.vertexAttribPointer(program.vertexPointLocation, 3, gl.FLOAT, false, 0, 0);

            // Compute and specify the MVP matrix.
            ScreenImage.matrix.copy(dc.screenProjection);
            ScreenImage.matrix.multiplyMatrix(this.imageTransform);

            ScreenImage.matrix.multiplyByTranslation(0.5, 0.5, 0.5); // shift Z to prevent image clipping
            ScreenImage.matrix.multiplyByRotation(1, 0, 0, this.imageTilt);
            ScreenImage.matrix.multiplyByRotation(0, 0, 1, this.imageRotation);
            ScreenImage.matrix.multiplyByTranslation(-0.5, -0.5, 0);

            program.loadModelviewProjection(gl, ScreenImage.matrix);

            // Enable texture for both normal display and for picking. If picking is enabled in the shader (set in
            // beginDrawing() above) then the texture's alpha component is still needed in order to modulate the
            // pick color to mask off transparent pixels.
            program.loadTextureEnabled(gl, true);

            // Set the pick color for picking or the color and opacity if not picking.
            if (dc.pickingMode) {
                this.pickColor = dc.uniquePickColor();
                program.loadColor(gl, this.pickColor);
            } else {
                program.loadColor(gl, this.imageColor);
                program.loadOpacity(gl, this.opacity * this.layer.opacity);
            }

            this.texCoordMatrix.setToIdentity();
            this.texCoordMatrix.multiplyByTextureTransform(this.activeTexture);
            program.loadTextureMatrix(gl, this.texCoordMatrix);

            if (this.activeTexture.bind(dc)) { // returns false if active texture cannot be bound
                // Draw the placemark's image quad.
                gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
            }
        };

        return ScreenImage;
    })
;