Source: main/webapp/modules/Display.js

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

var Guacamole = Guacamole || {};

/**
 * The Guacamole display. The display does not deal with the Guacamole
 * protocol, and instead implements a set of graphical operations which
 * embody the set of operations present in the protocol. The order operations
 * are executed is guaranteed to be in the same order as their corresponding
 * functions are called.
 * 
 * @constructor
 */
Guacamole.Display = function() {

    /**
     * Reference to this Guacamole.Display.
     * @private
     */
    var guac_display = this;

    var displayWidth = 0;
    var displayHeight = 0;
    var displayScale = 1;

    // Create display
    var display = document.createElement("div");
    display.style.position = "relative";
    display.style.width = displayWidth + "px";
    display.style.height = displayHeight + "px";

    // Ensure transformations on display originate at 0,0
    display.style.transformOrigin =
    display.style.webkitTransformOrigin =
    display.style.MozTransformOrigin =
    display.style.OTransformOrigin =
    display.style.msTransformOrigin =
        "0 0";

    // Create default layer
    var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);

    // Create cursor layer
    var cursor = new Guacamole.Display.VisibleLayer(0, 0);
    cursor.setChannelMask(Guacamole.Layer.SRC);

    // Add default layer and cursor to display
    display.appendChild(default_layer.getElement());
    display.appendChild(cursor.getElement());

    // Create bounding div 
    var bounds = document.createElement("div");
    bounds.style.position = "relative";
    bounds.style.width = (displayWidth*displayScale) + "px";
    bounds.style.height = (displayHeight*displayScale) + "px";

    // Add display to bounds
    bounds.appendChild(display);

    /**
     * The X coordinate of the hotspot of the mouse cursor. The hotspot is
     * the relative location within the image of the mouse cursor at which
     * each click occurs.
     * 
     * @type {!number}
     */
    this.cursorHotspotX = 0;

    /**
     * The Y coordinate of the hotspot of the mouse cursor. The hotspot is
     * the relative location within the image of the mouse cursor at which
     * each click occurs.
     * 
     * @type {!number}
     */
    this.cursorHotspotY = 0;

    /**
     * The current X coordinate of the local mouse cursor. This is not
     * necessarily the location of the actual mouse - it refers only to
     * the location of the cursor image within the Guacamole display, as
     * last set by moveCursor().
     * 
     * @type {!number}
     */
    this.cursorX = 0;

    /**
     * The current X coordinate of the local mouse cursor. This is not
     * necessarily the location of the actual mouse - it refers only to
     * the location of the cursor image within the Guacamole display, as
     * last set by moveCursor().
     * 
     * @type {!number}
     */
    this.cursorY = 0;

    /**
     * Fired when the default layer (and thus the entire Guacamole display)
     * is resized.
     * 
     * @event
     * @param {!number} width
     *     The new width of the Guacamole display.
     *
     * @param {!number} height
     *     The new height of the Guacamole display.
     */
    this.onresize = null;

    /**
     * Fired whenever the local cursor image is changed. This can be used to
     * implement special handling of the client-side cursor, or to override
     * the default use of a software cursor layer.
     * 
     * @event
     * @param {!HTMLCanvasElement} canvas
     *     The cursor image.
     *
     * @param {!number} x
     *     The X-coordinate of the cursor hotspot.
     *
     * @param {!number} y
     *     The Y-coordinate of the cursor hotspot.
     */
    this.oncursor = null;

    /**
     * The queue of all pending Tasks. Tasks will be run in order, with new
     * tasks added at the end of the queue and old tasks removed from the
     * front of the queue (FIFO). These tasks will eventually be grouped
     * into a Frame.
     *
     * @private
     * @type {!Task[]}
     */
    var tasks = [];

    /**
     * The queue of all frames. Each frame is a pairing of an array of tasks
     * and a callback which must be called when the frame is rendered.
     *
     * @private
     * @type {!Frame[]}
     */
    var frames = [];

    /**
     * Flushes all pending frames.
     * @private
     */
    function __flush_frames() {

        var rendered_frames = 0;

        // Draw all pending frames, if ready
        while (rendered_frames < frames.length) {

            var frame = frames[rendered_frames];
            if (!frame.isReady())
                break;

            frame.flush();
            rendered_frames++;

        } 

        // Remove rendered frames from array
        frames.splice(0, rendered_frames);

    }

    /**
     * An ordered list of tasks which must be executed atomically. Once
     * executed, an associated (and optional) callback will be called.
     *
     * @private
     * @constructor
     * @param {function} [callback]
     *     The function to call when this frame is rendered.
     *
     * @param {!Task[]} tasks
     *     The set of tasks which must be executed to render this frame.
     */
    function Frame(callback, tasks) {

        /**
         * Cancels rendering of this frame and all associated tasks. The
         * callback provided at construction time, if any, is not invoked.
         */
        this.cancel = function cancel() {

            callback = null;

            tasks.forEach(function cancelTask(task) {
                task.cancel();
            });

            tasks = [];

        };

        /**
         * Returns whether this frame is ready to be rendered. This function
         * returns true if and only if ALL underlying tasks are unblocked.
         * 
         * @returns {!boolean}
         *     true if all underlying tasks are unblocked, false otherwise.
         */
        this.isReady = function() {

            // Search for blocked tasks
            for (var i=0; i < tasks.length; i++) {
                if (tasks[i].blocked)
                    return false;
            }

            // If no blocked tasks, the frame is ready
            return true;

        };

        /**
         * Renders this frame, calling the associated callback, if any, after
         * the frame is complete. This function MUST only be called when no
         * blocked tasks exist. Calling this function with blocked tasks
         * will result in undefined behavior.
         */
        this.flush = function() {

            // Draw all pending tasks.
            for (var i=0; i < tasks.length; i++)
                tasks[i].execute();

            // Call callback
            if (callback) callback();

        };

    }

    /**
     * A container for an task handler. Each operation which must be ordered
     * is associated with a Task that goes into a task queue. Tasks in this
     * queue are executed in order once their handlers are set, while Tasks 
     * without handlers block themselves and any following Tasks from running.
     *
     * @constructor
     * @private
     * @param {function} [taskHandler]
     *     The function to call when this task runs, if any.
     *
     * @param {boolean} [blocked]
     *     Whether this task should start blocked.
     */
    function Task(taskHandler, blocked) {

        /**
         * Reference to this Task.
         *
         * @private
         * @type {!Guacamole.Display.Task}
         */
        var task = this;
       
        /**
         * Whether this Task is blocked.
         * 
         * @type {boolean}
         */
        this.blocked = blocked;

        /**
         * Cancels this task such that it will not run. The task handler
         * provided at construction time, if any, is not invoked. Calling
         * execute() after calling this function has no effect.
         */
        this.cancel = function cancel() {
            task.blocked = false;
            taskHandler = null;
        };

        /**
         * Unblocks this Task, allowing it to run.
         */
        this.unblock = function() {
            if (task.blocked) {
                task.blocked = false;
                __flush_frames();
            }
        };

        /**
         * Calls the handler associated with this task IMMEDIATELY. This
         * function does not track whether this task is marked as blocked.
         * Enforcing the blocked status of tasks is up to the caller.
         */
        this.execute = function() {
            if (taskHandler) taskHandler();
        };

    }

    /**
     * Schedules a task for future execution. The given handler will execute
     * immediately after all previous tasks upon frame flush, unless this
     * task is blocked. If any tasks is blocked, the entire frame will not
     * render (and no tasks within will execute) until all tasks are unblocked.
     * 
     * @private
     * @param {function} [handler]
     *     The function to call when possible, if any.
     *
     * @param {boolean} [blocked]
     *     Whether the task should start blocked.
     *
     * @returns {!Task}
     *     The Task created and added to the queue for future running.
     */
    function scheduleTask(handler, blocked) {
        var task = new Task(handler, blocked);
        tasks.push(task);
        return task;
    }

    /**
     * Returns the element which contains the Guacamole display.
     * 
     * @return {!Element}
     *     The element containing the Guacamole display.
     */
    this.getElement = function() {
        return bounds;
    };

    /**
     * Returns the width of this display.
     * 
     * @return {!number}
     *     The width of this display;
     */
    this.getWidth = function() {
        return displayWidth;
    };

    /**
     * Returns the height of this display.
     * 
     * @return {!number}
     *     The height of this display;
     */
    this.getHeight = function() {
        return displayHeight;
    };

    /**
     * Returns the default layer of this display. Each Guacamole display always
     * has at least one layer. Other layers can optionally be created within
     * this layer, but the default layer cannot be removed and is the absolute
     * ancestor of all other layers.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The default layer.
     */
    this.getDefaultLayer = function() {
        return default_layer;
    };

    /**
     * Returns the cursor layer of this display. Each Guacamole display contains
     * a layer for the image of the mouse cursor. This layer is a special case
     * and exists above all other layers, similar to the hardware mouse cursor.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The cursor layer.
     */
    this.getCursorLayer = function() {
        return cursor;
    };

    /**
     * Creates a new layer. The new layer will be a direct child of the default
     * layer, but can be moved to be a child of any other layer. Layers returned
     * by this function are visible.
     * 
     * @return {!Guacamole.Display.VisibleLayer}
     *     The newly-created layer.
     */
    this.createLayer = function() {
        var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);
        layer.move(default_layer, 0, 0, 0);
        return layer;
    };

    /**
     * Creates a new buffer. Buffers are invisible, off-screen surfaces. They
     * are implemented in the same manner as layers, but do not provide the
     * same nesting semantics.
     * 
     * @return {!Guacamole.Layer}
     *     The newly-created buffer.
     */
    this.createBuffer = function() {
        var buffer = new Guacamole.Layer(0, 0);
        buffer.autosize = 1;
        return buffer;
    };

    /**
     * Flush all pending draw tasks, if possible, as a new frame. If the entire
     * frame is not ready, the flush will wait until all required tasks are
     * unblocked.
     * 
     * @param {function} [callback]
     *     The function to call when this frame is flushed. This may happen
     *     immediately, or later when blocked tasks become unblocked.
     */
    this.flush = function(callback) {

        // Add frame, reset tasks
        frames.push(new Frame(callback, tasks));
        tasks = [];

        // Attempt flush
        __flush_frames();

    };

    /**
     * Cancels rendering of all pending frames and associated rendering
     * operations. The callbacks provided to outstanding past calls to flush(),
     * if any, are not invoked.
     */
    this.cancel = function cancel() {

        frames.forEach(function cancelFrame(frame) {
            frame.cancel();
        });

        frames = [];

        tasks.forEach(function cancelTask(task) {
            task.cancel();
        });

        tasks = [];

    };

    /**
     * Sets the hotspot and image of the mouse cursor displayed within the
     * Guacamole display.
     * 
     * @param {!number} hotspotX
     *     The X coordinate of the cursor hotspot.
     *
     * @param {!number} hotspotY
     *     The Y coordinate of the cursor hotspot.
     *
     * @param {!Guacamole.Layer} layer
     *     The source layer containing the data which should be used as the
     *     mouse cursor image.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source layer's coordinate
     *     space to copy data from.
     */
    this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) {
        scheduleTask(function __display_set_cursor() {

            // Set hotspot
            guac_display.cursorHotspotX = hotspotX;
            guac_display.cursorHotspotY = hotspotY;

            // Reset cursor size
            cursor.resize(srcw, srch);

            // Draw cursor to cursor layer
            cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0);
            guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY);

            // Fire cursor change event
            if (guac_display.oncursor)
                guac_display.oncursor(cursor.toCanvas(), hotspotX, hotspotY);

        });
    };

    /**
     * Sets whether the software-rendered cursor is shown. This cursor differs
     * from the hardware cursor in that it is built into the Guacamole.Display,
     * and relies on its own Guacamole layer to render.
     *
     * @param {boolean} [shown=true]
     *     Whether to show the software cursor.
     */
    this.showCursor = function(shown) {

        var element = cursor.getElement();
        var parent = element.parentNode;

        // Remove from DOM if hidden
        if (shown === false) {
            if (parent)
                parent.removeChild(element);
        }

        // Otherwise, ensure cursor is child of display
        else if (parent !== display)
            display.appendChild(element);

    };

    /**
     * Sets the location of the local cursor to the given coordinates. For the
     * sake of responsiveness, this function performs its action immediately.
     * Cursor motion is not maintained within atomic frames.
     * 
     * @param {!number} x
     *     The X coordinate to move the cursor to.
     *
     * @param {!number} y
     *     The Y coordinate to move the cursor to.
     */
    this.moveCursor = function(x, y) {

        // Move cursor layer
        cursor.translate(x - guac_display.cursorHotspotX,
                         y - guac_display.cursorHotspotY);

        // Update stored position
        guac_display.cursorX = x;
        guac_display.cursorY = y;

    };

    /**
     * Changes the size of the given Layer to the given width and height.
     * Resizing is only attempted if the new size provided is actually different
     * from the current size.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to resize.
     *
     * @param {!number} width
     *     The new width.
     *
     * @param {!number} height
     *     The new height.
     */
    this.resize = function(layer, width, height) {
        scheduleTask(function __display_resize() {

            layer.resize(width, height);

            // Resize display if default layer is resized
            if (layer === default_layer) {

                // Update (set) display size
                displayWidth = width;
                displayHeight = height;
                display.style.width = displayWidth + "px";
                display.style.height = displayHeight + "px";

                // Update bounds size
                bounds.style.width = (displayWidth*displayScale) + "px";
                bounds.style.height = (displayHeight*displayScale) + "px";

                // Notify of resize
                if (guac_display.onresize)
                    guac_display.onresize(width, height);

            }

        });
    };

    /**
     * Draws the specified image at the given coordinates. The image specified
     * must already be loaded.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y 
     *     The destination Y coordinate.
     *
     * @param {!CanvasImageSource} image
     *     The image to draw. Note that this not a URL.
     */
    this.drawImage = function(layer, x, y, image) {
        scheduleTask(function __display_drawImage() {
            layer.drawImage(x, y, image);
        });
    };

    /**
     * Draws the image contained within the specified Blob at the given
     * coordinates. The Blob specified must already be populated with image
     * data.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!Blob} blob
     *     The Blob containing the image data to draw.
     */
    this.drawBlob = function(layer, x, y, blob) {

        var task;

        // Prefer createImageBitmap() over blob URLs if available
        if (window.createImageBitmap) {

            var bitmap;

            // Draw image once loaded
            task = scheduleTask(function drawImageBitmap() {
                layer.drawImage(x, y, bitmap);
            }, true);

            // Load image from provided blob
            window.createImageBitmap(blob).then(function bitmapLoaded(decoded) {
                bitmap = decoded;
                task.unblock();
            });

        }

        // Use blob URLs and the Image object if createImageBitmap() is
        // unavailable
        else {

            // Create URL for blob
            var url = URL.createObjectURL(blob);

            // Draw and free blob URL when ready
            task = scheduleTask(function __display_drawBlob() {

                // Draw the image only if it loaded without errors
                if (image.width && image.height)
                    layer.drawImage(x, y, image);

                // Blob URL no longer needed
                URL.revokeObjectURL(url);

            }, true);

            // Load image from URL
            var image = new Image();
            image.onload = task.unblock;
            image.onerror = task.unblock;
            image.src = url;

        }

    };

    /**
     * Draws the image within the given stream at the given coordinates. The
     * image will be loaded automatically, and this and any future operations
     * will wait for the image to finish loading. This function will
     * automatically choose an appropriate method for reading and decoding the
     * given image stream, and should be preferred for received streams except
     * where manual decoding of the stream is unavoidable.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!Guacamole.InputStream} stream
     *     The stream along which image data will be received.
     *
     * @param {!string} mimetype
     *     The mimetype of the image within the stream.
     */
    this.drawStream = function drawStream(layer, x, y, stream, mimetype) {

        // If createImageBitmap() is available, load the image as a blob so
        // that function can be used
        if (window.createImageBitmap) {
            var reader = new Guacamole.BlobReader(stream, mimetype);
            reader.onend = function drawImageBlob() {
                guac_display.drawBlob(layer, x, y, reader.getBlob());
            };
        }

        // Lacking createImageBitmap(), fall back to data URIs and the Image
        // object
        else {
            var reader = new Guacamole.DataURIReader(stream, mimetype);
            reader.onend = function drawImageDataURI() {
                guac_display.draw(layer, x, y, reader.getURI());
            };
        }

    };

    /**
     * Draws the image at the specified URL at the given coordinates. The image
     * will be loaded automatically, and this and any future operations will
     * wait for the image to finish loading.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!string} url
     *     The URL of the image to draw.
     */
    this.draw = function(layer, x, y, url) {

        var task = scheduleTask(function __display_draw() {

            // Draw the image only if it loaded without errors
            if (image.width && image.height)
                layer.drawImage(x, y, image);

        }, true);

        var image = new Image();
        image.onload = task.unblock;
        image.onerror = task.unblock;
        image.src = url;

    };

    /**
     * Plays the video at the specified URL within this layer. The video
     * will be loaded automatically, and this and any future operations will
     * wait for the video to finish loading. Future operations will not be
     * executed until the video finishes playing.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} mimetype
     *     The mimetype of the video to play.
     *
     * @param {!number} duration
     *     The duration of the video in milliseconds.
     *
     * @param {!string} url
     *     The URL of the video to play.
     */
    this.play = function(layer, mimetype, duration, url) {

        // Start loading the video
        var video = document.createElement("video");
        video.type = mimetype;
        video.src = url;

        // Start copying frames when playing
        video.addEventListener("play", function() {
            
            function render_callback() {
                layer.drawImage(0, 0, video);
                if (!video.ended)
                    window.setTimeout(render_callback, 20);
            }
            
            render_callback();
            
        }, false);

        scheduleTask(video.play);

    };

    /**
     * Transfer a rectangle of image data from one Layer to this Layer using the
     * specified transfer function.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     *
     * @param {!function} transferFunction
     *     The transfer function to use to transfer data from source to
     *     destination.
     */
    this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) {
        scheduleTask(function __display_transfer() {
            dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction);
        });
    };

    /**
     * Put a rectangle of image data from one Layer to this Layer directly
     * without performing any alpha blending. Simply copy the data.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
        scheduleTask(function __display_put() {
            dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y);
        });
    };

    /**
     * Copy a rectangle of image data from one Layer to this Layer. This
     * operation will copy exactly the image data that will be drawn once all
     * operations of the source Layer that were pending at the time this
     * function was called are complete. This operation will not alter the
     * size of the source Layer even if its autosize property is set to true.
     * 
     * @param {!Guacamole.Layer} srcLayer
     *     The Layer to copy image data from.
     *
     * @param {!number} srcx
     *     The X coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcy
     *     The Y coordinate of the upper-left corner of the rectangle within
     *     the source Layer's coordinate space to copy data from.
     *
     * @param {!number} srcw
     *     The width of the rectangle within the source Layer's coordinate
     *     space to copy data from.
     *
     * @param {!number} srch
     *     The height of the rectangle within the source Layer's coordinate space to copy data from.
     *
     * @param {!Guacamole.Layer} dstLayer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The destination X coordinate.
     *
     * @param {!number} y
     *     The destination Y coordinate.
     */
    this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
        scheduleTask(function __display_copy() {
            dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y);
        });
    };

    /**
     * Starts a new path at the specified point.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the point to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the point to draw.
     */
    this.moveTo = function(layer, x, y) {
        scheduleTask(function __display_moveTo() {
            layer.moveTo(x, y);
        });
    };

    /**
     * Add the specified line to the current path.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the endpoint of the line to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the line to draw.
     */
    this.lineTo = function(layer, x, y) {
        scheduleTask(function __display_lineTo() {
            layer.lineTo(x, y);
        });
    };

    /**
     * Add the specified arc to the current path.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} y
     *     The Y coordinate of the center of the circle which will contain the
     *     arc.
     *
     * @param {!number} radius
     *     The radius of the circle.
     *
     * @param {!number} startAngle
     *     The starting angle of the arc, in radians.
     *
     * @param {!number} endAngle
     *     The ending angle of the arc, in radians.
     *
     * @param {!boolean} negative
     *     Whether the arc should be drawn in order of decreasing angle.
     */
    this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) {
        scheduleTask(function __display_arc() {
            layer.arc(x, y, radius, startAngle, endAngle, negative);
        });
    };

    /**
     * Starts a new path at the specified point.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} cp1x
     *     The X coordinate of the first control point.
     *
     * @param {!number} cp1y
     *     The Y coordinate of the first control point.
     *
     * @param {!number} cp2x
     *     The X coordinate of the second control point.
     *
     * @param {!number} cp2y
     *     The Y coordinate of the second control point.
     *
     * @param {!number} x
     *     The X coordinate of the endpoint of the curve.
     *
     * @param {!number} y
     *     The Y coordinate of the endpoint of the curve.
     */
    this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) {
        scheduleTask(function __display_curveTo() {
            layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
        });
    };

    /**
     * Closes the current path by connecting the end point with the start
     * point (if any) with a straight line.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.close = function(layer) {
        scheduleTask(function __display_close() {
            layer.close();
        });
    };

    /**
     * Add the specified rectangle to the current path.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} x
     *     The X coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} y
     *     The Y coordinate of the upper-left corner of the rectangle to draw.
     *
     * @param {!number} w
     *     The width of the rectangle to draw.
     *
     * @param {!number} h
     *     The height of the rectangle to draw.
     */
    this.rect = function(layer, x, y, w, h) {
        scheduleTask(function __display_rect() {
            layer.rect(x, y, w, h);
        });
    };

    /**
     * Clip all future drawing operations by the current path. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as fillColor()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to affect.
     */
    this.clip = function(layer) {
        scheduleTask(function __display_clip() {
            layer.clip();
        });
    };

    /**
     * Stroke the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) {
        scheduleTask(function __display_strokeColor() {
            layer.strokeColor(cap, join, thickness, r, g, b, a);
        });
    };

    /**
     * Fills the current path with the specified color. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!number} r
     *     The red component of the color to fill.
     *
     * @param {!number} g
     *     The green component of the color to fill.
     *
     * @param {!number} b
     *     The blue component of the color to fill.
     *
     * @param {!number} a
     *     The alpha component of the color to fill.
     */
    this.fillColor = function(layer, r, g, b, a) {
        scheduleTask(function __display_fillColor() {
            layer.fillColor(r, g, b, a);
        });
    };

    /**
     * Stroke the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!string} cap
     *     The line cap style. Can be "round", "square", or "butt".
     *
     * @param {!string} join
     *     The line join style. Can be "round", "bevel", or "miter".
     *
     * @param {!number} thickness
     *     The line thickness in pixels.
     *
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the stroke.
     */
    this.strokeLayer = function(layer, cap, join, thickness, srcLayer) {
        scheduleTask(function __display_strokeLayer() {
            layer.strokeLayer(cap, join, thickness, srcLayer);
        });
    };

    /**
     * Fills the current path with the image within the specified layer. The
     * image data will be tiled infinitely within the stroke. The current path
     * is implicitly closed. The current path can continue to be reused
     * for other operations (such as clip()) but a new path will be started
     * once a path drawing operation (path() or rect()) is used.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     *
     * @param {!Guacamole.Layer} srcLayer
     *     The layer to use as a repeating pattern within the fill.
     */
    this.fillLayer = function(layer, srcLayer) {
        scheduleTask(function __display_fillLayer() {
            layer.fillLayer(srcLayer);
        });
    };

    /**
     * Push current layer state onto stack.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.push = function(layer) {
        scheduleTask(function __display_push() {
            layer.push();
        });
    };

    /**
     * Pop layer state off stack.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.pop = function(layer) {
        scheduleTask(function __display_pop() {
            layer.pop();
        });
    };

    /**
     * Reset the layer, clearing the stack, the current path, and any transform
     * matrix.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to draw upon.
     */
    this.reset = function(layer) {
        scheduleTask(function __display_reset() {
            layer.reset();
        });
    };

    /**
     * Sets the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.setTransform = function(layer, a, b, c, d, e, f) {
        scheduleTask(function __display_setTransform() {
            layer.setTransform(a, b, c, d, e, f);
        });
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     *
     */
    this.transform = function(layer, a, b, c, d, e, f) {
        scheduleTask(function __display_transform() {
            layer.transform(a, b, c, d, e, f);
        });
    };

    /**
     * Sets the channel mask for future operations on this Layer.
     * 
     * The channel mask is a Guacamole-specific compositing operation identifier
     * with a single bit representing each of four channels (in order): source
     * image where destination transparent, source where destination opaque,
     * destination where source transparent, and destination where source
     * opaque.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} mask
     *     The channel mask for future operations on this Layer.
     */
    this.setChannelMask = function(layer, mask) {
        scheduleTask(function __display_setChannelMask() {
            layer.setChannelMask(mask);
        });
    };

    /**
     * Sets the miter limit for stroke operations using the miter join. This
     * limit is the maximum ratio of the size of the miter join to the stroke
     * width. If this ratio is exceeded, the miter will not be drawn for that
     * joint of the path.
     * 
     * @param {!Guacamole.Layer} layer
     *     The layer to modify.
     *
     * @param {!number} limit
     *     The miter limit for stroke operations using the miter join.
     */
    this.setMiterLimit = function(layer, limit) {
        scheduleTask(function __display_setMiterLimit() {
            layer.setMiterLimit(limit);
        });
    };

    /**
     * Removes the given layer container entirely, such that it is no longer
     * contained within its parent layer, if any.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being removed from its parent.
     */
    this.dispose = function dispose(layer) {
        scheduleTask(function disposeLayer() {
            layer.dispose();
        });
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix) to the given layer.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being distorted.
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.distort = function distort(layer, a, b, c, d, e, f) {
        scheduleTask(function distortLayer() {
            layer.distort(a, b, c, d, e, f);
        });
    };

    /**
     * Moves the upper-left corner of the given layer to the given X and Y
     * coordinate, sets the Z stacking order, and reparents the layer
     * to the given parent layer.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer being moved.
     *
     * @param {!Guacamole.Display.VisibleLayer} parent
     *     The parent to set.
     *
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     *
     * @param {!number} z
     *     The Z coordinate to move to.
     */
    this.move = function move(layer, parent, x, y, z) {
        scheduleTask(function moveLayer() {
            layer.move(parent, x, y, z);
        });
    };

    /**
     * Sets the opacity of the given layer to the given value, where 255 is
     * fully opaque and 0 is fully transparent.
     *
     * @param {!Guacamole.Display.VisibleLayer} layer
     *     The layer whose opacity should be set.
     *
     * @param {!number} alpha
     *     The opacity to set.
     */
    this.shade = function shade(layer, alpha) {
        scheduleTask(function shadeLayer() {
            layer.shade(alpha);
        });
    };

    /**
     * Sets the scale of the client display element such that it renders at
     * a relatively smaller or larger size, without affecting the true
     * resolution of the display.
     *
     * @param {!number} scale
     *     The scale to resize to, where 1.0 is normal size (1:1 scale).
     */
    this.scale = function(scale) {

        display.style.transform =
        display.style.WebkitTransform =
        display.style.MozTransform =
        display.style.OTransform =
        display.style.msTransform =

            "scale(" + scale + "," + scale + ")";

        displayScale = scale;

        // Update bounds size
        bounds.style.width = (displayWidth*displayScale) + "px";
        bounds.style.height = (displayHeight*displayScale) + "px";

    };

    /**
     * Returns the scale of the display.
     *
     * @return {!number}
     *     The scale of the display.
     */
    this.getScale = function() {
        return displayScale;
    };

    /**
     * Returns a canvas element containing the entire display, with all child
     * layers composited within.
     *
     * @return {!HTMLCanvasElement}
     *     A new canvas element containing a copy of the display.
     */
    this.flatten = function() {
       
        // Get destination canvas
        var canvas = document.createElement("canvas");
        canvas.width = default_layer.width;
        canvas.height = default_layer.height;

        var context = canvas.getContext("2d");

        // Returns sorted array of children
        function get_children(layer) {

            // Build array of children
            var children = [];
            for (var index in layer.children)
                children.push(layer.children[index]);

            // Sort
            children.sort(function children_comparator(a, b) {

                // Compare based on Z order
                var diff = a.z - b.z;
                if (diff !== 0)
                    return diff;

                // If Z order identical, use document order
                var a_element = a.getElement();
                var b_element = b.getElement();
                var position = b_element.compareDocumentPosition(a_element);

                if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1;
                if (position & Node.DOCUMENT_POSITION_FOLLOWING) return  1;

                // Otherwise, assume same
                return 0;

            });

            // Done
            return children;

        }

        // Draws the contents of the given layer at the given coordinates
        function draw_layer(layer, x, y) {

            // Draw layer
            if (layer.width > 0 && layer.height > 0) {

                // Save and update alpha
                var initial_alpha = context.globalAlpha;
                context.globalAlpha *= layer.alpha / 255.0;

                // Copy data
                context.drawImage(layer.getCanvas(), x, y);

                // Draw all children
                var children = get_children(layer);
                for (var i=0; i<children.length; i++) {
                    var child = children[i];
                    draw_layer(child, x + child.x, y + child.y);
                }

                // Restore alpha
                context.globalAlpha = initial_alpha;

            }

        }

        // Draw default layer and all children
        draw_layer(default_layer, 0, 0);

        // Return new canvas copy
        return canvas;
        
    };

};

/**
 * Simple container for Guacamole.Layer, allowing layers to be easily
 * repositioned and nested. This allows certain operations to be accelerated
 * through DOM manipulation, rather than raster operations.
 * 
 * @constructor
 * @augments Guacamole.Layer
 * @param {!number} width
 *     The width of the Layer, in pixels. The canvas element backing this Layer
 *     will be given this width.
 *
 * @param {!number} height
 *     The height of the Layer, in pixels. The canvas element backing this
 *     Layer will be given this height.
 */
Guacamole.Display.VisibleLayer = function(width, height) {

    Guacamole.Layer.apply(this, [width, height]);

    /**
     * Reference to this layer.
     *
     * @private
     * @type {!Guacamole.Display.Layer}
     */
    var layer = this;

    /**
     * Identifier which uniquely identifies this layer. This is COMPLETELY
     * UNRELATED to the index of the underlying layer, which is specific
     * to the Guacamole protocol, and not relevant at this level.
     * 
     * @private
     * @type {!number}
     */
    this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++;

    /**
     * The opacity of the layer container, where 255 is fully opaque and 0 is
     * fully transparent.
     *
     * @type {!number}
     */
    this.alpha = 0xFF;

    /**
     * X coordinate of the upper-left corner of this layer container within
     * its parent, in pixels.
     *
     * @type {!number}
     */
    this.x = 0;

    /**
     * Y coordinate of the upper-left corner of this layer container within
     * its parent, in pixels.
     *
     * @type {!number}
     */
    this.y = 0;

    /**
     * Z stacking order of this layer relative to other sibling layers.
     *
     * @type {!number}
     */
    this.z = 0;

    /**
     * The affine transformation applied to this layer container. Each element
     * corresponds to a value from the transformation matrix, with the first
     * three values being the first row, and the last three values being the
     * second row. There are six values total.
     * 
     * @type {!number[]}
     */
    this.matrix = [1, 0, 0, 1, 0, 0];

    /**
     * The parent layer container of this layer, if any.
     * @type {Guacamole.Display.VisibleLayer}
     */
    this.parent = null;

    /**
     * Set of all children of this layer, indexed by layer index. This object
     * will have one property per child.
     *
     * @type {!Object.<number, Guacamole.Display.VisibleLayer>}
     */
    this.children = {};

    // Set layer position
    var canvas = layer.getCanvas();
    canvas.style.position = "absolute";
    canvas.style.left = "0px";
    canvas.style.top = "0px";

    // Create div with given size
    var div = document.createElement("div");
    div.appendChild(canvas);
    div.style.width = width + "px";
    div.style.height = height + "px";
    div.style.position = "absolute";
    div.style.left = "0px";
    div.style.top = "0px";
    div.style.overflow = "hidden";

    /**
     * Superclass resize() function.
     * @private
     */
    var __super_resize = this.resize;

    this.resize = function(width, height) {

        // Resize containing div
        div.style.width = width + "px";
        div.style.height = height + "px";

        __super_resize(width, height);

    };
  
    /**
     * Returns the element containing the canvas and any other elements
     * associated with this layer.
     *
     * @returns {!Element}
     *     The element containing this layer's canvas.
     */
    this.getElement = function() {
        return div;
    };

    /**
     * The translation component of this layer's transform.
     *
     * @private
     * @type {!string}
     */
    var translate = "translate(0px, 0px)"; // (0, 0)

    /**
     * The arbitrary matrix component of this layer's transform.
     *
     * @private
     * @type {!string}
     */
    var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity

    /**
     * Moves the upper-left corner of this layer to the given X and Y
     * coordinate.
     * 
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     */
    this.translate = function(x, y) {

        layer.x = x;
        layer.y = y;

        // Generate translation
        translate = "translate("
                        + x + "px,"
                        + y + "px)";

        // Set layer transform 
        div.style.transform =
        div.style.WebkitTransform =
        div.style.MozTransform =
        div.style.OTransform =
        div.style.msTransform =

            translate + " " + matrix;

    };

    /**
     * Moves the upper-left corner of this VisibleLayer to the given X and Y
     * coordinate, sets the Z stacking order, and reparents this VisibleLayer
     * to the given VisibleLayer.
     * 
     * @param {!Guacamole.Display.VisibleLayer} parent
     *     The parent to set.
     *
     * @param {!number} x
     *     The X coordinate to move to.
     *
     * @param {!number} y
     *     The Y coordinate to move to.
     *
     * @param {!number} z
     *     The Z coordinate to move to.
     */
    this.move = function(parent, x, y, z) {

        // Set parent if necessary
        if (layer.parent !== parent) {

            // Maintain relationship
            if (layer.parent)
                delete layer.parent.children[layer.__unique_id];
            layer.parent = parent;
            parent.children[layer.__unique_id] = layer;

            // Reparent element
            var parent_element = parent.getElement();
            parent_element.appendChild(div);

        }

        // Set location
        layer.translate(x, y);
        layer.z = z;
        div.style.zIndex = z;

    };

    /**
     * Sets the opacity of this layer to the given value, where 255 is fully
     * opaque and 0 is fully transparent.
     * 
     * @param {!number} a
     *     The opacity to set.
     */
    this.shade = function(a) {
        layer.alpha = a;
        div.style.opacity = a/255.0;
    };

    /**
     * Removes this layer container entirely, such that it is no longer
     * contained within its parent layer, if any.
     */
    this.dispose = function() {

        // Remove from parent container
        if (layer.parent) {
            delete layer.parent.children[layer.__unique_id];
            layer.parent = null;
        }

        // Remove from parent element
        if (div.parentNode)
            div.parentNode.removeChild(div);
        
    };

    /**
     * Applies the given affine transform (defined with six values from the
     * transform's matrix).
     *
     * @param {!number} a
     *     The first value in the affine transform's matrix.
     *
     * @param {!number} b
     *     The second value in the affine transform's matrix.
     *
     * @param {!number} c
     *     The third value in the affine transform's matrix.
     *
     * @param {!number} d
     *     The fourth value in the affine transform's matrix.
     *
     * @param {!number} e
     *     The fifth value in the affine transform's matrix.
     *
     * @param {!number} f
     *     The sixth value in the affine transform's matrix.
     */
    this.distort = function(a, b, c, d, e, f) {

        // Store matrix
        layer.matrix = [a, b, c, d, e, f];

        // Generate matrix transformation
        matrix =

            /* a c e
             * b d f
             * 0 0 1
             */
    
            "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";

        // Set layer transform 
        div.style.transform =
        div.style.WebkitTransform =
        div.style.MozTransform =
        div.style.OTransform =
        div.style.msTransform =

            translate + " " + matrix;

    };

};

/**
 * The next identifier to be assigned to the layer container. This identifier
 * uniquely identifies each VisibleLayer, but is unrelated to the index of
 * the layer, which exists at the protocol/client level only.
 * 
 * @private
 * @type {!number}
 */
Guacamole.Display.VisibleLayer.__next_id = 0;